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- αντικαθιστά την τρέχουσα εικόνα διεργασίας· η τυπική επόμενη κλήση σε παιδί αμέσως μετά απόforkwait- μπλοκάρει μέχρι να εξέλθει οποιοδήποτε παιδί και επιστρέφει το PID του και την κατάστασή του στο$?waitpid- συγκομίστε ένα συγκεκριμένο παιδί, προαιρετικά χωρίς μπλοκάρισμα μέσωWNOHANGexit- τερματίζει την τρέχουσα διεργασία· χρησιμοποιήστε στο παιδί για να το τερματίσετε χωρίς να πέσει στον κώδικα του γονέαpipe- δημιουργήστε ένα ζεύγος συνδεδεμένων filehandles πριν το fork για να δώσετε σε γονέα και παιδί ένα κανάλι επικοινωνίας$$- το τρέχον ID διεργασίας, διαβάζεται εκ νέου μετά απόforkώστε κάθε πλευρά να βλέπει το δικό της PID%SIG- ιδίως το$SIG{CHLD}, το οποίο ελέγχει αν τα παιδιά συγκομίζονται αυτόματα ή πρέπει να συλλεχθούν μεwait/waitpidperlipc- πληρέστερη πραγμάτευση του forking, των σημάτων, των σωλήνων και της συγκομιδής ετοιμοθάνατων παιδιών