מערכות הפעלה תרגול 5 אלגוריתם זימון התהליכים ב- Linux
תוכן התרגול אלגוריתם זימון התהליכים ב- Linux איך בוחרים, בכל נקודת זמן, את התהליך הבא להרצה במעבד? 2
הערה מקדימה אלגוריתם הזימון המוצג בתרגול - האלגוריתם של 2.6 Linux אינו חלק מגרסת הגרעין X.2.4 מופיע בספר Understanding the Linux Kernel גירסה 3 אלגוריתם יעיל יותר ועל כן משולב בגרעין המתוקן של RedHat 8.0 לקבלת מידע באינטרנט: חפשו מידע על על קבצי קוד הגרעין העוסקים בזימון בגרסאות הגרעין X.2.6 הקבצים העיקריים: include/linux/sched.h ניתן לחפש גם לפי שם מחבר האלגוריתם החדש: ו- kernel/sched.c Ingo Molnar ישנם אפילו שיפורים נוספים בגירסת האלגוריתם בגרעין X.2.6 3
זימון תהליכים ב- Linux Linux עובדת לפי עקרון של time sharing חלוקת זמן מעבד בין תהליכים. אלגוריתם הזימון קובע את סדר זימון התהליכים ואת זמן המעבד שכל תהליך מקבל. העברת המעבד מתהליך אחד לאחר באמצעות החלפת הקשר. התהליכים ב Linux מתחלקים לשני סוגים : תהליכי זמן-אמת תהליכים הנדרשים להגיב על אירועי מערכת בזמן קצר ואחיד ככל האפשר. תהליכים רגילים. הקביעה אם תהליך הוא תהליך זמן-אמת נעשית ע"י המשתמש. תהליכי זמן-אמת מועדפים לריצה על-פני תהליכים רגילים. תהליכים רגילים אינם זוכים לרוץ אם יש תהליכי זמן-אמת מוכנים לריצה )מצב )TASK_RUNNING 4
מדיניות זימון של תהליכי זמן-אמת עבור תהליכי זמן-אמת קיימים שני סוגים של מדיניות זימון: המעבד :SCHED_FIFO שיתוף פעולה )non-preemptive( עם עדיפויות מתפנה רק כאשר התהליך מחליט לוותר עליו או כאשר תהליך עדיף יותר מוכן לריצה. המעבד מוקצה לאחד מהתהליכים העדיפים ביותר המוכנים לריצה :SCHED_RR חלוקת זמן שווה Robin( )Round זמן המעבד מחולק בצורה שווה בין כל התהליכים העדיפים ביותר המוכנים לריצה 5
זימון תהליכים רגילים התהליכים הרגילים מתחלקים לשני סוגים לפי אופי הפעילות שלהם: תהליך חישובי מעוניין להגדיל את זמן המעבד שלו ככל הניתן לא מוותר על המעבד מרצונו אלא מופקע. תהליך אינטראקטיבי קלט/פלט מול המשתמש מעוניין להקטין את זמן התגובה שלו מוותר על המעבד מרצונו אחרי פרק זמן קצר שני סוגי התהליכים הרגילים מזומנים לפי אותה מדיניות זימון שונים. SCHED_OTHER )נרחיב בהמשך(, אך פרמטרי הזימון 6
עדיפות תהליך עדיפות נקבעת עבור כל תהליך במערכת בצורה הבאה:.-20 nice עדיפות בסיסית עבור תהליך רגיל היא 120 עבור כל תהליך מוגדר הערך, nice כך ש: 19+ 120+nice העדיפות הסטטית של תהליך נקבעת לפי: ככל שהערך המספרי של העדיפות גבוה יותר, כך העדיפות נמוכה יותר. העדיפות קובעת: עבור תהליכים חישוביים - את משך זמן המעבד שיוקצה עבור תהליכים אינטראקטיביים את זמן התגובה 7
עיקרון זימון תהליכים לכל תהליך מוקצב פרק זמן לשימוש במעבד slice time אורך פרוסת הזמן slice) (time ליניארי בעדיפות. RR בין כל התהליכים המוכנים לריצה )בסדר יורד של עדיפויות(.Epoch שלם נקרא RR פרק זמן הנדרש כדי לבצע סבב :TASK_TIMESLICE מוקצה לתהליך במאקרו time slice #define MIN_TIMESLICE (10 * HZ / 1000) /* 10 msec */ #define MAX_TIMESLICE (300 * HZ / 1000) /* 300 msec */ #define TASK_TIMESLICE(p) \ MIN_TIMESLICE + (MAX_TIMESLICE MIN_TIMESLICE) * (MAX_PRIO 1 (p)->static_prio)/39 msec תהליך בעדיפות בסיסית )120( מקבל זמן טיפוסי 150 עדיפות סטטית גבוהה - slice time קטן יותר 8
עיקרון זימון תהליכים )2( ביצירת תהליך חדש, תהליך האב מאבד מחצית מה- time slice שלו לטובת תהליך הבן המטרה: למנוע מצב בו תהליך יכול להרוויח זמן מעבד בתוך אותה תקופה, למשל באופן הבא: לפני סיום ה- slice,time התהליך מייצר תהליך בן שממשיך להריץ את הקוד למשך time slice נוסף באותה תקופה וכ"ו מימוש המנגנון בפונקציה קובץ גרעין do_fork() שמבצעת את fork() kernel/fork.c p->time_slice = (current->time_slice + 1) >> 1; current->time_slice >>= 1; 9
תהליכים אינטראקטיביים תהליך אינטראקטיבי שנבחר לריצה רץ עד שמוותר על המעבד )או כמובן מופקע ע"י תהליך בעל עדיפות טובה יותר( זכרו שתהליך אינטראקטיבי צורך מעט זמן מעבד בכל פעם. ככל שעדיפותו של התהליך טובה יותר הוא יגיע לרוץ על המעבד מהר יותר =< זמן התגובה יקטן אם ה time slice של התהליך נגמר הוא מקבל מייד slice נוסף, במסגרת ה- Epoch הנוכחי. time 10
אלגוריתם הזימון של תהליכים רגילים זימון של כל התהליכים )גם חישוביים וגם אינטראקטיביים( נעשה ע"י זמן תהליכים (scheduler( אחד. תמיד נבחר לריצה תהליך עם העדיפות הגבוהה ביותר, שמוכן לריצה, ושעוד נותר זמן מה- slice time שלו. מבנה הנתונים,runqueue המשמש את scheduler תהליכים, גם הוא משותף לכל התהליכים הרגילים. לזימון 11
ז מ ן התהליכים scheduler.1.2.3.4 מהו? ז מ ן התהליכים הוא רכיב תוכנה בגרעין Linux שאחראי על זימון התהליך הבא למעבד)מימוש פונקציה )schedule() מתי מופעל? בתגובה על פסיקת שעון לאחר שתהליך כילה את ה- slice time שלו )בעקבות השגרה.)scheduler_tick() בטיפול בקריאת מערכת הגורמת לתהליך הקורא לעבור להמתנה, כך שהמעבד מתפנה להריץ תהליך אחר. בחזרה של תהליך מהמתנה יש לשבץ את התהליך ב- runqueue ולבדוק אם כתוצאה מכך יש לבצע החלפת הקשר. כאשר תהליך מחליט לוותר עם המעבד מרצונו בקריאת מערכת כגון.sched_yield() 12
מבט מקרוב על ה- runqueue nr_active queue bitmap.... 0 58.... 1 112.... 1 120.... מתאר תהליך swapper prio_array_t prio_array_t runqueue nr_running curr idle active expired expired_ts... 13
מבט מקרוב על ה- runqueue )2( runqueue הוא מבנה נתונים המשמש את זמן התהליכים. כל runqueue )על כל מעבד( מכיל את הנתונים הבאים: מספר התהליכים ב- runqueue nr_running curr מצביע למתאר התהליך שרץ כרגע idle מצביע למתאר תהליך ה- swapper )לא כולל את ה- swapper ( active מצביע למערך תורי העדיפויות של התהליכים הפעילים, כלומר תהליכים מוכנים לריצה )TASK_RUNNING( שנותר להם זמן ריצה במסגרת התקופה הנוכחית expired מצביע למערך תורי העדיפויות של התהליכים המוכנים לריצה שכילו את ה- slice time שלהם בתקופה הנוכחית ל- expired מתי עבר התהליך הראשון מ- active expired_timestamp בתקופה הנוכחית 14
מבט מקרוב על ה- runqueue )3( מערך תורי עדיפויות הנתונים הבאים: prio_array( )struct מכיל את nr_active bitmap[] ביטים( מספר התהליכים המצויים במערך זה וקטור ביטים בגודל מספר דרגות העדיפות 140( דלוק i ביט )1( אם יש תהליכים בתור המתאים לעדיפות i התהליך הבא לזימון הוא התהליך הראשון בתור העדיפות הנמוך ביותר ב- active שהביט שלו דלוק חישוב ב-( O(1 queue[] מערך התורים עצמו, עם כניסה לכל דרגת עדיפות )140 כניסות(. כל כניסה מכילה ראש תור מסוג list_t כמתואר בתרגול הקודם. 15
באיזה מצב התהליכים מגיעים ל הפונקציה?schedule() במצב TASK_(UN)INTERRUPTIBLE sleep_on )מקרה 2 בשקף )12 מתוך הפונקציה שם תהליך נוכחי משנה למצב TASK_(UN)INTERRUPTIBLE מכניס את עצמו בתור המתנה קורא ל schedule אחרי החזרה מ schedule מוציא את עצמו מתור המתנה במצב TASK_RUNNING need_resched תהליך אחר השתחרר מהמתנה בעקבות פסיקה שהדליקה דגל )מקרים 1,3,4 בשקף 12( פסיקת שעון שבה נגמר לתהליך הנוכחי ה time_slice sched_yield() )קריאה ל- schedule ישירות ולא באמצעות )need_resched 16
)1( הפונקציה schedule() )שמוותר על המעבד( current; prev = rq = this_rq();.. spin_lock_irq(&rq->lock); switch(prev->state) { case TASK_INTERRUPTIBLE: if (signal_pending(prev)) { prev->state = TASK_RUNNING; break; } default: deactivate_task(prev, rq); case TASK_RUNNING: ; } prev מצביע למתאר התהליך הנוכחי rq מכיל את ה- runqueue של המעבד הנוכחי חסימת הפסיקות רק תהליך שעבר למצב המתנה מוצא מתוך ה- runqueue למעט מצב של הפרעה בהמתנה 17
)2( הפונקציה schedule() if (!rq->nr_running) { next = rq->idle; rq->expired_timestamp = 0; goto switch_tasks; } array = rq->active; if (!array->nr_active) { rq->active = rq->expired; rq->expired = array; array = rq->active; rq->expired_timestamp = 0; } אם אין יותר תהליכים לזימון מתוך ה- runqueue, יזומן תהליך ה- swapper אם לא נותרו תהליכים ב- array,active מחליפים בין ה- active וה- expired )סיום תקופה והתחלת תקופה חדשה( 18
)3( הפונקציה schedule() idx = sched_find_first_bit(array->bitmap); queue = array->queue + idx; next = list_entry(queue->next, task_t, run_list); switch_tasks: clear_tsk_need_resched(prev); if (prev!= next) {.. rq->curr = next; context_switch(prev, next);.. spin_unlock_irq(&rq->lock); } else spin_unlock_irq(&rq->lock); ביצוע החלפת ההקשר ע"י קריאה ל-() context_switch התהליך הבא לזימון למעבד )next( הוא התהליך הראשון בתור ב- array active בעל העדיפות הגבוהה ביותר שאלה: מי מבצע את אפשור הפסיקות מחדש לאחר הקריאה ל-() context_switch? תשובה : התהליך next 19
שימו לב! ברגע שנגמר ה time slice של תהליך חישובי, הוא עובר לתור ה expired לעומתו, תהליך אינטראקטיבי שמסיים time slice מיד מקבל time slice נוסף ונשאר בתור ה active נראה את זה בתרגול הבא 20
אפיון התנהגות תהליך )1( Linux מודדת את "זמן ההמתנה הממוצע" של תהליך "זמן המתנה ממוצע" = סך כל זמן ההמתנה בטווח הבינוני פחות סך כל זמן הריצה בכל פעם שתהליך מוותר על המעבד, ערך השעון נשמר )פונקצית :)schedule() p->sleep_timestamp = jiffies; כאשר תהליך חוזר מהמתנה מוסף זמן ההמתנה לחשבון )פונקציה :MAX_SLEEP_AVG עד לגודל מקסימלי )activate_task() #define MAX_SLEEP_AVG (2*HZ) sleep_time = jiffies p->sleep_timestamp; p->sleep_avg += sleep_time; if (p->sleep_avg > MAX_SLEEP_AVG) p->sleep_avg = MAX_SLEEP_AVG; 21
אפיון התנהגות תהליך )2( 22 כל פעימת שעון בה התהליך רץ מורידה מהממוצע )הפונקציה )sheduler_tick() עד למינימום 0 if (p->sleep_avg) p->sleep_avg--; על-פי שיטת חישוב זו תהליך עתיר חישוב )חישובי( צפוי להגיע ל"זמן המתנה ממוצע" נמוך. תהליך עתיר ק/פ )אינטראקטיבי( צפוי להגיע ל"זמן המתנה ממוצע" גבוה. שימו לב: זמן המתנה ב runqueue בשום מקום לא נלקח בחשבון
)1( הפונקציה scheduler_tick() פונקציה זו מופעלת בכל פסיקת שעון ומעדכנת נתוני זימון של תהליכים. מופעלת כשהפסיקות חסומות כדי למנוע שיבוש נתונים ע"י הפעלת הפונקציה במקביל. מזהה צורך בהחלפת הקשר בעקבות סיום time slice של התהליך הנוכחי ומעדכנת את הדגל.need_resched משתמשת בפונקציות enqueue_task() ו-() dequeue_task להוצאה והכנסה של תהליך לאחד ממערכי התורים active( או )expired עדכון ה- sleep_avg של התהליך הנוכחי task_t *p = current; runqueue_t *rq = this_rq();.. if (p->sleep_avg) p->sleep_avg--; 23
)2( הפונקציה scheduler_tick() if (!--p->time_slice) { dequeue_task(p, rq->active); set_tsk_need_resched(p); p->prio = effective_prio(p); p->time_slice = TASK_TIMESLICE(p); אם התהליך הנוכחי כילה את ה- slice time של עצמו, הוא מוצא מה- active, העדיפות הדינמית וה- slice time הבא שלו מחושבים מחדש, ויבוצע לו schedule() בהמשך if (!TASK_INTERACTIVE(p) EXPIRED_STARVING(rq)) { if (!rq->expired_timestamp) כאשר התהליך הראשון בתקופה הנוכחית עובר ל- expired, מעודכן jiffies; rq->expired_timestamp = expired_timestamp enqueue_task(p, rq->expired); תהליך אינטראקטיבי מוחזר ל- active לרוץ time slice נוסף בתקופה הנוכחית כאשר אין הרעבה } else enqueue_task(p, rq->active); 24
חישוב דינמי של עדיפות תהליך )1( Linux מעדכנת את העדיפות של כל תהליך "רגיל" באופן דינמי בהתאם לזמן ההמתנה הממוצע של התהליך. עדיפויות תהליכי זמן-אמת מקובעות לערך הסטטי החישוב מתבצע בפונקציה.effective_prio() העדיפות הדינמית מחושבת לפי האלגוריתם הבא: sleep _ avg 1 bonus 25% 40 5 bonus 5 MAX _ SLEEP _ AVG 2 prio = static_prio bonus if (prio < MAX_RT_PRIO) prio = MAX_RT_PRIO; if (prio > MAX_PRIO - 1) prio = MAX_PRIO 1; 25
חישוב דינמי של עדיפות תהליך )2( הגבלת גודל ה- bonus ל- 5 נועדה למנוע מצב שבו יתבצע היפוך עדיפויות בין תהליכים שעדיפויותיהם הבסיסיות )הסטטיות( רחוקות זו מזו. למשל, תהליך בעל nice 19 )עדיפות 139( יהפוך לעדיף יותר מתהליך בעל nice 0 )עדיפות 120( שיטת חישוב זו משפרת את העדיפות של תהליך ש"ממתין הרבה" )צפוי לתהליכים אינטראקטיביים( ומרעה את העדיפות של תהליך ש"ממתין מעט" )צפוי לתהליכים חישוביים( הערך של מחצית,MAX_SLEEP_AVG כלומר HZ פעימות או שניה אחת, נקבע בתור הסף בין "המתנה מרובה" ל"המתנה מועטת" 26
תהליכים אינטראקטיביים) 1 ( העדיפות ההתחלתית,שנקבעת ע"י המשתמש, נקבעת לפי הערכתו האם תהליך הוא חישובי או אינטראקטיבי. 20- = nice =< תהליך אינטראקטיבי )כמעט בוודאות(. >= תהליך חישובי. = 19 nice.scheduler ההחלטה דינמית של ה >= nice = 0 27
תהליכים אינטראקטיביים )2( סיווג תהליך כאינטראקטיבי מבוצע במאקרו :TASK_INETRACTIVE #define TASK_INTERACTIVE(p) \ ((p)->prio <= (p)->static_prio DELTA(p)) כאשר DELTA(p) ניתנת להגדרה כדלקמן: 1 TASK _ NICE( p) TASK _ NICE( p) DELTA( p) 25% 40 2 5 2 2 20 20 תהליך מסווג כאינטראקטיבי אם העדיפות מהעדיפות הסטטית שלו. )הדינמית( שלו "חורגת" תוצאה של זמן המתנה ממוצע גבוה המקנה בונוס בחישוב העדיפות הדינמית של התהליך 28
תהליכים אינטראקטיביים )3( 29 ככל שמשתפרת העדיפות הסטטית של התהליך, יותר" לתהליך להיות מסווג כאינטראקטיבי. "קל "קל יותר": התהליך צריך לצבור זמן המתנה ממוצע נמוך יותר על-מנת להיחשב כאינטראקטיבי לדוגמה: תהליך עם 20- nice יכול להיות "חזיר" בצריכת המעבד ולהגיע לעדיפות דינמית של בונוס שלילי 3- ועדיין להיות מסווג כאינטראקטיבי. לעומת זאת, תהליך עם nice 19 לא יכול כלל להיות מסווג כאינטראקטיבי כי נדרש בונוס של 7+ והבונוס המקסימלי הוא +5
נקודות למחשבה מה קורה כשתהליך חישובי יוצא להמתנה ואז חוזר? הוא יכול להיחשב אינטראקטיבי לפרק זמן מסוים ולקבל "פיצוי" על ההמתנה. מה קורה כשתהליך אינטראקטיבי מנצל time slice מלא? נוצרת בעיה ה expired עלול : "לתקוע" 30 את התהליכים האחרים בתור בנוסף עלול "לתקוע" את האינטראקטיביים האחרים בעדיפות גבוהה יותר ב active
מניעת הרעבה כדי למנוע הרעבה של תהליכים חישוביים ע י תהליכים אינטראקטיביים, מוגדר "סף הרעבה" על זמן ההמתנה של התהליכים ב- expired : #define EXPIRED_STARVING(rq) \ ((rq)->expired_timestamp && \ (jiffies (rq)->expired_timestamp >= \ STARVATION_LIMIT * ((rq)->nr_running + 1)) כאשר התהליכים ב- expired מורעבים מעבר לסף, מופסקת הענקת time slices הנוכחית. נוספים לתהליכים אינטראקטיביים בתקופה הסף המוגדר פרופורציוני למספר התהליכים ב-,runqueue יותר תהליכים מוכנים לריצה, הסף גבוה יותר. כך שככל שיש 31
מניעת הרעבה - חסרון המנגנון הוא גס: מאפשר לאינטראקטיביים להרעיב את החישוביים, במיוחד כשיש עומס על המערכת. לאחר שעוברים את הסף =< מאפשר לחישוביים לעכב את האינטראקטיביים לזמן רב )כי אינט' לא מקבלים time slices נוספים( 32
איפה משתנה מה - סיכום sleep_avg מוקטן בכל פעימת שעון של התהליך הנוכחי ב scheduler_tick() מוגדל חזרה מהמתנה time_slice מוקטן כל פעימת שעון של התהליך הנוכחי ב scheduler_tick() כשנגמר נותנים חדש מלא )גודלו תלוי ב )static_prio Fork ½ מהאב כשבן מת היתרה חוזרת לאב prio )הכוונה לעדיפות דינמית( כשנגמר time_slice נוכחי ב scheduler_tick() בחזרה מהמתנה בפונקציה wakeup_process ביחד עם שינוי sleep_avg מחשבים גם מחדש prio ומשבצים בתור המתאים חישוב prio מתבצע ע"י פונקציה effective_prio() שימו לב: time_slice לא משתנה כאן 33
מי משפיע על מי - סיכום time_slice הוא פונקציה של: static_prio static_prio = 120+nice prio הוא פונקציה של: sleep_avg static_prio המיקום ב runqueue נקבע ע"פ: prio 34