לדלג לתוכן

6.3 מתזמן התהליכים פתרון

פתרון תרגול - מתזמן התהליכים

1. מצבי תהליכים

האותיות במצבי תהליך:
- R - Running (רץ או מוכן לרוץ) - TASK_RUNNING
- S - Sleeping (ישן, ניתן להפסקה) - TASK_INTERRUPTIBLE
- D - Disk sleep (ישן, לא ניתן להפסקה) - TASK_UNINTERRUPTIBLE
- T - Stopped (עצור) - TASK_STOPPED
- Z - Zombie (זומבי) - TASK_ZOMBIE

כמה תהליכים במצב R:
בדרך כלל רואים 1-3 תהליכים במצב R. המספר נמוך כי:
- רוב התהליכים ישנים (מצב S) ומחכים לאירוע - קלט מקלדת, נתונים מרשת, timeout, וכו'
- תהליך שלא מחכה לכלום (CPU-bound) לא נפוץ - רוב התוכנות עושות IO בעיקר
- אחד מהתהליכים במצב R הוא top/htop עצמו

תוכנית שיוצרת זומבי:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    pid_t pid = fork();

    if (pid == 0) {
        // תהליך הבן - יוצא מיד
        printf("child (PID %d) exiting\n", getpid());
        _exit(0);
    } else if (pid > 0) {
        // תהליך האב - ישן בלי 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 מקבל משקל גדול יותר מתהליך עם nice=19, ולכן הvruntime שלו עולה לאט יותר והוא מקבל הרבה יותר זמן CPU.

kill %1 %2

3. מידע מproc

sleep 300 &
# נניח שהPID הוא 5678

מצב:

cat /proc/5678/status | grep State

פלט:
State:  S (sleeping)

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

מידע תזמון:

cat /proc/5678/sched

יציג vruntime, nr_switches, ועוד. אם נבדוק שוב אחרי כמה שניות, הvruntime לא ישתנה - כי התהליך ישן ולא צורך CPU. הvruntime עולה רק כשהתהליך באמת רץ על 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). התהליך עצור לחלוטין - הוא לא יתעורר גם כשה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 גורמת לתהליך לוותר רצונית על הCPU (voluntary context switch). ייתכנו כמה תוספות נוספות בגלל קריאות IO (fopen/fgets) שגם הן יכולות לגרום לcontext switch.

שימו לב שnonvoluntary נשאר 0 - כי התהליך הזה כמעט ולא צורך CPU, אז הscheduler לא צריך להפקיע ממנו את הCPU בכוח.


5. שאלה תיאורטית - vruntime של תהליך חדש

מה יקרה אם vruntime=0:
אם תהליך חדש יקבל vruntime=0, הוא יהפוך מיד לתהליך עם הvruntime הנמוך ביותר בעץ. הCFS יבחר בו לרוץ באופן מיידי, ויתן לו לרוץ זמן רב עד שהvruntime שלו יתקרב לזה של התהליכים האחרים.

זה יגרום ל"הרעבה" (starvation) זמנית של כל שאר התהליכים - הם לא יקבלו CPU עד שהתהליך החדש "יתפוס" אותם בvruntime. במערכת עמוסה שבה תהליכים ותיקים צברו vruntime גבוה, ההשפעה תהיה חמורה - התהליך החדש ירוץ כמעט ללא הפרעה דקות ארוכות.

גרוע מזה - אם מישהו כותב תוכנית שעושה fork בלולאה, כל תהליך חדש יקבל vruntime=0 ויירוץ מיד. זו למעשה מתקפת מניעת שירות (DoS) על הscheduler.

מה הCFS באמת עושה:
הCFS מאתחל את הvruntime של תהליך חדש לערך של min_vruntime של התור - כלומר, הvruntime המינימלי מבין כל התהליכים שכבר רצים. ככה התהליך החדש מתחיל מ"אותו מקום" כמו כולם, ולא מקבל יתרון לא הוגן.

בפועל, הCFS אפילו מוסיף קצת לvruntime ההתחלתי (sched_child_runs_first) כדי שהתהליך החדש לא יידחוף את כולם מיד, אלא יתמזג בהדרגה לתחרות ההוגנת.