לדלג לתוכן

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;
}

בדיקה:

gcc -o zombie zombie.c
./zombie &
ps aux | grep Z

פלט צפוי (דוגמה):

user  12346  0.0  0.0  0  0  pts/0  Z+  10:00  0:00  [zombie] <defunct>


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;
}

gcc -O0 -o counter counter.c
./counter &
nice -n 19 ./counter &

התוצאה תלויה בחומרה ובעומס, אבל בדרך כלל נראה שהתוכנית עם nice=0 מבצעת הרבה יותר איטרציות - בערך פי 5 עד פי 20 יותר מהתוכנית עם nice=19.

ההסבר: הCFS מחלק את זמן הCPU לפי משקלות שנגזרים מערך הnice. התהליך עם nice=0 מקבל משקל גדול יותר מprocess עם nice=19, ולכן הvruntime שלו עולה לאט יותר והוא מקבל הרבה יותר זמן CPU.

kill %1 %2

3. מידע מproc

sleep 300 &
# assume the PID is 5678

מצב:

cat /proc/5678/status | grep State

פלט:
State:  S (sleeping)

הprocess במצב S (TASK_INTERRUPTIBLE) כי הוא ישן - מחכה שהsleep יסתיים.

מידע תזמון:

cat /proc/5678/sched

יציג vruntime, nr_switches, ועוד. אם נבדוק שוב אחרי כמה שניות, הvruntime לא ישתנה - כי הprocess ישן ולא צורך CPU. הvruntime עולה רק כשהprocess באמת רץ על CPU.

context switches:

cat /proc/5678/status | grep ctxt

פלט לדוגמה:
voluntary_ctxt_switches:    2
nonvoluntary_ctxt_switches: 0

הרוב יהיו voluntary כי sleep הוא ויתור רצוני על CPU.

שליחת SIGSTOP:

kill -STOP 5678
cat /proc/5678/status | grep State

פלט:
State:  T (stopped)

המצב השתנה ל-T (TASK_STOPPED). הprocess עצור לחלוטין - הוא לא יתעורר גם כשהsleep יסתיים, עד שיקבל SIGCONT.

kill -CONT 5678
kill 5678

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;
}

קימפול והרצה:

gcc -o switches switches.c
./switches

פלט לדוגמה:

--- 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 החדש לא יידחוף את כולם מיד, אלא יתמזג בהדרגה לתחרות ההוגנת.