שימוש בקריאות מערכת של 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

 

  בשפות תכנות ב-Windows  כמו Visual C++ קריאת המערכת ליצירת תהליכים נקראית בדרך כלל spawnl ווריאציות שלה.  זו פקודה דומה מאד ברמת הפרמטרים ל-execl של Unix  למעט פרמטר שלם נוסף.  כמובן שיש כאן הבדל עקרוני שבמקום שהתהליך הנוכחי יחליף קוד לקוד הממומש כקובץ בינארי על דיסק, כאן נוצר תהליך חדש, בלתי תלוי, שהקוד שלו הוא זה המצוין בפקודה, בעוד שהתהליך המקורי ממשיך להתקיים ולבצע את הקוד המקורי שלו.

 

 ההכרזה של 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().