מערכות הפעלה תהליכים ב- Linux תרגול 3
תוכן התרגול מבוא לתהליכים ב- Linux API לעבודה עם תהליכים מבוא לניהול תהליכים בתוך הגרעין )process descriptor( מתאר התהליך )process list( רשימת התהליכים מאגר התהליכים המוכנים לריצה )"הטווח הקצר"( תורי המתנה )"הטווח הבינוני / ארוך"( 2
מבוא לתהליכים ב- Linux )1( תהליך )process( הוא ביצוע סדרתי של משימה, המוגדרת על-ידי תכנית )program( תהליך = מופע )instance( של ביצוע תכנית תהליך נקרא גם task במקומות שונים מספר תהליכים מתבצעים "בו זמנית" על המעבד במחשב למעשה המעבד "ממתג" בין התהליכים בתדירות גבוהה באמצעות מנגנון החלפת הקשר. פרטים בתרגול הבא עבור מערכת ההפעלה, תהליך הינו יישות עצמאית הצורכת משאבים זיכרון, זמן מעבד, שטח דיסק וכו' לכל תהליך ב- Linux יש מזהה הקרוי PID Process IDentifier מספר שלם בן 32 ביט ייחודי לתהליך )עד 32K תהליכים מטעמי תאימות הסטורית( ערכי ה- pid ממוחזרים מתהליכים שסיימו לתהליכים חדשים שנוצרים 3
מבוא לתהליכים ב- Linux )2( באיתחול Linux גרעין מערכת ההפעלה יוצר שני תהליכים בלבד (pid=0) swapper \ idle משמש לניהול זיכרון ; מורץ כאשר אין אף תהליך אחר מוכן לריצה ; ממנו נוצר התהליך init (pid=1) init ממנו נוצרים כל שאר התהליכים במערכת כל תהליך נוסף נוצר ב- Linux כעותק של תהליך קיים התהליך המקורי נקרא תהליך אב )או הורה( התהליך החדש נקרא תהליך בן fork() תהליך הבן נוצר בעקבות ביצוע קריאת מערכת כדוגמת על-ידי תהליך האב תהליך אב יכול ליצור יותר מתהליך בן אחד 4
מבוא לתהליכים ב- Linux )3( תהליך יכול לאחר היווצרו לבצע משימה שונה מאביו על-ידי הסתעפות בקוד התכנית הקיימת, מהאב על-ידי טעינת משימה חדשה )תכנית חדשה( למשל, על-ידי קריאת המערכת execv() לאחר ההתפצלות לביצוע תהליך אב יכול לבדוק סיום של כל תהליך בן שלו אך לא של "נכדים", "נינים", "אחים" וכדומה אב יכול להמתין לסיום בן לפני המשך פעולתו למשל, על-ידי קריאת המערכת wait() 5
מבוא לתהליכים ב- Linux )4( כדי לאפשר לאב לקבל מידע על סיום הבן, לאחר שתהליך מסיים את פעולתו הוא עובר למצב מיוחד zombie שבו התהליך קיים כרשומת נתונים בלבד ללא שום ביצוע משימה הרשומה נמחקת לאחר שהאב קיבל את המידע על סיום הבן מה קורה לתהליך שהופך ל"יתום" )orphan( לאחר שאביו כבר סיים? התהליך הופך להיות בן של init התהליך init ממשיך להתקיים לאורך כל פעולתה של מערכת ההפעלה אחד מתפקידיו העיקריים נתוניהם לאחר סיום - המתנה לכל בניו, כדי לפנות את 6
)1( לתהליכים ב- Linux API קריאת המערכת pid_t fork(); fork() תחביר: פעולה: מעתיקה את תהליך האב לתהליך הבן וחוזרת בשני התהליכים קוד זהה )ומיקום בקוד( זיכרון זהה )משתנים וערכיהם, חוצצים( סביבה זהה )קבצים פתוחים,,file descriptors ספרית עבודה נוכחית( פרמטרים: אין ערך מוחזר: במקרה של כישלון: 1- לאב )אין בן( במקרה של הצלחה: לבן מוחזר 0 ולאב מוחזר ה- pid של הבן 7
)2( לתהליכים ב- Linux API לאחר פעולת fork() מוצלחת, אמנם יש לאב ולבן את אותם משתנים בזיכרון, אך בעותקים נפרדים כלומר, שינוי ערכי המשתנים אצל האב לא ייראה אצל הבן, וההיפך כמו כן, תהליך הבן הוא תהליך נפרד מתהליך האב לכל דבר. בפרט, יש לו pid משלו מה מדפיס הקוד הבא? main() { fork(); printf( hello ); } 8
)3( לתהליכים ב- Linux API hellohello hheellollo תשובות אפשריות )בהנחה ש-() fork הצליחה(: הסיבה: שני תהליכים כותבים פלט בצורה לא מתואמת status = fork(); מבנה תכנית אופייני המשתמש ב-() fork : if (status < 0) // fork() failed handle error (e.g. message & exit) if (status == 0) // son process do son code else // father process do father code 9
)4( לתהליכים ב- Linux API קריאת המערכת execv() תחביר: int execv(const char *filename, char *const argv[]); פעולה: פרמטרים: טוענת תכנית חדשה לביצוע על-ידי התהליך הקורא filename מסלול אל הקובץ המכיל את התכנית לטעינה argv מערך מצביעים למחרוזות המכיל את הפרמטרים עבור התכנית. האיבר הראשון מקיים argv[0] == filename או רק מכיל את שם קובץ התכנית. האיבר שאחרי הפרמטר האחרון מכיל NULL ערך מוחזר: במקרה של כישלון: 1- במקרה של הצלחה: הקריאה אינה חוזרת. איזורי הזיכרון של התהליך מאותחלים לתכנית החדשה שמתחילה להתבצע מההתחלה. 10
)5( לתהליכים ב- Linux API main() { char *argv[] = { date, NULL}; execv( /bin/date, argv); printf( hello ); } מה ידפיס הקוד הבא? התשובה: מצליחה: את התאריך והשעה נכשלת: hello execv() אם execv() אם 11
)6( לתהליכים ב- Linux API :execv() pid = fork(); if (pid < 0) { דוגמה אופיינית לשילוב fork() עם // handle fork() error } else if (pid == 0) { execv( son_prog, argv_son); //handle execv() error } else // do father code 12
)7( לתהליכים ב- Linux API קריאת המערכת exit() void exit(int status); תחביר: פעולה: מסיימת את ביצוע התהליך הקורא ומשחררת את כל המשאבים שברשותו. התהליך עובר למצב zombie עד שתהליך האב יבקש לבדוק את סיומו ואז יפונה לחלוטין פרמטרים: ערך סיום המוחזר לאב אם יבדוק את סיום התהליך status ערך מוחזר: הקריאה אינה חוזרת 13
)8( לתהליכים ב- Linux API קריאת המערכת pid_t wait(int *status); wait() תחביר: פעולה: גורמת לתהליך הקורא להמתין עד אשר אחד מתהליכי הבן שלו יסיים פרמטרים: status ערך מוחזר: מצביע למשתנה בו יאוחסן סטטוס הבן שסיים אם אין בנים, או שכל הבנים כבר סיימו וכבר בוצע להם wait() - חזרה מיד עם ערך 1- אם יש בן שסיים ועדיין לא בוצע לו wait() )zombie( חזרה מייד עם ה- pid של הבן הנ"ל ועם סטטוס הסיום שלו. מאקרו-ים שונים מאפשרים לקבל מתוך הסטטוס מידע על הבן. למשל (status) WEXITSTATUS יתן את ערך הסיום של בן שסיים )הערך שהעביר כארגומנט ל-() exit (. אחרת המתנה עד שבן כלשהו יסיים 14
)9( לתהליכים ב- Linux API דוגמת קוד אופיינית בה האב מחכה לסיום כל תהליכי הבן: while (wait(&status)!= -1); קריאה שימושית נוספת: waitpid() המתנה לסיום בן מסוים pid_t waitpid(pid_t pid, int *status, int options); ניתן למשל, באמצעות,options לבחור רק לבדוק אם הבן סיים )ערך )WNOHANG או להמתין לסיום הבן )ערך 0( 15
)10( לתהליכים ב- Linux API קריאת המערכת pid_t getpid() getpid() תחביר: פעולה: מחזירה לתהליך הקורא את ה- pid של עצמו פרמטרים: אין ערך מוחזר: ה- pid של התהליך הקורא קריאה שימושית דומה: getppid() תהליך האב של התהליך הקורא getppid() מה המשמעות של == 1 טיפוסי? תשובה: תהליך האב שיצר את התהליך הנוכחי סיים מחזירה את ה- pid של עבור תהליך משתמש 16
אתחול תהליכים ב- Linux.1.2.3 משתמשים מתחברים לעבודה ב- Linux מסוף = מסך + מקלדת )מקומי או מרוחק( דרך מסופים התהליך init יוצר עבור כל מסוף של Linux תהליך בן הטוען ומבצע את המשימות הבאות לפי הסדר : איתחול של המסוף בתכנית getty תכנית login המאפשרת למשתמש להיכנס למערכת לאחר שאושרה כניסת המשתמש: תכנית shell )כמו tcsh )bash המאפשרת למשתמש להעביר פקודות למערכת ההפעלה או כאשר ה- shell שמבצע אותה, הבאה מקבל פקודה, הוא מייצר תהליך בן ממתין לסיום הבן ואז קורא את הפקודה 17
אתחול תהליכים ב- Linux )2( Pid=1 init fork() fork() fork() wait() Pid=7223 Pid=30498 Pid=8837 exec(getty) exec(getty) exec(getty) exit() exec(login) exec(login) exec(shell) shell fork() wait() Pid=5562 exec(command) exit() 18
ניהול תהליכים בגרעין )1( לכל תהליך ב- Linux קיים בגרעין מתאר תהליך descriptor(,)process שהוא רשומה מסוג )include/linux/sched.h גרעין )קובץ task_struct המכילה: מצב התהליך עדיפות התהליך )pid( מזהה התהליך מצביע לטבלת איזורי הזיכרון של התהליך מצביע לטבלת הקבצים הפתוחים של התהליך מצביעים למתארי תהליכים נוספים )רשימה מקושרת( מצביעים למתאר תהליך האב ו"קרובי משפחה" נוספים מסוף איתו התהליך מתקשר ועוד.. 19
ניהול תהליכים בגרעין )2( מצב התהליך נמצא בשדה,state שהוא משתנה בגודל 32 ביט המתפקד כמערך ביטים בכל זמן שהוא, בדיוק אחד מהביטים ב- state למצב התהליך באותו זמן Linux מגדירה את המצבים הבאים לכל תהליך: דלוק בהתאם TASK_RUNNING התהליך רץ או מוכן לריצה, כלומר נמצא בטווח הקצר TASK_INTERRUPTIBLE התהליך ממתין לאירוע כלשהו )טווח בינוני/ארוך( אך ניתן להפסיק את המתנת התהליך ולהחזירו למצב TASK_RUNNING באמצעות שליחת אות )Signal( כלשהו לתהליך. זהו מצב ההמתנה הנפוץ. 20
ניהול תהליכים בגרעין )3( TASK_UNINTERRUPTIBLE התהליך ממתין לאירוע כלשהו )בדומה ל- )TASK_INTERRUPTIBLE אך פרט לאירוע לו הוא ממתין, לא ניתן "להעיר" את התהליך מצב המתנה נדיר לשימוש למשל כאשר התהליך מבקש לגשת לחומרה ומערכת ההפעלה צריכה לסרוק אחר החומרה ללא הפרעה TASK_STOPPED ריצת התהליך נעצרה בצורה מבוקרת על-ידי תהליך אחר )בדרך-כלל debugger או )tracer TASK_ZOMBIE ריצת התהליך הסתיימה, אך תהליך האב של התהליך שסיים עדיין לא ביקש מידע על סיום התהליך באמצעות קריאה כדוגמת.wait() התהליך קיים כמתאר בלבד את ערך השדה state ניתן לשנות בהצבה ישירה או על- ידי המאקרו set_task_state או set_current_state )קובץ גרעין )include/linux/sched.h 21
ניהול תהליכים בגרעין )4( לכל תהליך יש מחסנית נוספת הקרויה,kernel mode stack כלומר "מחסנית גרעין" מחסנית זו משמשת את גרעין מערכת ההפעלה בטיפול באירועים במהלך ריצת התהליך פסיקות בכלל קריאות מערכת בפרט מחסנית הגרעין של כל תהליך מאוחסנת באיזור הזיכרון של הגרעין כאשר במהלך ריצת התהליך מתבצע מעבר בין user mode ו- ו- esp ( ss מתבצעת החלפת מחסניות )שינוי ערכי,kernel mode בין המחסנית הרגילה של התהליך ומחסנית הגרעין ערכי ss:esp המצביעים למחסנית הרגילה נשמרים על-ידי המעבד במחסנית הגרעין מיד עם המעבר ל- mode kernel ומשוחזרים במעבר החוזר ל- user mode 22
ניהול תהליכים בגרעין )5( מחסנית הגרעין מאוחסנת יחד עם מתאר התהליך בקטע זיכרון אחד בגודל,8KB המתחיל בכתובת שהיא כפולה של ( 8KB 13 )2 Kernel Mode Stack 0x015fbfff union task_union { struct task_struct task; unsigned long stack[2048]; }; esp current Process Descriptor 0x015fa878 0x015fa3cb 0x015fa000 המחסנית לא דורסת את מתאר התהליך מפני שאיננה גדלה מעבר ל- 7200 בתים, וגודל מתאר התהליך קטן מ- 1000 בתים 23
ניהול תהליכים בגרעין )6( מצורת האחסון הנ"ל נובעת דרך פשוטה "לשלוף" את כתובת מתאר התהליך מתוך esp כאשר המעבד ב- mode :kernel לאפס את 13 הביטים הנמוכים של esp בהתאם לדוגמה בשקף הקודם: esp = 0x15fa878 כתובת מתאר התהליך 0x15fa000 esp & 0xffffe000 = המאקרו current )קובץ גרעין )include/asm-i386/current.h משתמש בשיטה זו על מנת לאחסן את כתובת מתאר התהליך בערך מוחזר p: movl $0xffffe000, %ecx movl %esp, p andl %ecx, p 24
)1( ניהול תהליכים בגרעין רשימת התהליכים מתארי כל התהליכים מחוברים ברשימה מקושרת כפולה מעגלית הקרויה רשימת התהליכים list( )process באמצעות השדות prev_task ו- next_task רשימה זו מקבילה ל"טבלת התהליכים" הקיימת במערכות ההפעלה אחרות ראש הרשימה הוא המתאר של התהליך swapper )מוצבע ע"י )init_task init_task prev_task next_task prev_task next_task prev_task next_task swapper init 25
)2( ניהול תהליכים בגרעין רשימת התהליכים המאקרו SET_LINKS ו- REMOVE_LINKS )קובץ גרעין )include/linux/sched.h משמשים להוספה והסרה של מתאר תהליך ברשימת התהליכים מטפלים גם ב"קשרי משפחה" המאקרו for_each_task בין תהליכים. פרטים בהמשך )אותו קובץ גרעין( מאפשר לעבור על כל התהליכים ברשימה בסריקה דרך השדה :next_task #define for_each_task(p) for (p = &init_task; (p = p->next_task)!= &init_task; ) 26
)1( ניהול תהליכים בגרעין מיפוי PID למתאר תהליך אמנם קריאות מערכת המתייחסות לתהליך מציינות את ה- pid של התהליך, אך הגרעין עובד עם מתאר התהליך לפיכך, הוגדר בגרעין מנגנון המאתר את מתאר התהליך לפי ה- pid של התהליך המנגנון מבוסס על hash-table בגודל PIDHASH_SZ )בד"כ 1024( כניסות בדרך-כלל מספר התהליכים במערכת קטן בהרבה מ- 32K אין צורך להחזיק כניסות עבור כל ה- pid האפשריים התנגשויות בפונקצית ה- hash נפתרות על-ידי קישור מתארי התהליך, המתמפים לאותה כניסה בטבלה, ברשימה מקושרת כפולה דרך השדות pidhash_next pidhash_pprev ולכן ו- 27
)2( ניהול תהליכים בגרעין מיפוי PID למתאר התהליך הפונקציות hash_pid() ו-() unhash_pid מאפשרות להוסיף ולהסיר מתאר תהליך לטבלה הפונקציה find_task_by_pid() מבצעת את איתור מתאר התהליך לפי ה- pid הנתון pidhash 0 199 216 PID 199 PID 26800 PID 26799 pidhash_next pidhash_pprev 1023 28
)1( ניהול תהליכים בגרעין ניהול קשרי משפחה בגרעין "קשרי המשפחה" בין תהליכים מיוצגים בגרעין באמצעות מצביעים בין מתארי תהליכים מתאר תהליך אב מצביע למתאר תהליך הבן הצעיר ביותר שלו )שנוצר אחרון( באמצעות השדה p_cptr במתאר התהליך מתאר תהליך מצביע למתאר תהליך האב שלו באמצעות השדה p_opptr במתאר התהליך קיים שדה מצביע נוסף הקרוי p_pptr המצביע למתאר תהליך האב בפועל. ערך זה שונה מתהליך האב כאשר התהליך נמצא בריצה מבוקרת ע "י debugger או tracer מתאר תהליך מצביע למתאר תהליך ה"אח הבוגר" ( older,)sibling כלומר מתאר התהליך שאביו יצר לפניו, באמצעות השדה p_osptr מתאר תהליך מצביע למתאר תהליך ה"אח הצעיר" ( younger,)sibling כלומר מתאר התהליך שאביו יצר אחריו, באמצעות השדה p_ysptr 29
)2( ניהול תהליכים בגרעין ניהול קשרי משפחה בגרעין באמצעות "קשרי המשפחה" תהליך יכול לאתר את אביו למשל, עבור getppid() תהליך יכול לאתר את בניו לפי סדר יצירתם למשל, עבור wait() P 0 P 1 P 2 P 3 p_(o)pptr p_ysptr p_osptr P 4 p_cptr 30
)1( ניהול תהליכים בגרעין רשימות מקושרות בגרעין לצורך ניהול תורים ומבני נתונים אחרים הגרעין משתמש ברשימות מקושרות כפולות מעגליות struct list_head { struct list_head *next, *prev; }; typedef struct list_head list_t; list_head הגדרת מבנה הרשימה בקובץ הגרעין include/linux/list.h כל איבר ברשימה הוא מסוג list_t next next next next prev prev prev prev data structure 1 data structure 2 data structure 3 31
)2( ניהול תהליכים בגרעין רשימות מקושרות בגרעין האיברים ברשימה הם שדות המוכלים ברשומות מבני נתונים מבני הנתונים המכילים את אברי הרשימה מקושרים זה לזה באמצעות הרשימה הפעולות על הרשימה כוללות, בין השאר: יצירת )ראש( הרשימה: LIST_HEAD הוספת איבר במקום נתון )list_add( ובסוף הרשימה )list_add_tail( הסרת איבר נתון )list_del( בדיקה האם הרשימה ריקה )list_empty( גישה לרשומה המכילה איבר נתון )list_entry( #define list_entry(ptr, type, member) \ ((type *)((char *)(ptr) (unsigned long)(&((type *)0)->member))) לולאת מעבר על איברים ברשימה )list_for_each( 32
)1( ניהול תהליכים בגרעין הטווח הקצר מתארי התהליכים המוכנים לריצה ב- Linux )מצב )TASK_RUNNING נגישים מתוך מבנה נתונים הקרוי )kernel/sched.c גרעין )קובץ runqueue לכל מעבד יש runqueue משלו כל runqueue מכיל מספר תורים של מתארי תהליכים, אחד לכל עדיפות של תהליך כל תור ממומש כרשימה מעגלית כפולה שתוארה קודם השדה run_list במתאר התהליך הוא איבר הקישור ברשימה )מסוג )list_head 33
)2( ניהול תהליכים בגרעין הטווח הקצר ו-() dequeue_task enqueue_task() הפונקציות מכניסות ומוציאות ]מתאר[ תהליך ב- runqueue הפונקציה wake_up_process() הופכת תהליך ממתין למוכן לריצה,)TASK_RUNNING( מוסיפה את התהליך ל- runqueue באמצעות enqueue_task() ומסמנת צורך בהחלפת הקשר אם התהליך החדש מועדף לריצה על-פני האחרים 34
)1( ניהול תהליכים בגרעין הטווח הבינוני/ארוך תהליך שצריך להמתין לאירוע כלשהו לפני המשך ריצתו )TASK_(UN)INTERRUPTIBLE( נכנס לתור המתנה )wait queue( כמו כן, התהליך יוצא מה- runqueue ומוותר על המעבד כל תור המתנה משויך לאירוע או סוג אירוע כלשהו, פסיקת חומרה, למשל דיסק או שעון התפנות משאב מערכת לשימוש. לדוגמה: שהתפנה וניתן לשלוח דרכו נתונים אירועים אחרים כלשהם, כמו סיום תהליך ערוץ תקשורת כגון כאשר קורה האירוע אליו מקושר תור ההמתנה, מערכת ההפעלה "מעירה" תהליכים מתוך התור, כלומר מחזירה אותם למצב ריצה )TASK_RUNNING( 35
)2( ניהול תהליכים בגרעין הטווח הבינוני/ארוך תהליך ממתין בתור יכול להיות באחד משני מצבים: exclusive )בלעדי( כאשר האירוע המעורר קורה, מעירים אחד מהתהליכים שממתינים עם סימון "בלעדי". למשל: כאשר האירוע הוא שחרור של משאב שניתן לשימוש רק על-ידי תהליך יחיד ב-זמנית non-execlusive )משותף( כאשר האירוע המעורר קורה, מעירים את כל התהליכים שממתינים עם סימון "משותף". למשל: כאשר האירוע הוא פסיקת שעון שיכולה לסמן סוף המתנה עבור תהליכים שונים הממתינים למשך זמן קצוב בדרך-כלל אין באותו תור המתנה ממתינים בלעדיים ומשותפים יחד 36
)3( ניהול תהליכים בגרעין הטווח הבינוני/ארוך תור המתנה ממומש כרשימה מקושרת כפולה מעגלית שתוארה קודם )קובץ גרעין )include/linux/wait.h struct wait_queue_head { spinlock_t lock; מיועד להגן על התור struct list_head task_list; }; typedef wait_queue_head wait_queue_head_t; השדה lock מפני גישה במקביל על-ידי שני תהליכים או יותר כל תהליך בתור מוצבע מאיבר ברשימה המוגדר כדלהלן: struct wait_queue { unsigned int flags; non- )1( או struct task_struct *task; struct list_head task_list; }; typedef struct wait_queue wait_queue_t; השדה flags מציין האם ההמתנה היא exclusive )0( exclusive 37
)4( ניהול תהליכים בגרעין הטווח הבינוני/ארוך sleep_on() פונקציה להכנסת תהליך להמתנה בתור - void sleep_on(wait_queue_head_t *q) { unsigned long flags; wait_queue_t wait; wait.flags = 0; wait.task = current; current->state = TASK_UNINTERRUPTIBLE; add_wait_queue(q, &wait); schedule(); remove_wait_queue(q, &wait); } הפונקציות add_wait_queue[_exclusive]() ו- remove_wait_queue() מכניסות ומוציאות תהליך מהתור 38
)5( ניהול תהליכים בגרעין הטווח הבינוני/ארוך פונקציות נוספות מאפשרות הכנסת תהליך לתור כשהוא ממתין במצב interruptible ו/או כשהממתין בלעדי במקביל, פונקציות המשמשות "להעיר" תהליכים: wake_up מעירה את כל הממתינים המשותפים ואחד מהבלעדיים גרסאות נוספות של :wake_up להעיר מספר מוגבל של תהליכים ממתינים להעיר רק ממתינים שהם interruptible לבצע החלפת הקשר אם התהליך המועדף לריצה משתנה לאחר שמעירים תהליכים 39