תקשורת בין תהליכים (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);
הקריאה
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 של
שטחי הזיכרון היא
סגמנט זיכרון משותף הוא משאב שהינו שטח
בזיכרון המתקבל בהקצאה דינמית מיוחדת
ממערכת ההפעלה כך שיותר מתהליך אחד יכול לגשת לזיכרון הזה. לפיכך השטח הזה יכול לשמש להעברת אינפורמציה
בין תהליכים. צריך לזכור שכל שטח זיכרון מאחד הסוגים הסטנדרטיים נגיש אך ורק
לתהליך שיצר אותו, זה לא משנה אם מדובר
במשתנים לוקליים, גלובליים, סטטיים או
דינמיים, ושום מניפולציה של פוינטרים לא תעזור.
ההכרזה של הרוטינה שהיא גרסת ה-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 = .