Διεργασίες

fork#

Δημιουργεί μια νέα διεργασία που εκτελεί το ίδιο πρόγραμμα στο ίδιο σημείο.

Η fork εκδίδει την κλήση συστήματος fork(2). Μετά την επιστροφή της, δύο διεργασίες εκτελούν το ίδιο πρόγραμμα Perl στην ίδια εντολή - η αρχική (γονέας) και ένα σχεδόν πανομοιότυπο αντίγραφο (παιδί). Οι δύο διακρίνονται μόνο από την τιμή επιστροφής της fork. Οι περιγραφείς αρχείων και, σε ορισμένα συστήματα, τα κλειδώματα που κρατούνται σε αυτούς μοιράζονται μεταξύ γονέα και παιδιού· καθετί άλλο αντιγράφεται. Στο Linux η αντιγραφή είναι φθηνή: οι σελίδες δεδομένων μοιράζονται copy-on-write, οπότε η πραγματική διπλασίαση συμβαίνει μόνο καθώς κάθε πλευρά γράφει στη μνήμη.

Σύνοψη#

my $pid = fork;

Τι επιστρέφεται#

Τρεις τιμές επιστροφής, μία ανά ρόλο:

  • Γονέας: το PID του παιδιού (θετικός ακέραιος).

  • Παιδί: 0.

  • Αποτυχία: undef, με το $! ορισμένο στο errno από την υποκείμενη fork(2) (συνήθως EAGAIN - όριο πόρων που έχει εξαντληθεί - ή ENOMEM).

Η κανονική αποστολή είναι μια τριμερής διακλάδωση. Ελέγξτε πρώτα για undef, μετά για 0, ώστε η διαδρομή του γονέα να είναι η εξ ορισμού:

my $pid = fork // die "fork failed: $!";
if ($pid == 0) {
    # child
    exec $cmd, @args;
    die "exec failed: $!";
}
# parent continues here, with $pid = child's PID

Ο ιδιωματισμός δύο γραμμών «fork or die»:

defined(my $pid = fork) or die "fork: $!";

Η συγγραφή fork or die (χωρίς defined) αποτελεί σφάλμα: αντιμετωπίζει τη νόμιμη επιστροφή 0 του παιδιού ως αποτυχία και πεθαίνει σε κάθε παιδί.

Εκκενώστε πριν κάνετε fork#

Η Perl επιχειρεί να εκκενώσει όλους τους χειριστές εξόδου πριν από την κλήση fork(2), αλλά δεν θα πρέπει να βασίζεστε μόνο σε αυτό - οποιαδήποτε μη εκκενωμένα δεδομένα ενταμιευτή που τελικά διαφεύγουν διπλασιάζονται και στις δύο διεργασίες, οπότε τα ίδια bytes γράφονται δύο φορές. Η κανονική άμυνα είναι να απενεργοποιήσετε την ενταμίευση σε κάθε filehandle που σκοπεύετε να χρησιμοποιήσετε διασταυρωτά σε ένα fork:

STDOUT->autoflush(1);           # or: $| = 1 while STDOUT is selected
STDERR->autoflush(1);

print "about to fork\n";        # now actually on the wire
my $pid = fork // die "fork: $!";

Η φόρτωση του IO::Handle δεν απαιτείται για το autoflush στη σύγχρονη Perl - η μέθοδος είναι διαθέσιμη σε όλα τα filehandles. Ο ορισμός του $| στο τρέχον επιλεγμένο handle έχει το ίδιο αποτέλεσμα για αυτό το ένα handle.

Συγκομιδή παιδιών - ή $SIG{CHLD}#

Κάθε παιδί που τερματίζει πριν ο γονέας το συγκομίσει γίνεται zombie: μια εγγραφή στον πίνακα διεργασιών του πυρήνα που κρατείται ανοιχτή ώστε ο γονέας να μπορεί να διαβάσει την κατάσταση εξόδου. Αν τα αγνοήσετε, ο πίνακας διεργασιών γεμίζει.

Δύο τρόποι για να το κρατήσετε καθαρό:

  • Συγκομίστε ενεργά με wait ή waitpid:

    my $pid = fork // die "fork: $!";
    if ($pid == 0) {
        # ... child work ...
        exit 0;
    }
    my $reaped = waitpid $pid, 0;     # block until this child exits
    my $status = $?;                  # exit status of the child
    
  • Πείτε στον πυρήνα ότι δεν σας ενδιαφέρει:

    $SIG{CHLD} = 'IGNORE';            # children auto-reaped, no status
    

    Χρήσιμο για εργάτες τύπου fire-and-forget. Χάνετε την κατάσταση εξόδου - οι wait και waitpid επίσης δεν θα βλέπουν πλέον τα παιδιά.

Για γονείς μακράς διάρκειας που ενδιαφέρονται για την κατάσταση εξόδου, εγκαταστήστε έναν χειριστή συγκομιδής που αποστραγγίζει όλα τα εκκρεμή παιδιά χωρίς να μπλοκάρει:

$SIG{CHLD} = sub {
    while ((my $pid = waitpid -1, POSIX::WNOHANG) > 0) {
        # optionally inspect $? here
    }
};

Δείτε το perlipc για πληρέστερα μοτίβα.

Τι κληρονομεί το παιδί#

  • Ανοιχτοί περιγραφείς αρχείων: μοιραζόμενοι. Ένα print σε οποιαδήποτε από τις δύο διεργασίες προωθεί την ίδια υποκείμενη μετατόπιση αρχείου. Κλείστε τους περιγραφείς που δεν χρειάζεται το παιδί, και ανοίξτε εκ νέου τα handles που το παιδί δεν θα έπρεπε να μοιράζεται με τον γονέα - ιδιαίτερα τα STDIN/STDOUT/STDERR όταν είναι συνδεδεμένα σε σωλήνα ή υποδοχή που οδηγεί τον καλούντα του γονέα. Ένα σενάριο CGI σε φόντο που κάνει fork και εξέρχεται χωρίς να κλείσει το κληρονομημένο του STDOUT θα αφήσει τον πελάτη HTTP να κρέμεται, επειδή το παιδί κρατά ακόμη την υποδοχή ανοιχτή:

    open STDIN,  '<', '/dev/null' or die $!;
    open STDOUT, '>', '/dev/null' or die $!;
    open STDERR, '>', '/dev/null' or die $!;
    
  • Μνήμη: λογικά αντιγραμμένη, φυσικά copy-on-write. Η τροποποίηση μιας μεγάλης δομής δεδομένων στη μία πλευρά υλοποιεί μόνο τις σελίδες που τροποποιήθηκαν.

  • Process ID: αλλάζει. Το $$ (επίσης $PID / $PROCESS_ID υπό το English) διαβάζεται εκ νέου από τον πυρήνα κατά την πρόσβαση μετά από fork, οπότε τόσο ο γονέας όσο και το παιδί βλέπουν το δικό τους PID.

  • Χειριστές σημάτων, %ENV, κατάλογος εργασίας, umask, ομάδα διεργασιών, ελεγκτικό τερματικό: όλα αντιγράφονται.

  • Εκκρεμή alarms και χρονομετρητές: δεν κληρονομούνται από το παιδί (κατά POSIX).

  • Κλειδώματα που κρατούνται μέσω flock: μοιράζονται με τον γονέα - και οι δύο διεργασίες κρατούν το ίδιο κλείδωμα στην ίδια περιγραφή ανοιχτού αρχείου. Τα κλειδώματα που αποκτώνται μέσω κλειδωμάτων εγγραφής fcntl συμπεριφέρονται διαφορετικά· συμβουλευτείτε το fcntl(2).

Παραδείγματα#

Κλασικό fork/exec - εκκινήστε μια εξωτερική εντολή χωρίς να περάσετε από το κέλυφος:

my $pid = fork // die "fork: $!";
if ($pid == 0) {
    exec '/usr/bin/gzip', '-9', $file;
    die "exec: $!";                   # only reached if exec fails
}
waitpid $pid, 0;
die "gzip failed: $?" if $?;

Διασκορπίστε N εργάτες, μετά συγκομίστε τους όλους:

my @kids;
for my $i (1 .. 4) {
    my $pid = fork // die "fork: $!";
    if ($pid == 0) {
        do_work($i);
        exit 0;
    }
    push @kids, $pid;
}
waitpid $_, 0 for @kids;

Επικοινωνία γονέα/παιδιού μέσω σωλήνα. Ανοίξτε τον σωλήνα πριν το fork ώστε και οι δύο πλευρές να κληρονομήσουν τα δύο άκρα:

pipe(my $reader, my $writer) or die "pipe: $!";
my $pid = fork // die "fork: $!";
if ($pid == 0) {
    close $reader;
    $writer->autoflush(1);
    print $writer "hello from $$\n";
    exit 0;
}
close $writer;
chomp(my $line = <$reader>);
waitpid $pid, 0;
print "got: $line\n";

Παρατηρήστε το close στο μη χρησιμοποιούμενο άκρο σε κάθε διεργασία - αφήστε και τα δύο άκρα ανοιχτά και στις δύο διεργασίες και ο αναγνώστης δεν θα δει ποτέ EOF.

Αποσπάστε ένα παιδί τύπου δαίμονα και αφήστε τον πυρήνα να το συγκομίσει:

$SIG{CHLD} = 'IGNORE';
my $pid = fork // die "fork: $!";
if ($pid == 0) {
    # child: will be auto-reaped on exit
    run_background_task();
    exit 0;
}
# parent moves on, never calls wait

Οριακές περιπτώσεις#

  • fork μέσα σε νήμα ή μετά από κώδικα ευαίσθητο στο DESTROY: μόνο το καλούν νήμα επιβιώνει στο παιδί· οτιδήποτε περιμένει σε mutex ή συνενωμένο νήμα σε άλλο νήμα χάνεται. Κρατήστε τα σημεία fork πριν δημιουργήσετε νήματα.

  • Filehandles με ενταμιευτή στο παιδί: οποιαδήποτε δεδομένα ακόμη ενταμιευμένα τη στιγμή του fork είναι παρόντα στους ενταμιευτές και των δύο διεργασιών. Όταν κάθε πλευρά τελικά εκκενώσει, τα bytes γράφονται δύο φορές. Κάντε autoflush πριν το fork.

  • Η exit στο παιδί εκτελεί τα μπλοκ END και τους DESTROY-ers που γράφτηκαν από τον γονέα. Αυτό σπάνια είναι αυτό που θέλετε - προτιμήστε POSIX::_exit στο παιδί μετά από αποτυχία exec, ή εξασφαλίστε ότι τα μπλοκ END ελέγχουν το $$ έναντι του PID που καταγράφηκε κατά την εκκίνηση του προγράμματος.

  • Σύγχυση σφάλματος και παιδιού: η fork επιστρέφει 0 νόμιμα στο παιδί και undef σε αποτυχία. Χρησιμοποιήστε // και defined, ποτέ || / ελέγχους αλήθειας.

  • EAGAIN: παροδικό - έχει συμπληρωθεί το όριο διεργασιών ανά χρήστη του πυρήνα. Δοκιμάστε ξανά με σύντομο sleep, ή μειώστε την ταυτοχρονικότητά σας.

  • Ο κατάλογος εργασίας και το chdir στο παιδί δεν επηρεάζουν τον γονέα. Το ίδιο ισχύει για το %ENV, το umask και τους χειριστές σημάτων: το παιδί έχει το δικό του αντίγραφο από τη στιγμή του fork και μετά.

  • Οι αναζητήσεις σε κοινούς περιγραφείς μοιράζονται: αν γονέας και παιδί γράφουν και οι δύο στο ίδιο κληρονομημένο STDOUT, η έξοδός τους εναλλάσσεται ανάλογα με ό,τι αποφασίζει ο χρονοπρογραμματιστής του ΛΣ. Συντονιστείτε με ρητό κλείδωμα ή δώστε σε κάθε παιδί το δικό του handle εξόδου.

Διαφορές από το upstream#

Πλήρως συμβατό με το upstream Perl 5.42.

Η pperl στοχεύει αποκλειστικά σε Linux, οπότε η fork είναι πάντα πραγματική fork(2). Σε αντίθεση με την παραδοσιακή perl5 στα Windows - όπου η fork εξομοιώνεται με ψευδο-διεργασίες σε επίπεδο διερμηνέα που μοιράζονται μία διεργασία ΛΣ, με τις ιδιαιτερότητες που τεκμηριώνονται στο perlfork - η fork της pperl δημιουργεί μια ανεξάρτητη διεργασία ΛΣ με το δικό της PID, χώρο διευθύνσεων και εγγραφή στον πίνακα διεργασιών. Κώδικας που υπέθετε σημασιολογία ψευδο-διεργασιών (κοινή καθολική κατάσταση μεταξύ των «forked» πλευρών) δεν θα μεταφερθεί στην pperl.

Δείτε επίσης#

  • exec - αντικαθιστά την τρέχουσα εικόνα διεργασίας· η τυπική επόμενη κλήση σε παιδί αμέσως μετά από fork

  • wait - μπλοκάρει μέχρι να εξέλθει οποιοδήποτε παιδί και επιστρέφει το PID του και την κατάστασή του στο $?

  • waitpid - συγκομίστε ένα συγκεκριμένο παιδί, προαιρετικά χωρίς μπλοκάρισμα μέσω WNOHANG

  • exit - τερματίζει την τρέχουσα διεργασία· χρησιμοποιήστε στο παιδί για να το τερματίσετε χωρίς να πέσει στον κώδικα του γονέα

  • pipe - δημιουργήστε ένα ζεύγος συνδεδεμένων filehandles πριν το fork για να δώσετε σε γονέα και παιδί ένα κανάλι επικοινωνίας

  • $$ - το τρέχον ID διεργασίας, διαβάζεται εκ νέου μετά από fork ώστε κάθε πλευρά να βλέπει το δικό της PID

  • %SIG - ιδίως το $SIG{CHLD}, το οποίο ελέγχει αν τα παιδιά συγκομίζονται αυτόματα ή πρέπει να συλλεχθούν με wait / waitpid

  • perlipc - πληρέστερη πραγμάτευση του forking, των σημάτων, των σωλήνων και της συγκομιδής ετοιμοθάνατων παιδιών