תקשורת
בסיסית בין תהליכים
שיטת העברת
מידע ותאום הנפוץ ביותר ב-unix הוא העברת מידע דרך צינור pipe ומוכר לכל משתמש
שמבצע פקודות כמו
ls –l | grep drwx
ls –l | more
וכו'.
לפני כן נקדים
לתאר עוד קריאות מערכת שאנחנו צריכים.
קריאת
המערכת dup, dup2
קריאת המערכת
dup מקבלת מספר סידורי של
ערוץ ו"משכפלת אותו" כלומר מקצה מספר סידורי נוסף לאותו ערוץ. תכונה
מרכזית שלה היא שהיא כמו open מחויבת להשתמש את במספר הערוץ
הפנוי הקטן ביותר כאשר היא נענית
לבקשה. במקרה של כשלון הוא מחזיר 1-.
ההכרזה של הקריאה היא
int dup(int oldfd);
הדרך לנתב
קלט/פלט של תוכניות, כמו שדיברנו לעיל, הוא
לפתוח את הקובץ ב-open, לסגור את הערוץ הרצוי (0,1,2) , ולשכפל אתו ב-dup, מתוך הנחה שהוא ישתמש במספר
הערוץ הרצוי לנו. אפשר לבצע את התהליך על
כל שלושת הערוצים הסטנדרטיים.
נדגים על ידי מימוש שלישי של ניתוב של hello.c כמקודם.
/* redirect3.c */
#include
<stdio.h>
#include
<fcntl.h>
void fatal(char str[])
{
fprintf(stderr, "%s\n", str);
exit(1);
}
// fatal
void sys_err(char str[])
{
perror(str);
exit(2);
}
// sys_err
int main()
{
if (fork() == 0) /* Child */
{
int fd, fd2;
if ( (fd = open("hello.txt",
O_WRONLY | O_CREAT |O_TRUNC, 0644)) == -1)
sys_err("open");
if( close(1) == -1)
sys_err("close");
fd2 = dup(fd);
if ( fd2 == -1 )
sys_err("dup");
else
if (fd2 != 1)
fatal("Unexpected dup result");
execl("./hello",
"hello", 0);
} /* if */
printf("Parent terminating\n");
}
/* main */
משמעות ריצה
ופלט כמו קודם.
למעשה ישנה
פקודת מערכת חדשה יחסית dup2 שמאפשרת לנו לבקש את מה שאנחנו
רוצים ישירות ההכרזה על dup2
הינה:
int dup2(int oldfd, int newfd);
כאשר newfd זה המספר הערץ שאנחנו
רוצים עבור הכפיל ולמרות שתשובת הפונקציה היא גם מספר הערוץ החדש. dup2 גם סוגרת את הערץ אם יש צורך בכך,
ונחסך לנו הקריאה ל-close. הסיבה היחידה שלא להשתמש בהקשר הזה ב-dup2 הוא שיש גרסאות, בעיקר ישנות, של unix שלא תומכות בה.
/*
redirect4.c */
#include
<unistd.h>
#include
<stdio.h>
#include
<fcntl.h>
void sys_err(char str[])
{
perror(str);
exit(2);
}
// sys_err
int main()
{
if (fork() == 0) /* Child */
{
int fd, fd2;
if ( (fd = open("hello.txt",
O_WRONLY | O_CREAT |O_TRUNC, 0644)) == -1)
sys_err("open");
fd2 = dup2(fd, 1);
if ( fd2 == -1 )
sys_err("dup");
execl("./hello",
"hello", 0);
} /* if */
printf("Parent terminating\n");
}
/* main */
העברת
מידע דרך צינור pipe
צינור הינו מנגנון העברת מידע מסונכרן בין 2 תהליכים דרך חוצץ בזיכרון. גודל החוצץ אינו
סטנדרטי ותלוי בדרך כלל במערכת הפעלה הספציפית. בשביל להשתמש במנגנון הזה התהליכים
חייבים להיות בעלי אב קדמון משותף. לצינור יש קצה קריאה וקצה כתיבה. ניסיון לקרוא מצינור ריק עוצר את התהליך הקורא
וגורם לו להמתין עד שיהיה לו מה לקרוא. הדבר
יקרה כאשר תהליך אחר יקרא מידע מהצינור. ניסיון לכתוב לתוך צינור מלא גורם לתהליך הכותב להיעצר
להמתנה דומה עד שיתפנה מקום ע"י כך שתהליך אחר יבצע כתיבה על אותו צינור. ברגע שנתון בצינור נקרא הוא "מתבטל".
צינור הוא
משאב שמבקשים אותו בקריאת המערכת pipe. קריאת המערכת הזו מחזירה 2 מספרים שלמים
המשמשים מספרי ערוץ לקריאה ולכתיבה. מספרי
הערוצים הללו משמשים לכתיבה וקריאה ע"י קריאות המערכת ה-write וה-read בהתאמה. ניתן גם לסגור את כל אחד מהקצוות ע"י
קריאת המערכת close.
במידה ואנחנו רוצים לנתב את הערוצים הסטנדרטיים לקצוות של צינור, הדבר אפשרי ונעשה
תוך שימוש בקריאות המערכת dup או dup2.
צינור יחיד
יהיה בדרך כלל להעברת מידע רק בכוון אחד ברגע נתון. כאשר משתמשים הצינור לעיתים קרובות שני
התהליכים, משיקולי זהירות, סוגרים כל אחד
את אחד הקצוות, למרות שזה לא חובה. יצירת
הצינור חייבת להיעשות ע"י האב הקדמון לפני היווצרות התהליכים המשתתפים (למעט האב הקדמון עצמו אם אחד
מהם). המנגנון מתבסס על כך ש-fork משכפל את הניתוב של הערוצים ו-execl
אינו משנה זאת.
קריאת המערכת pipe
קריאת המערכת pipe מבקשת הקצאה של צינור ומנתבת 2 מספרי ערוצים אחד
כקצה כתיבה ושני קצה קריאה.
ההכרזה של pipe היא כלהלן:
int pipe(int filedes[2]);
קריאת המערכת pipe מקבלת כפרמטר מערך של שני שלמים ומחזירה
בכניסה הראשונה filedes[0] את
ערוץ הקריאה מהצינור וב- filedes[1] את ערוץ הכתיבה. תשובת הפונקציה
של pipe הוא 0 במקרה של הצלחה
ו-1- במקרה של כשלון.
בתור דוגמא
נראה תוכנית המעבירה את המחרוזת "Hello World!" בית
אחר בית דרך צינור, כאשר התוכנית השולחת נרדמת ל-3 שניות לאחר הפיצול (כלומר
התוכנית הקוראת בטוח מקדימה אותה) והדבר עובד.
/*
pipe1.c - send information through pipe.
*/
#include
<stdio.h>
#include
<unistd.h>
void syserr(char str[])
{
perror(str);
exit(1);
}
/* sys_arr */
void child(int pfd[])
{
int i;
char msg[80];
if (close(pfd[1]) == -1)
syserr("close");
i = 0;
do {
if (read(pfd[0], &msg[i], 1) == -1)
syserr("read");
i++;
} while (msg[i-1] != '\0');
printf("Received = %s\n", msg);
exit(0);
}
/* child */
int main()
{
int
pfd[2];
char fdstr[10];
int i;
char str[] = "Hello World!\n";
if (pipe(pfd) == -1)
syserr("pipe");
printf("pfd[0] = %d, pfd[1] =
%d\n", pfd[0], pfd[1]);
switch(fork())
{
case -1:
syserr("fork");
case 0:
child(pfd);
default:
break;
} /* switch */
/*
parent only */
if (close(pfd[0]) == -1)
syserr("close");
sleep(3);
for(i=0; i <= strlen(str); i++)
if (write(pfd[1], &str[i], 1) == -1)
syserr("write");
return 0;
}/*
main */
פלט ריצה:
% cc pipe1.c
% ./a.out
pfd[0] = 3, pfd[1] = 4
%
Received = Hello World!
שים לב שמספרי
הערוצים שהקצו הם 3 ו-4.
התוכנית הבאה
מעבירה את ההודעה במכה אחת:
/*
pipe2.c - send information through pipe.
*/
#include
<stdio.h>
#include
<unistd.h>
void syserr(char str[])
{
perror(str);
exit(1);
}
/* sys_arr */
void child(int pfd[])
{
char msg[80];
if (close(pfd[1]) == -1)
syserr("close");
if (read(pfd[0], msg, 80) == -1)
syserr("read");
printf("Received = %s\n", msg);
exit(0);
}
/* child */
int main()
{
int
pfd[2];
char fdstr[10];
int i;
char str[] = "Hello World!\n";
if (pipe(pfd) == -1)
syserr("pipe");
printf("pfd[0] = %d, pfd[1] =
%d\n", pfd[0], pfd[1]);
switch(fork())
{
case -1:
syserr("fork");
case 0:
child(pfd);
default:
break;
} /* switch */
/*
parent only */
if (close(pfd[0]) == -1)
syserr("close");
sleep(3);
if (write(pfd[1], str, strlen(str)+1) == -1)
syserr("write");
return 0;
}/*
main */
פלט הריצה
בדיוק כמו קודם.
התוכנית הבאה
משתמשת בצינור למידע דו כיווני. שים לב
שהיינו צריכים לסנכרן ע"י sleep את
שני התהליכים, בכדי שתהליך הבן לא יקרא את מה שהוא עצמו כתב:
/*
pipe3.c - send information through pipe.
*/
#include
<stdio.h>
#include
<unistd.h>
void syserr(char str[])
{
perror(str);
exit(1);
}
/* sys_arr */
void child(int pfd[])
{
char msg[80];
char str[] = "Good Morning
Parent!\n";
if (read(pfd[0], msg, 80) == -1)
syserr("read");
printf("Received = %s\n", msg);
if (write(pfd[1], str, strlen(str)+1) == -1)
syserr("write");
exit(0);
}
/* child */
int main()
{
int
pfd[2];
char fdstr[10];
int i;
char str[] = "Good Morning
Child!\n";
char msg[80];
if (pipe(pfd) == -1)
syserr("pipe");
printf("pfd[0] = %d, pfd[1] =
%d\n", pfd[0], pfd[1]);
switch(fork())
{
case -1:
syserr("fork");
case 0:
child(pfd);
default:
break;
} /* switch */
/*
parent only */
sleep(3);
if (write(pfd[1], str, strlen(str)+1) == -1)
syserr("write");
sleep(2);
if (read(pfd[0], msg, 80) == -1)
syserr("read");
printf("Received = %s\n", msg);
return 0;
}/*
main */
פלט ריצה:
%
cc pipe3.c
pfd[0] = 3, pfd[1] = 4
Received
= Good Morning Child!
Received
= Good Morning Parent!
%
עכשיו ניגש
לראות אי מממשים רעיונות מהסוג
%
ls –l | more
%ls –l | grep drwx
וכו'.
הרעיון הוא
כמו בניתוב לקבצים לסגור את הערוצים 0 או 1 או 2 או כל צירוף שלהם ולהפנות אותם
לקצוות של צינור. אלא שכאן מוכרחים להשתמש
ב-dup או ב-dup2. זאת משום שאת הקריאה ל-pipe חייבים לעשות בתהליך האב (הקדמון)
לפני יצירת תהליך היעד, ואם נסגור את ערוץ ה-standard input או standard output או standard error לפני הקריאה ל-pipe הדבר יסגור את הערוץ הזה לא רק
לתהליך היעד, אלא גם לאב (הקדמון) דבר שבהכרח לא תמיד נוכל להרשות לעצמנו.
נראה את
המימוש של הרעיון בשני דוגמאות.
הראשונה תהיה
הסבה של pipe3.c:
/*
pipe4.c - send information through pipe.
*/
#include
<stdio.h>
#include
<unistd.h>
void syserr(char str[])
{
perror(str);
exit(1);
}
/* sys_arr */
void child(int pfd[])
{
char msg[80];
char str[] = "Good Morning
Parent!\n";
if (dup2(pfd[0], 0) == -1)
syserr("dup2");
gets(msg);
printf("Received = %s\n", msg);
sleep(1);
if (dup2(pfd[1], 1) == -1)
syserr("dup2");
printf("%s", str);
exit(0);
}
/* child */
int main()
{
int
pfd[2];
char fdstr[10];
int i;
char str[] = "Good Morning
Child!\n";
char msg[80];
if (pipe(pfd) == -1)
syserr("pipe");
printf("pfd[0] = %d, pfd[1] =
%d\n", pfd[0], pfd[1]);
switch(fork())
{
case -1:
syserr("fork");
case 0:
child(pfd);
default:
break;
} /* switch */
/*
parent only */
if (write(pfd[1], str, strlen(str)+1) == -1)
syserr("write");
sleep(2);
if (read(pfd[0], msg, 80) == -1)
syserr("read");
printf("Received = %s\n", msg);
return 0;
}/*
main */
פלט ריצה:
%
cc pipe4.c
/tmp/ccebxXrv.o(.text+0x65): In function
`child':
:
the `gets' function is dangerous and should not be used.
% ./a.out
pfd[0] = 3, pfd[1] = 4
Received
= Good Morning Child!
Received
= Good Morning Parent!
`@
%
שימו לב שהפעם
לא יכולנו להרשות לעצמנו לסגור את ה-standard
output של תהליך האב, כי אחרת הוא לא היה יכול לדווח על ההודעה שהוא
קיבל.
הדוגמה הבאה מממשת
כאלו את הפקודה
%
date | wc
1 6 29
%
כאשר date היא פקודה סטנדרטית המממשת שליחת
התאריך והשעה למסך ו-wc היא
פקודה סטנדרטית לספור שורות, מילים ו-תוים.
במקרה של
date |wc
התשובה תמיד
תהיה
1
6 29
או
1
6 28
התוכנית:
/*
pipe5.c - implement date | wc. */
#include
<stdio.h>
#include
<unistd.h>
void syserr(char str[])
{
perror(str);
exit(1);
}
/* sys_arr */
int main()
{
int
pfd[2];
int status;
if (pipe(pfd) == -1)
syserr("pipe");
switch(fork())
{
case -1:
syserr("fork");
case 0:
if (dup2(pfd[1], 1) == -1)
syserr("dup2");
if (close(pfd[0]) == -1 || close(pfd[1]) == -1)
syserr("close");
execlp("date", "date",
NULL);
syserr("execlp");
} /* switch */
switch(fork())
{
case -1:
syserr("fork");
case 0:
if (dup2(pfd[0], 0) == -1)
syserr("dup2");
if (close(pfd[0]) == -1 || close(pfd[1]) == -1)
syserr("close");
execlp("wc", "wc",
NULL);
syserr("execlp2");
} /* switch */
if (close(pfd[0]) == -1 || close(pfd[1]) == -1)
syserr("close");
while (wait(&status) != -1)
;
return 0;
}
/* main */
ריצה:
%
cc pipe5.c
% ./a.out
1 6 29
%
ה-close –ים של הקצוות של
הצינור הם הכרחיים בכדי ש-wc יקבל הודעת "סיום קלט".