תקשורת בסיסית בין תהליכים

 

שיטת העברת מידע ותאום הנפוץ ביותר ב-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 יקבל הודעת "סיום קלט".