fork#
יוצר תהליך חדש המריץ את אותה תוכנית באותה נקודה.
fork מנפיק את קריאת המערכת fork(2). לאחר שהיא חוזרת, שני תהליכים מבצעים את אותה תוכנית Perl באותו משפט - המקורי (הורה) ועותק כמעט זהה (ילד). השניים נבדלים רק לפי ערך ההחזרה של fork. descriptors של קבצים, ובמערכות מסוימות גם הנעילות המוחזקות עליהם, משותפים בין הורה לילד; כל השאר מועתק. ב־Linux ההעתקה זולה: דפי נתונים משותפים בהעתקה־בכתיבה, כך שהשכפול בפועל מתרחש רק כאשר כל צד כותב לזיכרון.
תקציר#
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 הלגיטימית של הילד ככשל ומתה בכל ילד.
לעשות flush לפני fork#
Perl מנסה לעשות flush לכל מטפלי הפלט לפני הקריאה ל־fork(2), אך אין להסתמך על כך לבדו - נתונים מאוחסנים ב־buffer שלא עברו flush ומשתחלים דרכם משוכפלים בשני התהליכים, כך שאותם בייטים נכתבים פעמיים. ההגנה הקנונית היא לבטל buffering על כל מטפל שמתכוונים להשתמש בו לרוחב 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 מודרני - המתודה זמינה בכל מטפלי הקובץ. הגדרת $| על המטפל הנבחר הנוכחי בעלת אותה השפעה עבור אותו מטפל יחיד.
איסוף ילדים - או $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
שימושי לעובדי שגר־ושכח. מאבדים את סטטוס היציאה -
waitו־waitpidגם לא יראו עוד את הילדים.
עבור הורים ארוכי־טווח שכן אכפת להם מסטטוס היציאה, יש להתקין מטפל אוסף שמרוקן את כל הילדים הממתינים מבלי לחסום:
$SIG{CHLD} = sub {
while ((my $pid = waitpid -1, POSIX::WNOHANG) > 0) {
# optionally inspect $? here
}
};
ראו perlipc לדפוסים מלאים יותר.
מה הילד יורש#
descriptors של קבצים פתוחים: משותפים.
printבכל אחד מהתהליכים מקדם את אותו offset קובץ בסיסי. יש לסגור את ה־descriptors שהילד אינו זקוק להם, ולפתוח מחדש מטפלים שהילד לא צריך לחלוק עם ההורה - בפרטSTDIN/STDOUT/STDERRכשהם מחוברים ל־pipe או socket שמניע את הקורא של ההורה. סקריפט CGI ברקע שעושה fork ויוצא מבלי לסגור את ה־STDOUTשירש ישאיר את לקוח ה־HTTP תלוי, כי הילד עדיין מחזיק את ה־socket פתוח:open STDIN, '<', '/dev/null' or die $!; open STDOUT, '>', '/dev/null' or die $!; open STDERR, '>', '/dev/null' or die $!;
זיכרון: לוגית מועתק, פיזית בהעתקה־בכתיבה. שינוי מבנה נתונים גדול בצד אחד ממש רק את הדפים שנגעו בהם.
מזהה תהליך: משתנה.
$$(גם$PID/$PROCESS_IDתחתEnglish) נקרא מחדש מהקרנל בעת גישה לאחר fork, כך שגם ההורה וגם הילד רואים את ה־PID שלהם.מטפלי אות,
%ENV, ספריית עבודה, umask, קבוצת תהליכים, טרמינל שולט: הכל מועתק.התראות וטיימרים ממתינים: אינם עוברים בירושה לילד (לפי POSIX).
נעילות המוחזקות דרך
flock: משותפות עם ההורה - שני התהליכים מחזיקים אותה נעילה על אותו תיאור קובץ פתוח. נעילות שנרכשו דרך נעילות רשומות שלfcntlמתנהגות אחרת; יש לעיין ב־fcntl(2).
דוגמאות#
fork/exec קלאסי - הפעלת פקודה חיצונית מבלי לעבור דרך ה־shell:
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;
תקשורת הורה/ילד דרך pipe. יש לפתוח את ה־pipe לפני ה־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בתוך thread או אחרי קוד רגיש ל־DESTROY: רק ה־thread הקורא שורד בילד; כל מה שממתין על mutex או thread מצורף ב־thread אחר נעלם. יש להחזיק נקודות fork לפני יצירת threads.מטפלי קובץ עם buffer בילד: כל נתון שעדיין ב־buffer ברגע ה־fork נוכח בשני ה־buffers של התהליכים. כאשר כל צד עושה flush בסופו של דבר, הבייטים נכתבים פעמיים. יש לעשות autoflush לפני ה־fork.
exitבילד מריץ בלוקיENDו־DESTROY-ים שנכתבו על ידי ההורה. זה לעיתים רחוקות מה שרוצים - מומלץPOSIX::_exitבילד לאחר כשל ב־exec, או לוודא שבלוקיENDבודקים את$$מול ה־PID שנרשם בתחילת התוכנית.בלבול שגיאה מול ילד:
forkמחזיר0באופן לגיטימי בילד ו־undefבכשל. יש להשתמש ב־//וב־defined, לעולם לא ב־||/ בדיקות אמת.EAGAIN: זמני - הוגעה למגבלת התהליכים של הקרנל לכל משתמש. יש לנסות שוב עם sleep קצר, או להוריד את רמת המקביליות.ספריית עבודה ו־
chdirבילד אינן משפיעות על ההורה. אותו דבר נכון לגבי%ENV,umask, ומטפלי אותות: לילד יש עותק משלו מרגע ה־fork והלאה.Seeks על descriptors משותפים הם משותפים: אם הורה וילד כותבים שניהם לאותו
STDOUTשנירש, הפלט שלהם משתלב בכל מה ש־scheduler מערכת ההפעלה מחליט. יש לתאם עם נעילה מפורשת או לתת לכל ילד מטפל פלט משלו.
הבדלים מהמקור#
תואם במלואו ל־Perl 5.42 במקור.
pperl מכוון ל־Linux בלבד, כך ש־fork הוא תמיד fork(2) אמיתי. שלא כמו perl5 מסורתי ב־Windows - שבו fork מדומה עם פסאודו־תהליכים ברמת המפרש החולקים תהליך OS יחיד, עם המוזרויות המתועדות ב־perlfork - ה־fork של pperl יוצר תהליך OS עצמאי עם PID משלו, מרחב כתובות, ורשומה בטבלת התהליכים. קוד שהניח סמנטיקת פסאודו־תהליך (מצב גלובלי משותף לרוחב הצדדים ה־“forked“) לא יועבר ל־pperl.
ראו גם#
exec- מחליף את תמונת התהליך הנוכחית; הקריאה הטיפוסית הבאה בילד מיד אחריforkwait- חוסם עד שילד כלשהו יוצא ומחזיר את ה־PID והסטטוס שלו ב־$?waitpid- אוסף ילד מסוים, אופציונלית ללא חסימה דרךWNOHANGexit- מסיים את התהליך הנוכחי; יש להשתמש בילד כדי לסיים אותו מבלי ליפול לקוד של ההורהpipe- יוצר זוג מטפלי קובץ מחוברים לפני fork כדי לתת להורה ולילד ערוץ תקשורת$$- מזהה התהליך הנוכחי, נקרא מחדש אחריforkכך שכל צד רואה את ה־PID שלו%SIG- בפרט$SIG{CHLD}, השולט אם ילדים נאספים אוטומטית או חייבים להיאסף עםwait/waitpidperlipc- טיפול מלא יותר ב־fork, אותות, pipes, ואיסוף ילדים גוססים