שימוש בקריאות מערכת של UNIX בתוכניות C
יצירת תהליכים ב-UNIX – קריאת המערכת fork
ב-UNIX
הדרך שתהליך מבקש מהמערכת ליצור תהליכים היא ע"י קריאה לקריאת המערכת fork (או הרוטינה vfork, אם
כי היום ברוב המערכות הם אותה רוטינה).
ההכרזה של fork
(והוריאציות שלו) היא באופן עקרוני
extern int fork(void);
קריאה ל-fork יוצרת כפיל כמעט זהה של התהליך
הקורא. התהליך הנוצר מחויב לאותו קוד,
מקבל שטח משתנים זהה בגודלו ובערכיו של תהליך האב, וממשיך מאותו נקודה בקוד.
תהליך הבן
נבדל מתהליך האב בחלק ממאפייני התהליך (ה-pid , ה-ppid) אך אחד ההבדלים העיקריים
הוא תשובת הפונקציה של fork: תהליך הבן מקבל תשובה 0, תהליך האב מקבל את ה-pid של
תהליך הבן (שלעולם אינו 0). תשובת
הפונקציה היא האמצעי העיקרי או הכמעט יחיד שמאפשר
למתכנת התהליכים לנתב את פעולות תהליך הבן למשהו אחר מתהליך האב.
אגב, במידה ולא ניתן ליצור תהליך חדש, fork מחזיר 1-. במצב כזה כמובן היציאה מ-fork משאיר את המצב של תהליך אחד,
והתהליך היחיד הזה, זה שקרא ל-fork הוא
המקבל את ה-1-.
כאשר תהליך יוצר תהליך בן ע"י
קריאה ל-fork, המאפיינים הבאים (הרשימה
לא מלאה) משותפים לתהליך האב והבן כאשר הבן מתחיל לרוץ (זה יכול להשתנות אחר כך):
-
הבעלים של התהליכים, הרשאות, מאפייני קבוצות משתמשים, קבוצות תהליכים.
-
מאפייני סביבת ריצה, כמו מרחב כתובות, מגבלות על משאבים וכו'
-
הצבעות על קבצים שהיו פתוחים בפני תהליך האב, כולל המסך.
-
פתיחות בפני תוכניות מעקב
-
עדיפות
-
יכולת גישה למשאבי מערכת שהיו פתוחים לתהליך האב, כמו שטחי זיכרון
משותפים.
-
הספרייה הנוכחית
-
הרשאות ברירת מחדל לקבצים שהתהליכים מיצרים
-
הטרמינל ששולט בו
המאפיינים הבאים (רשימה לא
מלאה) נבדלים בן תהליך האב והבן כבר כאשר
תהליך הבן נוצר:
-
המספר המזהה של התהליך
-
המספר המזהה של תהליך האב
-
ההצבעות על קבצים פתוחים הם בשטחי זיכרון נבדלים בין האב והבן, ולכן סגירת
הקובץ בידי אחד לא יתפוס עבור השני.
-
מגבלות מסוימות, כמו מחויבויות למנגנוני נעילות של תהליך האב אינם מחייבות
את תהליך הבן.
-
סטטיסטיקות זמן כלשהם, כמו כמה זמן
התהליך קיים וכמה זמן CPU הוא
ניצל, נקבעים עבור תהליך הבן כאפס ברגע היצירה, וכמובן בלתי תלויים בערכים של
תהליך האב.
-
רשימת תהליכי בן של תהליך שנוצר היא רשימה ריקה. הרשימה של תהליך האב בכל מקרה שונה, ומכילה
לפחות את תהליך החדש שנוצר.
-
מחויבות לקלט/פלט אסינכרוני, כמו קשר קלט
מהמקלדת למשל, אינם עוברים מהאב לבן.
הרוטינה execl
אם אנחנו מעונינים שתהליך הבן יעשה
קוד אחר לחלוטין אחר מאשר תהליך האב, אנחנו בדרך כלל נשתמש בקריאת המערכת execl. הרוטינה execl משנה את הקוד לה מחויב התהליך. הפרמטר הראשון שך execl הוא
הפניה לקובץ הבינארי (החדש) של התהליך.
יתר הפרמטרים הם תאור של מה שאנחנו מכנים argv – ערכי מערך הפוינטרים
ומה שהם מצביעים אליהם בתוכנית
החדשה.
במילים אחרות ההכרזה על execl הינה:
int execl(const char *path, const char *arg,
...);
ישנה גם וריאציה המאפשרת העברה של
פוינטר למערך של פוינטרים למחרוזות:
int execv(const char *path, char *const argv[]);
ווריאציות נוספות על אותו עקרון.
הרוטינה execl
קימת בכמעט כל גירסת שפת C בכל המערכות, כולל Windows.
לשם השוואה: יצירת תהליכים
ב-C
של Windows
קריאת המערכת spawnl
ההכרזה של spawnl (באופן עקרוני) היא:
int spawnl (int mode, char *path, char *arg0, ..., NULL);
כלומר כמו execl, רק
עם פרמטר שלם נוסף mode
היכול לקבל את הערכים הבאים:
#define _P_WAIT 0
#define _P_NOWAIT 1
#define _OLD_P_OVERLAY 2
#define _P_NOWAITO 3
#define _P_DETACH 4
אם mode = P_WAIT תהליך האב ממתין (נעצר)
עד שתהליך הבן מסיים.
אם mode = P_NOWAIT תהליך האב ירוץ במקביל
לתהליך הבן.
המקרים האחרים פחות חשובים.
יש גם גרסה spawnv
המקבלת מערך פוינטרים:
int spawnv (int mode, char *path, char *argv[]);
וגרסאות נוספות.
היתרונות של fork על פני spawnl
לדעתי, fork של Unix הוא כללי וגמיש יותר מ-spawnl של Windows מן הטעמים הבאים:
1.
לא צריך לטעון קובץ מהדיסק בכדי להפעיל את תהליך הבן.
2.
אין צורך לממש או לבצע מנגנון בכדי להעביר אינפורמציה מתהליך האב לתהליך
הבן. תהליך הבן "יודע" כל מידע שהיה ידוע לתהליך האב, דרך שטח המשתנים
המשוכפל.
3.
אם מעונינים בכך, אפשר לרכז את כל הקוד של אפליקציה רב תהליכית בתוך קובץ אחד.
החסרון העיקרי של המימוש הזה של
יצירת תהליכים ששכפול שטח משתנים יכול להיות פעולה "כבדה" ומיותרת. יש פטנט לחסוך את הפעולה הזו במידה ותהליך הבן
לא משתמש בשטח המוכפל, כולו או רובו, למשל אם תהליך הבן משנה את הקוד שלו ע"י
קריאה ל-execl מיד
או כמעט מיד אחרי נקודת הפיצול. הרעיון
הזה היה בעבר ממומש תחת vfork (היום בדרך כלל גם ב-fork , אין הבדל בדרך כלל בין השנים
יותר) הוא להסתמך על מנגנון הדפדוף (paging) וכל עוד שני התהליכים (האב והבן)
רק קוראים מהשטח האמור להשתכפל, לא מתבצע שכפול של שטח. רק כאשר אחד משני התהליכים כותב לזיכרון,
הדף המתאים (בדרך כלל 4k בתים) משתכפל אם לא שוכפל
עדיין.
הרוטינה wait
הרוטינה wait של UNIX גורם לתהליך להיעצר עד שאחד
מתהליכי הבן שלו יסיים. אם אין כאילו – wait יחזור עם ערך פונקציה 1-. אם יש ואחד מהתהליכים הללו יסיים, תהליך האב
יחזור מ-wait עם תשובת פונקציה ה-pid של
תהליך הבן שסיים.
הרוטינה getpid
לכל תהליך יש מספר מזהה pid
המיחד
רק אותו. כל תהליך יכול לברר אותו ע"י קריאה
לרוטינה getpid().
אגב, כל תהליך יכול לברר מי תהליך האב שלו ע"י קריאה לרוטינה getppid().