6.3 - מתזמן התהליכים - פתרון
פתרון תרגול - מתזמן הprocesses¶
1. מצבי processes¶
האותיות במצבי process:
- R - Running (רץ או מוכן לרוץ) - TASK_RUNNING
- S - Sleeping (ישן, ניתן להפסקה) - TASK_INTERRUPTIBLE
- D - Disk sleep (ישן, לא ניתן להפסקה) - TASK_UNINTERRUPTIBLE
- T - Stopped (עצור) - TASK_STOPPED
- Z - Zombie (זומבי) - TASK_ZOMBIE
כמה processes במצב R:
בדרך כלל רואים 1-3 processes במצב R. המספר נמוך כי:
- רוב הprocesses ישנים (מצב S) ומחכים לאירוע - קלט מקלדת, נתונים מרשת, timeout, וכו'
- התהליך שלא מחכה לכלום (CPU-bound) לא נפוץ - רוב התוכנות עושות IO בעיקר
- אחד מהprocesses במצב R הוא top/htop עצמו
תוכנית שיוצרת זומבי:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// child process - exits immediately
printf("child (PID %d) exiting\n", getpid());
_exit(0);
} else if (pid > 0) {
// parent process - sleeps without wait
printf("parent (PID %d), child was PID %d\n", getpid(), pid);
printf("sleeping 60 seconds without wait...\n");
sleep(60);
}
return 0;
}
בדיקה:
פלט צפוי (דוגמה):
2. השפעת nice¶
הנה התוכנית:
#include <stdio.h>
#include <time.h>
int main() {
long count = 0;
time_t start = time(NULL);
while (1) {
count++;
if (time(NULL) - start >= 1) {
printf("iterations: %ld\n", count);
count = 0;
start = time(NULL);
}
}
return 0;
}
התוצאה תלויה בחומרה ובעומס, אבל בדרך כלל נראה שהתוכנית עם nice=0 מבצעת הרבה יותר איטרציות - בערך פי 5 עד פי 20 יותר מהתוכנית עם nice=19.
ההסבר: הCFS מחלק את זמן הCPU לפי משקלות שנגזרים מערך הnice. התהליך עם nice=0 מקבל משקל גדול יותר מprocess עם nice=19, ולכן הvruntime שלו עולה לאט יותר והוא מקבל הרבה יותר זמן CPU.
3. מידע מproc¶
מצב:
פלט:
הprocess במצב S (TASK_INTERRUPTIBLE) כי הוא ישן - מחכה שהsleep יסתיים.
מידע תזמון:
יציג vruntime, nr_switches, ועוד. אם נבדוק שוב אחרי כמה שניות, הvruntime לא ישתנה - כי הprocess ישן ולא צורך CPU. הvruntime עולה רק כשהprocess באמת רץ על CPU.
context switches:
פלט לדוגמה:
הרוב יהיו voluntary כי sleep הוא ויתור רצוני על CPU.
שליחת SIGSTOP:
פלט:
המצב השתנה ל-T (TASK_STOPPED). הprocess עצור לחלוטין - הוא לא יתעורר גם כשהsleep יסתיים, עד שיקבל SIGCONT.
4. ספירת context switch-ים¶
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void print_switches() {
FILE *f = fopen("/proc/self/status", "r");
if (!f) {
perror("fopen");
return;
}
char line[256];
while (fgets(line, sizeof(line), f)) {
if (strstr(line, "voluntary_ctxt_switches") ||
strstr(line, "nonvoluntary_ctxt_switches")) {
printf("%s", line);
}
}
fclose(f);
}
int main() {
printf("--- before sleeps ---\n");
print_switches();
for (int i = 0; i < 5; i++) {
sleep(1);
}
printf("\n--- after 5 sleeps ---\n");
print_switches();
return 0;
}
קימפול והרצה:
פלט לדוגמה:
--- before sleeps ---
voluntary_ctxt_switches: 3
nonvoluntary_ctxt_switches: 0
--- after 5 sleeps ---
voluntary_ctxt_switches: 8
nonvoluntary_ctxt_switches: 0
הcontext switch-ים הרצוניים עולים בבערך 5 - אחד לכל sleep. כל קריאה ל-sleep גורמת לprocess לוותר רצונית על הCPU (voluntary context switch). ייתכנו כמה תוספות נוספות בגלל קריאות IO (fopen/fgets) שגם הן יכולות לגרום לcontext switch.
שימו לב שnonvoluntary נשאר 0 - כי הprocess הזה כמעט ולא צורך CPU, אז הscheduler לא צריך להפקיע ממנו את הCPU בכוח.
5. שאלה תיאורטית - vruntime של process חדש¶
מה יקרה אם vruntime=0:
אם process חדש יקבל vruntime=0, הוא יהפוך מיד לprocess עם הvruntime הנמוך ביותר בעץ. הCFS יבחר בו לרוץ באופן מיידי, ויתן לו לרוץ זמן רב עד שהvruntime שלו יתקרב לזה של הprocesses האחרים.
זה יגרום ל"הרעבה" (starvation) זמנית של כל שאר הprocesses - הם לא יקבלו CPU עד שהprocess החדש "יתפוס" אותם בvruntime. במערכת עמוסה שבה processes ותיקים צברו vruntime גבוה, ההשפעה תהיה חמורה - הprocess החדש ירוץ כמעט ללא הפרעה דקות ארוכות.
גרוע מזה - אם מישהו כותב תוכנית שעושה fork בלולאה, כל process חדש יקבל vruntime=0 ויירוץ מיד. זו למעשה מתקפת מניעת שירות (DoS) על הscheduler.
מה הCFS באמת עושה:
הCFS מאתחל את הvruntime של process חדש לערך של min_vruntime של הqueue - כלומר, הvruntime המינימלי מבין כל הprocesses שכבר רצים. ככה הprocess החדש מתחיל מ"אותו מקום" כמו כולם, ולא מקבל יתרון לא הוגן.
בפועל, הCFS אפילו מוסיף קצת לvruntime ההתחלתי (sched_child_runs_first) כדי שהprocess החדש לא יידחוף את כולם מיד, אלא יתמזג בהדרגה לתחרות ההוגנת.