תהליכי משנה ו־co-processes#

משימת ה־IPC הנפוצה ביותר היא הפשוטה ביותר: הרצת פקודה חיצונית ותקשורת איתה. עמוד זה מכסה את כל הטווח, מ־pipe open חד־כיווני ועד co-process דו־כיווני - ילד שאליו גם כותבים וגם ממנו קוראים.

מנגנון ה־pipe open החד־כיווני ("-|" ו־"|-") מתואר במלואו בעמוד Pipes. עמוד זה ממשיך מהנקודה שבה ההוא עוצר: כאשר כיוון אחד אינו מספיק, וכאשר נדרשים pipe ו־fork גולמיים משלך.

כיוון אחד: ה־pipe open#

כדי לקרוא את הפלט של פקודה, או כדי להזין לפקודה את הקלט שלה, pipe open הוא כל מה שנדרש:

open my $fh, "-|", "sort", "-u", "names.txt" or die "pipe: $!";
while (my $line = <$fh>) { ... }
close $fh;

זהו כל הסיפור לעבודה חד־כיוונית, והוא מסופר בעמוד Pipes - כולל ההבדל בין צורת הרשימה לצורת המחרוזת, מדוע צורת הרשימה עוקפת את ה־shell, וכיצד לפענח את סטטוס היציאה של הילד. התחל שם אם כיוון אחד מספיק.

שני הכיוונים: IPC::Open2#

כאשר נדרש לכתוב לפקודה וגם לקרוא את תשובתה - מסנן שמפעילים באופן אינטראקטיבי, co-process - pipe אחד אינו מספיק. IPC::Open2 פותח שניים: אחד אל תוך ה־stdin של הילד, ואחד החוצה מה־stdout שלו.

use IPC::Open2;

my $pid = open2(my $from_child, my $to_child, "tr", "a-z", "A-Z");

print $to_child "hello\n";
close $to_child;                     # signal end-of-input to the child

my $reply = <$from_child>;
print "got: $reply";                 # got: HELLO

waitpid($pid, 0);

open2 מחזיר את ה־process id של הילד. סדר הארגומנטים הוא המלכודת שכדאי לזכור: מטפל הקריאה תחילה, מטפל הכתיבה שני, ולאחר מכן הפקודה. open2(OUT, IN, ...) - המטפל שממנו קוראים מופיע לפני המטפל שאליו כותבים.

ה־deadlock שיש לתכנן סביבו#

co-process עלול להיכנס ל־deadlock, ו־IPC::Open2 אינו עושה דבר כדי למנוע זאת. צורת המלכודת:

  • אתה כותב בלוק גדול אל הילד.

  • הילד קורא חלק, מייצר פלט, והפלט הזה ממלא את ה־pipe חזרה אליך.

  • הילד נחסם בכתיבה ל־pipe מלא; אתה נחסם בכתיבה לילד שהפסיק לקרוא. אף אחד אינו מתקדם.

המתכון שלמעלה עוקף זאת על־ידי סגירת $to_child לפני הקריאה - שליחת כל הקלט, לאחר מכן איתות סוף־קובץ, ולאחר מכן ניקוז התשובה. זה עובד כאשר הקלט נכנס בנוחות לבופר ה־pipe. עבור streaming בלתי חסום בשני הכיוונים בו־זמנית, נדרשים קלט/פלט לא־חוסם או לולאת select; צורת שני ה־pipe לבדה אינה מספיקה.

לכידת שגיאת התקן גם כן: IPC::Open3#

IPC::Open2 מחבר אותך אל ה־stdin וה־stdout. כאשר נדרש גם ה־stderr של הילד על מטפל משלו, IPC::Open3 מוסיף שלישי:

use IPC::Open3;
use Symbol qw(gensym);

my $err = gensym;                    # a fresh anonymous handle for stderr
my $pid = open3(my $to_child, my $from_child, $err, "sort");

print $to_child "banana\napple\ncherry\n";
close $to_child;

my @sorted = <$from_child>;          # ("apple\n", "banana\n", "cherry\n")
print @sorted;                       # apple / banana / cherry, one per line

waitpid($pid, 0);

שני הבדלים מ־open2 שכדאי לשים לב אליהם:

  • סדר הארגומנטים מתהפך. open3 מקבל את מטפל הכתיבה תחילה, לאחר מכן את מטפל הקריאה, ולאחר מכן את מטפל השגיאות: open3(IN, OUT, ERR, ...). זהו ההפך מסדר הקריאה־תחילה של open2 - חוסר עקביות מושרש שמכשיל כל אחד פעם אחת.

  • מטפל השגיאות זקוק ל־gensym. העבר מטפל אנונימי רענן מ־Symbol::gensym עבור stderr; bareword או משתנה לקסיקלי לא־מוגדר לא יאסוף אותו באופן נקי.

לבנות בעצמך: pipe ו־fork#

כאשר הפקודה אינה תוכנית חיצונית אלא קוד Perl שברצונך להריץ בתהליך נפרד, בנה את הערוץ בעצמך. pipe גולמי מספק זוג קורא/כותב מחובר לפני ה־fork; לאחר fork כל צד שומר את הקצה שהוא זקוק לו וסוגר את האחר:

pipe(my $reader, my $writer) or die "pipe: $!";

my $kid = fork() // die "fork: $!";

if ($kid == 0) {
    # Child writes, then exits.
    close $reader;                   # child has no use for the read end
    $writer->autoflush(1);
    print $writer "down-the-pipe\n";
    close $writer;
    exit 0;
}

# Parent reads.
close $writer;                       # parent has no use for the write end
my $line = <$reader>;
print "parent read: $line";          # parent read: down-the-pipe
close $reader;

waitpid($kid, 0);

המשמעת זהה לזו של השרת המבצע fork בעמוד TCP sockets: כל צד סוגר את הקצה שאינו משתמש בו. אם ההורה משאיר את קצה הכתיבה פתוח, ה־close של הילד לעולם אינו מייצר סוף־קובץ בקצה הקריאה, והקריאה של ההורה נחסמת לנצח.

fork() מפורש יוצר תהליך נפרד של מערכת ההפעלה; ה־auto-parallelism של PetaPerl הוא מאגר threads בתוך־התהליך. ילד שנוצר ב־fork מריץ את הצמצומים המספריים האוטו־מקביליים שלו כרגיל בתוך עצמו, וההקבלה של ההורה ושל הילד אינן מפריעות זו לזו - אלה תהליכים עצמאיים עם מאגרי threads עצמאיים. עבור הרצת עבודה על פני ליבות CPU בתוך תהליך אחד במקום על פני תהליכים נפרדים, ראה את מדריך Concurrent Execution.

הבא#

עבור שתי תוכניות שאינן קשורות שנפגשות בנתיב במערכת הקבצים ולא דרך הורה משותף, ראה FIFOs.