תקשורת בין תהליכים (Inter Process Communication - IPC) ב-UNIX

 

   בכדי לממש אפליקציות רב תהליכיות יש צורך לממש אמצעי העברת אינפורמציה וסנכרון בין תהליכים.  בגרסאות ה-UNIX הראשונות הדבר היחיד שנתמך היה מושג ה-pipes  הקים גם היום אך היה מוגבל מאד בכך שהוא יכול להעביר מידע בין 2 תהליכים בדיוק, שחייבים להיות תהליכים קרובים (בעלי אב קדמון משותף).  תקשורת בין תהליכים הוכנס ל-UNIX מאוחר יותר, בסטנדרד של UNIX שנקרא System V.  האמצעים שבהם מדובר הם שטחי זיכרון משותפים, הודעות וסמפורים.  האמצעים הללו מתוכננים באופן כללי ביותר, כל אחד עם אופציות רבות.

 

   למשאבים הללו יש מעין מבנה משותף.  כולם בחזקת משאב שיש לבקש אותו ממערכת ההפעלה, הם מזוהים לפי מספר מזהה, כל תהליך יכול להתחבר למשאב דרך המספר הזה, והמשאב הזה משתחרר רק כאשר משחררים אותו בקוד מפורש (או reboot של המערכת).

 

  אפשר לחלק את קריאות המערכת של המשאבים האלו לשלושה סוגים:

 

1.    גרסת ה-get – בקשה להקצאת משאב.

2.    גרסת ה-ctl – בקרה על המשאב (כולל שחרור).

3.    גרסאות ספציפיות לאופי המשאב.

 

לדוגמא, גרסת ה-get לשטחי זיכרון משותפים נקרא shmget, גרסת ה-ctl נקרא shmctl, והרוטינות הספציפיות הן shmat ו-shmdt.

 

  גרסאות ה-get.

 

   עבור רוטינות ה-get , רשימות הפרמטרים שלהן אינן זהות לחלוטין, אך בשלושת המקרים יש להן שני פרמטרים שלמים משותפים: key ו-flags.

 

למשל עבור לשטחי זיכרון משותפים ההכרזה של shmget הוא משהו כמו

 

int shmget(int key, int size, int flags);

 

 

בכדי להבין את הפרמטר key, צריך להבין את ייעודו:  משאבי ה-IPC אמורים להיות כה כלליים, שהמידע יוכל לעבור בין תהליכים כלשהם, לאו דווקא תהליכים קרובים  או אפילו תהליכים שאינם של אותו user.  אבל בשביל לעשות זאת, איך יוכלו התהליכים הללו להתחבר לאותו משאב?  הדרך לעשות זאת הוא שהתהליכים הללו ידעו את המספר המזהה של המשאב.  אבל מאחר והמספר הזה יקבע רק בזמן הריצה של התהליכים, לפי מה שפנוי באותו רגע.  איך אפשר לתאם זאת בזמן כתיבת הקוד של התהליכים?  אם אפשר לשתף מידע בין התהליכים, כמו למשל שלכל התהליכים יש אב (קדמון) משותף, המנגנון של fork  מאפשר לנו לדאוג שלכל התהליכים יהיה את המספר המזהה.  ואכן במקרה הזה, ערך פרמטר ה-key יהיה אפס (או השם הסמלי IPC_PRIVATE) שמשמעותו "תקצה לי משאב שהיה עד עכשיו פנוי".

 

 אבל איך ניתן להעביר את המספר המזהה לתהליכים שאין להם אב קדמון משותף? לבעיה הזו אין פתרון מושלם אבל יש לו פתרון שיש סיכוי סביר שיצליח.  בוחרים איזה שהוא מספר 32 ביט, שונה מאפס, רצוי מספר גדול, ומקווים שאף אפליקציה פעילה אחרת בחרה אותה.  המספר הזה ישמש כמעין "כינוי" למשאב, והוא יוצב לתוך שדה ה-key,  בקריאה היוצרת את המשאב (בפועל מבקשת את הקצאת המשאב).   חלק ממשמעות הבקשה היא שערך ה-key ישמש שם משני. ניתן ע"י קריאות מתאימות לגרסת ה-get שמשמעותם מציאת מספר המזהה של משאב שהוקצה קודם, שהשם המשני שלו הוא כך וכך.

    לפיכך, קריאה לגרסת ה-get של המשאב יכולה להיות בעיקר בעלת אחת מהמשמעיות הבאות:

 

1.    בקשה להקצאת משאב שעד עכשיו היה פנוי, ללא שם משני.

2.    בקשה להקצאת משאב שעד עכשיו היה פנוי, עם שם משני, שבמידה והשם המשני הזה כבר בשימוש, אין להיענות לבקשה, החזר הודעת שגיאה.

3.    בקשה לבדיקה האם יש משאב עם שם משני כך וכך, ואם כן רק החזר את המספר המזהה.  אחרת, הקצה את המשאב והחזר את המספר המזהה.

4.    בקשה לבדיקה האם יש משאב עם שם משני כך וכך, ואם כן רק החזר את המספר המזהה.  אחרת, החזר הודעת שגיאה.

 

הדרך שרוטינות ה-get מבדילות בין סוגי הבקשות הוא דרך הערך של פרמטר ה-flags.   לפרמטר ה-flags  2 תפקידים:  לקבל את ההרשאות (9 ביטים) של המשאב במידה ויוצרים אותו,   ההרשאות משמשות בעיקר  לקובע אם תהליכים של user –ים אחרים יכולים לגשת למשאב,  לקריאה בלבד או לקריאה וכתיבה.  נוסף לכך הקורא לרוטינת ה-get יכול להדליק 2 ביטים נוספים, שיש להם משמעות רק אם key  שונה מאפס:  IPC_CREAT ו-IPC_EXCL. הראשון קובע "אם יש צורך, צור את המשאב" והשני "צור את המשאב רק אין משאב קיים בשם המשני הזה". לפיכך אפשר לציין איזה ערכים יש לתת לפרמטרים בכדי לקבל את האפקטים 1 עד 4:

 

1.     קריאה לגרסת ה-get עם key = 0  (או באופן שקול key = IPC_PRIVATE).

2.     קריאה לגרסת ה-get עם key≠0, ועם flags הכולל IPC_CREAT | IPC_EXCL.

3.     קריאה לגרסת ה-get עם key≠0, ועם flags הכולל IPC_CREAT.

4.     קריאה לגרסת ה-get עם key≠0 בלבד.

 

 לדוגמא, עבור סגמנטי זיכרון משותפים, הקריאה

 

Sid = shmget(IPC_PRIVATE, size, 0600);

 

היא בקשה ליצירת משאב סגמנט זיכרון מסוג 1, סגמנט חדש שהמספר המזהה שלו היה חופשי עד עכשיו, ללא שם משני, עם הרשאות 0600.

    הקריאה

 

Sid = shmget(5656, size, 0600 | IPC_CREAT | IPC_EXCL);

 

היא בקשה ליצירת משאב סגמנט זיכרון מסוג 2, סגמנט חדש שהמספר המזהה שלו היה חופשי עד עכשיו,  משני  עם שם משני 5656, בתנאי שבמערכת אין סגמנט קיים עם שם משני 5656, אחרת החזר 1-.

  הקריאה

 

Sid = shmget(5656, size, 0600 | IPC_CREAT );

 

היא בקשה  מסוג  3,  קודם כל לבדוק האם קיים סגמנט זיכרון משותף  עם שם משני  5656, אם כן רק החזר את המספר המזהה שלו, במידה ולא – תיצור אותו ותחזיר את המספר המזהה החדש (שלא היה בשימוש עד עכשיו).

 

 

הקריאה

 

sid = shmget(5656, size, 0600 );

 

היא בקשה  מסוג  4,  אך ורק לבדוק האם קיים סגמנט זיכרון משותף  עם שם משני  5656, אם כן רק החזר את המספר המזהה שלו, במידה ולא – תחזיר

 1-.

 

גרסאות ה-ctl

 

    גרסאות ה-ctl מבצעות פעולות בקרה על המשאב.  הפרמטרים הם די סטנדרטיים:  מספר המזהה של המשאב (תוצאת הקריאה לגרסת ה-get), דגל של הפעולה על המשאב (למשל שחרור, קריאת ערכי מאפיינים, הצבת ערך חדש למאפיינים),  ופוינטר לרשומה מיוחדת המכילה שדות לאכסון מאפיינים של המשאב הניתנים לקביעה או שינוי. אנחנו נשתמש רק באופצית השחרור. 

  לדוגמא, ההכרזה של גרסת ה-ctl של שטחי הזיכרון היא

 

int shmctl( int shmid, int cmd, struct shmid_ds *buf);

 

 

סגמנט  זיכרון משותפים

 

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

 

   ההכרזה של הרוטינה שהיא גרסת ה-get, היא

 

int shmget(int key, int size, int flags);

 

כאשר הפרמטר size קובע את הגודל של השטח בבתים (כמו הפרמטר של malloc).  הפרמטרים key ו-flags הם סטנדרטיים.

 

הרוטנות היחודיות הו shmat  ו-shmdt.   ההכרזות שלהם הן:

 

void *shmat(int shmid, cont void *addr, int shmflg);

 

int shmdt(const void *addr);

 

תפקידו של shmat הוא להחזיר פוינטר לשטח הזיכרון.  המנגנון כביכול מבצע תהליך של malloc בשני שלבים: shmget מחזיר את המספר המזהה של המשאב ו- shmat את פוינטר לשטח.  מעבר לנקודה הזו הקוד המתיחס לשטח הזה הוא כאל כל שטח שמצביע עליו פוינטר ב-C, רק שיכול להיות שיותר מתהליך אחד יפנה לשטח הזה. הפרמטר shmid הוא מספר המזהה של המשאב, הפרמטר addr אם שונה מאפס הוא בקשה למערכת לקבוע ערך מסוים (הערך addr עצמו) בתור פוינטר לשטח הסגמנט.  השאלה אם זה אפשרי או לא תלוי איך בנויות הכתובות במערכת (אין לזה סטנדרד). הפרמטר shmflg אם שונה מאפס מאפשר למערכת עיגול של addr אם addr שונה מאפס ואם יש צורך בכך, או לקבוע את הפוינטר כפוינטר לקריאה בלבד (גם אם ההרשאות מתירות כתיבה).

 

תפקידו של shmdt להפסיק את המיפוי של הפוינטר לשטח (גישה דרך הפוינטר מעתה ואילך תהיה בלתי חוקית).  זה לא משפיע על המשאב עצמו, רק על היכולת של התהליך לגשת אליו.  זהו מאין אמצעי לשיפור אמינות של תוכניות: למנוע גישות לא מכוונות למשאב.

 

הודעות ב-UNIX

 

   הודעה ב-IPC של System V הוא למעשה מערך של בתים בגודל רצוי, מעין שטח דינמי של אינפורמציה.  היעד של ההודעה היא  משאב של  המערכת הנקרא תור הודעות (למעשה מאגר של תורי הודעות).  כל הודעה נשלחת כחבילה יחד עם מספר שלם המציין את הסוג של ההודעה.  על פי רוב הדבר נעשה ע"י הגדרת רשומה הכוללת מספר שלם + מערך. גם כאשר תהליך מנסה לקרוא הודעה מהתור, הוא חייב לצין  מספר סוג.  כאשר תהליך מנסה לקרוא הודעה, רק הודעות מהסוג שציין הם רלוונטיים, אם אין כזה, המשמעות של הניסיון לקריאה הוא לתור הודעות ריק, גם אם יש בתור 100 הודעות בעלי מאפייני סוג אחרים.  משמעות הדבר היא שאפליקציה המשתמשת בהודעות למספר כלשהוא של מטרות אינה צריכה, ברמה העקרונית, לבקש  מהמערכת יותר מתור הודעות אחד, משום שהתור הוא בעצם מאגר הודעות.

 

  ההגדרה של גרסת ה-get כאן הינה:

int msgget(int key, int flags);

כלומר כאן יש רק את הפרמטרים הסטנדרטיים.  כנ"ל גם לגבי גרסת ה-ctl שההגדרה שלה היא

 

int msgctl(int msgid, int cmd, struct msqid_ds *buff);

 

הרוטינות הספציפיות לסוג המשאב,  הן msgsnd רוטינת שליחת הודעה ו-msgrcv רוטינה קריאת הודעה.

 

ההכרזה של msgsnd היא:

 

int msgsnd(int msgid, void *msgptr, int msgsize,  int msgflg);

 

כאשר msgid הוא המספר המזהה של תור ההודעות, msgsize הוא גודל ההודעה (לא כולל שדה הסוג שנכלל בה), הפוינטר  msgptr מצין מאיפוא להעתיק את ההודעה ו-msgflg מצין בחירת אופצית המתנה שמתוארות בהמשך.

 

  הפרמטר  msgptr מצביע, בדרך כלל, על משתנה רשומה המכיל משתנה סוג ומערך של אינפורמציה.  הפרמטר msgsize מידע את הקוד של המערכת באיזה גודל של שטח מדובר.  מאחר וכל הודעה כוללת בהתחלה את שדה הסוג, msgsize מצין רק את גודל השטח של האינפורמציה. משום כך לעיתים קרובות רואים בשדה הזה ביטוי נוסח

 sizeof(MSGREC) – sizeof(long int)

 

הפרמטר msgflg מצין אופציית המתנה.  ישנן 2 אופציות, שמשמעותן בחירה של התהליך השולח האם להמתין עד שההודעה תקרא או לחזור מיד.  בקוד זה בא לידי ביטוי ע"י הצבת הערך IPC_NOWAIT או אפס. בחירת הערך IPC_NOWAIT  פירושו לא להמתין.  אפס פירושו להמתין.

 

תוצאת הפונקציה היא 0 או 1-, בכדי לצין הצלחה או כשלון (למשל מידע שגוי  במספר המזהה של התור ההודעות, נניח אם הוא לא פעיל).

 

ההכרזה של הרוטינה msgrcv הינה

 

int msgrcv (int msgid, void *msgptr, int msgsize,

long int msgtype,  int msgtype);

 

כאשר msgid הוא המספר המזהה של תור ההודעות, msgptr הוא פוינטר למשתנה של התוכנית הקוראת לאן להעתיק את ההודעה (כולל שדה הסוג), msgtype  הוא סוג ההודעה שאותו מעונין הקורא ל-msgrcv, הערך msgsize הוא הגודל המרבי של הודעה שהתהליך הקורא ערוך לקרוא (לא כולל שדה הסוג).

הפרמטר  msgflg מצין אופציות נוספות:  האם להמתין להודעה במידה ואין הודעה מהסוג הזה כרגע (אפס או IPC_NOWAIT בהתאמה)  והאם להעביר הודעה מקוצצת או הודעת שגיאה במקרה של הודעה החורגת מ-msgsize (MSG_NOERROR או אפס, בהתאמה).

 

סמפורים

 

   הסמפורים ב-System V  בנויים כמו המשאבים הקודמים: הרבה אופציות וניסיון ליצור משאב שדי בהקצאה אחת שלו בשביל לענות על צורכי  האפליקציה. בשל כך כל פניה להקצאת משאב סמפור מקצה לא סמפור בודד אלא מערך של סמפורים.  זאת משום שאפליקציה שזקוקה למשאב הסמפור בדרך כלל זקוקה ליותר מסמפור אחד.

 

  ההכרזה של גרסת ה-get של משאב הסמפור הינה

 

int semget(int key, int nsems, int  flags);

 

שבהם הפרמטרים key ו-  flags הם הפרמטרים הסטנדרטיים, ואילו nsems הגודל של מערך או קבוצת הסמפורים.  כלומר המספר המזהה שיוחזר ע"י semget יהיה משאב של קבוצת סמפור, ממוספרים nsems-1 0 היכולים לשמש למטרות בלתי תלויות לחלוטין אחד מהשני.

 

הפרמטרים של גרסת ה-ctl הם קצת לא סטנדרטיים בגלל הגמישות של המשאב.

 ההגדרה של גרסת ה-ctl הינה

 

int semctl(int semid, int semnum, int cmd, …);

 

כאשר semid  הוא המספר המזהה של מערך הסמפורים, semnum גודל המערך, cmd קוד פקודה.  למשאב הסמפורים יש הרבה אופציות ולכן הפרמטר הבא הוא לא תמיד רשומת המאפיינים.   זה יהיה כך בשימוש היחיד שאנחנו נעשה בו (שחרור המשאב).

 

הפעולות על הסמפורים נעשים ע"י הפונקציה הייחודית semop.

 

השימוש ב-semop מבוסס על רשומת פעולה  struct sembuf המגדירה פעולה יחידה על סמפור. ההגדרה  של struct sembuf הינה:

 

struct sembuf

{

   short int sem_num;

   short int sem_op;

   short int sem_flg;

}

 

השדה sem_num מצין את האינדקס של הסמפור בתוך מערך הסמפור.

אשר לשני השדות האחרים, המשמעות שלהם מאד מורכבת, אני אסתפק בציון העובדה שהצבת הערכים

 

sem_op = -1;

sem_flg = 0;

 

יש לה את המשמעות  של הרוטינה wait של xinu, והערכים

 

sem_op = 1;

sem_flg = 0;

יש לה את המשמעות של הרוטינה signal  של xinu.

 

ההגדרה של הרוטינה semop הינה

 

int semop(int semid, struct sembuf *sops, unsigned nsop);

 

כאשר semid הוא המזהה של מערך הסמפורים,

sops הוא מצביע למערך של רשומות פעולה על סמפורים,

ו-nsop הוא גודל המערך.

כלומר, קריאה ל-semop מאפשר לנו לבצע סידרה של פעולות בגודל רצוי, על כמה סמפורים שנרצה. אנחנו נשתמש בשימוש הפשוט של

1nsop = .