Skip to content

Latest commit

 

History

History
622 lines (439 loc) · 28.4 KB

File metadata and controls

622 lines (439 loc) · 28.4 KB

1. תהליכים

  • תכנית (program) היא אוסף פקודות.
    • תהליך (process) הוא מופע (instance) של תכנית הנמצאת בשלבי ביצוע.

1.1. מה זה תהליך?

  • תהליך הוא ביצוע סדרתי של תכנית, כלומר מופע של קובץ הרצה.
  • יכולים להיות מספר תהליכים עצמאיים לאותו קובץ הרצה.
  • מערכת ההפעלה מחליטה בכל רגע איזה תהליך ירוץ על כל ליבת מעבד.
  • כדי להקל על פיתוח ולספק הגנה בין תהליכים, מערכת ההפעלה יוצרת עבור כל תהליך את האשליה שהוא לבדו במערכת.
  • מספר תהליכים רצים "בו-זמנית" על המעבד: מערכת ההפעלה מחליפה בין התהליכים במהירות ויוצרת אשליה שהם רצים יחד.
  • כל תהליך צורך משאבים, למשל זמן מעבד וזיכרון.

1.1.1. מבנה מרחב הכתובות של תהליך

  • מרחב הכתובות הווירטואלי של תהליך הוא רציף מבחינה לוגית, גם אם המיפוי לזיכרון פיזי מפוזר.
  • חלוקה טיפוסית למקטעים:
    • text / code segment - קוד התכנית ונתונים לקריאה בלבד.
    • data segment - נתונים סטטיים מאותחלים ו-bss (לא מאותחלים).
    • heap - זיכרון דינמי שמוקצה בזמן ריצה (malloc/free).
    • stack - מסגרות קריאה, משתנים אוטומטיים וכתובות חזרה.
  • כיוון גדילה: ה-stack גדל כלפי מטה, וה-heap גדלה כלפי מעלה.

1.2. תהליכוני גרעין

  • הגרעין אינו תהליך ולא ניתן לתזמון:
    • זה אוסף שגרות המבוצע בתגובה לאירועים (למשל פסיקות).
    • לעיתים מערכת ההפעלה כן תריץ קוד גרעין בתוך ישות הניתנת לזימון, הנקראת "תהליכון גרעין".

1.3. תהליכים בלינוקס

  • לכל תהליך בלינוקס יש מזהה הקרוי PID.

    • זהו מספר שלם בן 32 ביט, ייחודי לתהליך.
    • ברוב מערכות לינוקס משתמשים רק ב-15 הביטים התחתונים, ולכן ניתן ליצור עד 32K תהליכים. מנהל המערכת יכול להגדיר מספר גבוה יותר.
    • ערכי PID ממוחזרים מתהליכים שסיימו לתהליכים חדשים.
  • עם עליית המערכת, הגרעין יוצר את התהליך idle שמספרו pid=0.

    • הוא נקרא לריצה כאשר אין תהליכים מוכנים ומבצע את פקודת המכונה HLT, המכניסה את המעבד למצב שינה.
    • זה כדאי לחיסכון באנרגיה.
      • בנוסף, idle מאגד תחתיו תהליכונים שונים של הגרעין.
  • בארכיטקטורת x86, פקודת HLT היא פקודת אסמבלי שעוצרת את ה-CPU עד שתתקבל פסיקה חיצונית.

    • קוד המכונה של HLT ב-x86 הוא 0xF4.
  • התהליך idle יוצר את התהליך init שמספרו pid=1.

    • התהליך init יוצר את כל שאר התהליכים.

1.4. קריאת המערכת fork

pid_t fork(void);
  • מה עושה? יוצרת תהליך בן ע"י העתקת תהליך האב. שני התהליכים חוזרים מאותה נקודה בקוד.

  • מה משתכפל?

    • קוד זהה ומיקום ריצה זהה.
    • מרחב זיכרון זהה (משתנים וערכיהם, גם במחסנית וגם בערימה).
    • סביבה זהה (קבצים פתוחים, ספריית עבודה נוכחית).
  • ייחוד: תהליך הבן נפרד מהאב ולכן מקבל PID משלו.

  • פרמטרים: אין.

  • ערך מוחזר:

    מצב ערך מוחזר למי
    כישלון -1 אב
    הצלחה 0 בן
    הצלחה PID של הבן אב
  • שאלה: איך נבדיל בין אב לבן אם ה-PID של הבן יוצא במקרה 0?

  • תשובה: במקרה של הצלחה תמיד יוחזרו ערכים שונים לאב ולבן, כי ה-PID של הבן שונה מ-0 (המזהה הזה שמור ל-idle).

1.4.1. שכפול מרחב הזיכרון ע"י fork

  • לאחר פעולת fork מוצלחת יש לאב ולבן אותם משתנים בזיכרון, אך בעותקים נפרדים.
  • לכן שינוי ערכי המשתנים אצל האב לא ייראה אצל הבן, ולהיפך.

1.5. קריאת המערכת wait

pid_t wait(int *wstatus);
  • תפקיד: wait מנקה את המשאב שהוקצה בקריאת המערכת fork ומונעת מצב zombie לבנים שסיימו.

  • מה עושה? ממתינה עד שאחד מתהליכי הבן יסיים.

  • פרמטרים:

    • wstatus - מצביע למשתנה שבו יאוחסנו פרטי הבן שהסתיים.
      • wstatus כולל את ערך הסיום של הבן (הערך שהועבר כארגומנט ל-exit).
      • ערך הסיום מופיע בבית השני מתוך ארבעת בתי wstatus.
      • חילוץ ערך הסיום מתבצע עם המאקרו WEXITSTATUS(*wstatus) שמחזיר (*wstatus >> 8) & 0xff.
      • מאקרואים שימושיים נוספים:
        • WIFSIGNALED(*wstatus) — בודק האם הבן הסתיים עקב סיגנל.
        • WTERMSIG(*wstatus) — מחזיר את מספר הסיגנל שהרג את הבן.
    • אם אין צורך בסטטוס, אפשר להעביר NULL.
  • ערך מוחזר:

    מצב ערך מוחזר
    אין בנים או שכל הבנים כבר סיימו ובוצע להם wait -1
    יש בנים שסיימו ועדיין לא בוצע להם wait (מצב zombie) pid של אחד הבנים
    אחרת המתנה עד שבן כלשהו יסיים
  • קריאות נוספות במשפחה: waitid, wait3, wait4 (מספקות מידע/שליטה מתקדמים).

  • טריק: forkmalloc, waitfree.

  • שאלה: איך תהליך אב יכול לחכות לסיום כל תהליכי הבן?

  • תשובה: לקרוא ל-wait בלולאה עד שמתקבל -1 (כל הבנים נאספו).

pid_t pid;
while ((pid = wait(NULL)) > 0) {
}

הערה: האב יכול להמתין רק לבנים שלו בעזרת wait (לא לנכדים או לאחים).

1.6. קריאת המערכת waitpid

pid_t waitpid(pid_t pid, int *wstatus, int options);
  • מה עושה? ממתינה לסיום בן ספציפי שמספרו pid.

  • חסימה: wait ו-waitpid הן קריאות מערכת חוסמות.

    • הן עוצרות את התקדמות התהליך עד שמתקיים תנאי מסוים.
  • התנהגות לא חוסמת: הפרמטר options מאפשר לשנות את ההתנהגות.

    • אם options == WNOHANG הקריאה חוזרת מיד.
  • ערך מוחזר (כאשר WNOHANG פעיל):

    מצב ערך מוחזר
    אף בן עדיין לא סיים 0
    בן סיים והוא עדיין zombie pid חיובי של בן

1.7. קריאת המערכת exit

void exit(int status);
  • מה עושה? מסיימת את ביצוע התהליך הקורא ומשחררת את משאביו.

    • התהליך עובר למצב zombie עד שתהליך האב בודק את סיומו, ואז הוא מפונה לחלוטין.
  • פרמטרים:

    • status - ערך סיום המוחזר לאב אם יבדוק את סיום התהליך.
    • בפועל ניתן להעביר להורה רק 8 ביטים, ולכן מתקבל (status & 0xff).
  • ערך מוחזר: הקריאה אינה חוזרת, ולפי ה-man אינה יכולה להיכשל.

  • מטרת מצב zombie: לאפשר לאב לבדוק את סיבת הסיום ומידע בסיסי על הבן לפני פינוי מלא.

  • למה לא מספיק return בסוף main?

    • main אינה הפונקציה הראשית האמיתית; היא נקראת ע"י __libc_start_main שמפעילה exit.
int __libc_start_main(...) {
    exit(main(...));
}
  • מסקנה: exit תמיד נקראת בסיום סטנדרטי של תוכנית.
  • למה חייבים את מערכת ההפעלה? לדוגמה, כדי להעיר את האב שממתין לסיום הבן.
  • יתרון נוסף: exit מאפשר לסיים תהליך מיד מתוך עומק שרשרת קריאות בלי להחזיר שגיאות עד main.

תהליך יתום: אם תהליך מסתיים אחרי שאביו הסתיים בלי wait, הוא מאומץ ע"י init, שממשיך לרוץ ומבצע wait לבניו כדי לפנות את נתוניהם.

1.8. קריאת המערכת execv

int execv(const char *filename, char *const argv[]);
  • מה עושה? טוענת תכנית חדשה לביצוע במקום התהליך הקורא.

  • פרמטרים:

    • filename - מסלול אל הקובץ המכיל את התכנית.
    • argv - מערך מצביעים למחרוזות של הארגומנטים.
      • argv[0] == filename (שם התכנית).
      • אחרי הארגומנט האחרון מופיע NULL.
  • ערך מוחזר:

    • כישלון: -1.
    • הצלחה: הקריאה אינה חוזרת. אזורי הזיכרון (קוד, מחסנית, וכו') מאותחלים עבור התכנית החדשה שמתחילה מהתחלה.
  • למה צריך NULL? כדי לסמן את סוף רשימת הארגומנטים, כי מספרם אינו קבוע.

  • הערה: execv היא אחת ממשפחת פונקציות exec*.

  • דוגמה:

int main {
    char *argv[] = {"date", NULL};
    execv("/bin/date", argv);
    printf("hello");
    return 0;
}
  • הצלחה: יודפסו תאריך ושעה.
  • כישלון: יודפס hello.

1.9. קריאות המערכת getpid, getppid

pid_t getpid(void);
pid_t getppid(void);
  • getpid מחזירה לתהליך הקורא את ה-PID של עצמו.

  • getppid מחזירה את ה-PID של תהליך האב.

  • שאלה: מה המשמעות של getppid == 1 עבור תהליך משתמש טיפוסי?

  • תשובה: תהליך האב הוא init (לדוגמה כשבן הופך ליתום).

1.10. טיפול בשגיאות קריאות מערכת (errno)

  • ברוב קריאות המערכת, כישלון מסומן ע"י ערך מוחזר -1 והגרעין מציב את errno.
  • errno הוא משתנה גלובלי (Thread-Local) שמכיל את קוד השגיאה האחרון.
  • יש לקרוא את errno מיד לאחר הכישלון ולפני קריאה למערכת אחרת.
  • perror("prefix") מדפיסה את הודעת השגיאה עם קידומת.
  • strerror(errno) מחזירה מחרוזת תיאורית לקוד השגיאה.
  • כדי להשתמש ב-errno/strerror יש לכלול <errno.h> ו-<string.h>.
if (read(fd, buf, n) == -1) {
  perror("read");
  /* or: fprintf(stderr, "read: %s\n", strerror(errno)); */
}

2. ניהול תהליכים ב-Linux

2.1. מבני הנתונים (במבט-על)

  • קריאות מערכת מתבססות על:
    • מתאר תהליך (Process Descriptor / task_struct, נקרא גם PCB).
    • רשימת תהליכים (Process List).
    • טבלת ערבול PID -> PCB.
    • תורי ריצה (Run Queue) ו-תורי המתנה (Waiting Queue).

2.2. מתאר התהליך (task_struct)

  • שומר את המידע המרכזי על תהליך:
    • PID, מצב ריצה, עדיפות.
    • קשרי משפחה (אב/בנים/אחים).
    • מצביעים לזיכרון ולקבצים פתוחים.
    • קישורי רשימות ומסוף.

2.3. קשרי משפחה

  • real_parent - האב המקורי.
  • parent - האב בפועל (למשל תחת debugger).
  • children - רשימת הבנים.
  • siblings - רשימת האחים.
  • שימושים טיפוסיים: getppid לאיתור אב, wait לאיתור בנים.

תרשים - קשרי משפחה (PCB):

flowchart TB
  P0((P0));
  P1((P1));
  P2((P2));
  P3((P3));
  P4((P4));

  P1 -->|real_parent| P0; 
  P2 -->|real_parent| P0;
  P3 -->|real_parent| P0;

  P0 -->|children| P1;
  P0 -->|children| P2;
  P0 -->|children| P3;
  P3 -->|children| P4;

  P1 -.->|siblings| P2;
  P2 -.->|siblings| P3;

  linkStyle 3,4,5,6 stroke-dasharray: 6 3;
Loading

2.4. רשימת תהליכים

  • רשימה מקושרת כפולה מעגלית דרך prev_task ו-next_task.
  • הראש הוא idle (מוצבע ע"י init_task).
  • מוסיפה איבר: fork.
  • אינה מוסיפה: execv (לא יוצר תהליך חדש).
  • מוחקת איבר: wait; exit משאיר zombie עד לאיסוף.

2.5. טבלת ערבול PID -> PCB

  • חיפוש לפי PID ברשימה הוא $O(n)$.
  • טבלת ערבול מאפשרת איתור בממוצע $O(1)$ (למשל עבור waitpid).
  • גודל הטבלה הוא PIDHASH_SZ (לרוב 1024 כניסות).
  • לרוב מספר התהליכים קטן בהרבה מ-32K, לכן לא נדרשות כניסות לכל PID אפשרי.
  • התנגשויות נפתרות ב-separate chaining:
    • לכל תא בטבלה יש רשימה מקושרת של מתארי תהליך.
    • השרשרת היא רשימה מקושרת דו-כיוונית.

תרשים - טבלת ערבול PID -> PCB:

flowchart LR
  subgraph HashTable["Size = 1,024"]
    direction TB
    B0["0"];
    B1["1"];
    B2["2"];
    BDot["..."];
    BLast["1023"];
  end

  B1 --> PID57["PID 57"];
  PID57 --> PID20777["PID 20777"];

  B2 --> PID22100["PID 22100"];
  PID22100 --> PID18044["PID 18044"];
Loading

3. תורי תהליכים

3.1. מצב התהליך

  • מצב התהליך נשמר בשדה state במתאר התהליך (מערך ביטים בגודל 32). בכל רגע ביט אחד פעיל.
מצב תיאור קצר
TASK_RUNNING רץ או מוכן לריצה (זימון "בטווח הקצר").
TASK_ZOMBIE הסתיים וממתין ל-wait; נשאר רק המתאר.
TASK_INTERRUPTIBLE שינה שניתן להעיר באמצעות סיגנל (מצב נפוץ).
TASK_UNINTERRUPTIBLE שינה עמוקה; מתעורר רק מהאירוע (לרוב I/O/page fault).
TASK_STOPPED נעצר בצורה מבוקרת (לרוב debugger/tracer).

בתיאוריה, תהליך עובר בין המצבים ready ל-running, אך בלינוקס שני המצבים הללו מאוגדים תחת אותו דגל: TASK_RUNNING.

הערה: תהליך שנמצא במצב TASK_INTERRUPTIBLE ניתן להעיר בסיגנל (הקריאה מצפה לכך ולעיתים תמשיך אוטומטית). TASK_UNINTERRUPTIBLE מתעורר רק מהאירוע עצמו, לרוב I/O אחרי page fault, ולכן אינו מגיב לסיגנלים בזמן ההמתנה.

3.2. תור ריצה (runqueue)

  • תהליכים במצב TASK_RUNNING נמצאים ב-runqueue.
  • לכל ליבה תור ריצה משלה: struct rq runqueues[NR_CPUS];.
  • תהליך יכול להופיע בתור ריצה אחד בלבד, כדי למנוע ריצה כפולה במקביל.
  • מבנה התור: תורי זמן-אמת + עץ אדום-שחור לתהליכים רגילים.
  • פעולות עיקריות: activate_task, deactivate_task.
    • wake_up_process מעבירה תהליך ממתין ל-TASK_RUNNING ומוסיפה אותו לתור של ה-CPU הנוכחי (ועשויה לסמן צורך בהחלפת הקשר).

3.3. תור המתנה (wait queue)

  • תהליכים ב-TASK_INTERRUPTIBLE או TASK_UNINTERRUPTIBLE נמצאים בתורי המתנה, לא ב-runqueue.
  • לכל סוג אירוע יש תור נפרד (פסיקות חומרה, משאבים, סיום בן וכו').
  • כניסה לתור המתנה מתבצעת דרך קריאה חוסמת (read, wait, ...).
  • תור ההמתנה לסיום בנים נשמר בשדה wait_queue_head_t wait_chldexit במתאר האב.

3.4. החלפת הקשר (Context Switching)

  • החלפת הקשר מתרחשת בפסיקה, קריאת מערכת או כאשר המתזמן מחליט על החלפת תהליך.
  • שלבים עיקריים בגרעין:
    1. כניסה למצב גרעין עקב פסיקה/קריאת מערכת.
    2. שמירת מצב הריצה של התהליך הנוכחי (registers, PC, SP, flags) אל ה-PCB שלו.
    3. בחירת תהליך חדש לריצה ע"י המתזמן.
    4. טעינת מצב הריצה של התהליך החדש מה-PCB שלו.
    5. עדכון מרחב הכתובות (page tables) וחזרה למצב משתמש.

4. סיגנלים

4.1. הקדמה

  • "Everything is a file" (לא באמת, אבל נתעלם מזה בינתיים): עקרון ב-Unix שבו משאבים רבים מיוצגים כ-file descriptors ונגישים דרך open/read/write (למשל קבצים רגילים, התקני קלט/פלט, pipes, sockets, ואף קבצים וירטואליים כמו /proc). זה לא מילולית "הכול", אלא איחוד ממשק הגישה.

  • סיגנלים: אותות מספריים קצרים (בתחום 1-31) המיועדים להודעה על אירועים.

  • חסרונות תקשורת מבוססת קבצים: גישה לדיסק איטית ולעיתים נוצר קובץ זמני שצריך למחוק.

4.2. קצת על IPC

המונח IPC (Inter-Process Communication) הוא שם כולל למנגנוני תקשורת בין תהליכים.

מנגנון מי מתקשר אופי ההעברה שימוש נפוץ
signals בין תהליכים באותה מכונה, או ממערכת ההפעלה לתהליך אסינכרוני, מטא־מידע קצר הודעה/עצירה/המשך
pipes, FIFOs בין תהליכים באותה מכונה זרם נתונים חד-כיווני יצרן-צרכן
sockets בין תהליכים באותה מכונה או במכונות שונות דו-כיווני תקשורת רשת

4.3. מה זה סיגנל?

סיגנל הוא מנגנון לשליחת הודעה אסינכרונית לתהליך:

  • בין תהליך לתהליך, וגם ממערכת ההפעלה לתהליך.
  • ממומש בתוכנה בלבד (ללא תמיכת חומרה).
  • מגיע בכל רגע, בלי שהתהליך צריך "להאזין" לו.
  • לא קיים סיגנל 0, שליחה עם sig == 0 משמשת לבדיקה בלבד: האם התהליך קיים והאם יש הרשאה לשלוח לו סיגנל (ללא שליחה בפועל).

אירוע אסינכרוני הוא אירוע חיצוני לקוד המשתמש שקוטע את הריצה וגורם לביצוע שגרת טיפול.

הערה: סיגנלים אינם פסיקות. פסיקות מטופלות במצב גרעין, בעוד שסיגנלים מטופלים במצב משתמש בעת החזרה מהגרעין.

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

4.4. קריאת המערכת kill

פעולה: שולחת את הסיגנל שמספרו sig לתהליך המזוהה על-ידי pid.

  • אם sig == 0 — מתבצעת בדיקת קיום התהליך ללא שליחת סיגנל.
  • ערך מוחזר: 0 בהצלחה, -1 בכישלון (למשל אם אין תהליך בעל pid).

השם kill מטעה: הקריאה משמשת לשליחת כל סוגי הסיגנלים, לא רק SIGKILL. פקודת kill בלינוקס קוראת לקריאת המערכת הזו.

4.5. מנגנון העברת הסיגנלים

סיגנל עובר שני שלבים: רישום (generation) ואז טיפול (delivery).

שלב מה קורה בפועל
רישום מערכת ההפעלה מסמנת ב-PCB של התהליך שיש סיגנל ממתין (pending) באמצעות מערך ביטים. לכל מספר סיגנל יכול להיות לכל היותר סיגנל ממתין אחד.
טיפול בכל חזרה ממצב גרעין למצב משתמש, מערכת ההפעלה בודקת את הביטים ומטפלת בסיגנלים לפי הסדר (מהנמוך לגבוה).
  • לכל תהליך נשמרים ב-PCB שני מערכי ביטים:
    • pending — סיגנלים שממתינים לטיפול.
    • blocked — מסכת הסיגנלים החסומים (Signal Mask).
  • סיגנל יטופל רק אם הוא ממתין ואינו חסום; סיגנלים חסומים נשארים במצב pending עד להסרתם מהמסכה.
  • בנוסף נשמרת טבלת פעולות/מצביעים ל-handlers עבור כל סיגנל (ברירת מחדל SIG_DFL).

בסיום טיפול בסיגנל, הביט המתאים מתאפס.

הערה: כאשר תהליך נמצא במצב TASK_INTERRUPTIBLE עקב קריאה חוסמת, רישום סיגנל עשוי לשבור את ההמתנה ולגרום לקריאה להחזיר את השגיאה -EINTR.

4.6. טיפול בסיגנלים

לתהליך יש כמה דרכי תגובה לסיגנל:

אופן טיפול משמעות
terminate סיום התהליך
ignore התעלמות והמשך עבודה
stop עצירה למצב TASK_STOPPED (לרוב בשליטת debugger)
continue המשך תהליך שהיה עצור
catching הפעלת שגרת משתמש (signal handler)

4.6.1. קריאת המערכת signal

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

פעולה: משנה את אופן הטיפול בסיגנל שמספרו signum.

  • signum בתחום 1-31, פרט ל-SIGKILL ול-SIGSTOP (לא ניתנים ללכידה/התעלמות).
  • handler יכול להיות מצביע לפונקציית משתמש, או SIG_DFL, או SIG_IGN.
  • ערך מוחזר: בהצלחה — ה-handler הקודם; בכישלון — SIG_ERR.

חשוב: לא כל פונקציה בטוחה לקריאה מתוך handler (למשל printf, malloc). טכניקה נפוצה היא להגדיר דגל בתוך ה-handler ולבדוק אותו בקוד הראשי.

4.6.2. המבנה signal_struct

ב-PCB שמור מבנה עם פעולות הטיפול לכל סיגנל. לכל סיגנל נשמר אחד מאלה:

  • SIG_DFL — טיפול ברירת מחדל.
  • SIG_IGN — התעלמות.
  • מצביע ל-signal handler שהוגדר ע"י המשתמש.

דוגמה: ברירת המחדל של SIGFPE היא סיום עם dump. אפשר להחליף זאת ב-handler, אך המשך ריצה אחרי SIGFPE הוא undefined behavior ולכן כמעט תמיד לא רצוי.

4.6.3. שגרות טיפול בסיגנלים

שגרת טיפול רצה במצב משתמש ובהקשר של התהליך שקיבל את הסיגנל.

  • ההקשר (registers וכו') נשמר לפני ההרצה ומשוחזר אחריה.
  • אין החלפת הקשר תהליך — רק "קפיצה" זמנית ל-handler.
  • בזמן ההרצה נחסם זמנית הסיגנל שגרם ל-handler, כדי למנוע בעיות reentrancy.

4.7. מיסוך ושליטה בסיגנלים (Signal Masking & Control)

  • ניתן לחסום סיגנלים (למעט SIGKILL ו-SIGSTOP) באמצעות sigprocmask.
    • SIG_BLOCK להוספה למסכה, SIG_UNBLOCK להסרה, SIG_SETMASK לדריסה מלאה.
  • עבודה עם סטים של סיגנלים נעשית עם הטיפוס sigset_t והפונקציות: sigemptyset, sigfillset, sigaddset, sigismember.
  • sigaction היא חלופה מתקדמת ל-signal ומספקת שליטה מדויקת יותר (למשל מסכת חסימה בזמן handler ודגלים כמו SA_RESTART/SA_SIGINFO).

4.8. שליחת סיגנלים

4.8.1. שליחת סיגנלים בין תהליכים (דרך ה-Shell)

שימוש נפוץ בסיגנלים הוא שליטה בהרצת תהליך דרך ה-shell.

פעולה סיגנל טיפול ברירת מחדל
Ctrl+C SIGINT סיום התהליך
Ctrl+Z SIGTSTP עצירת התהליך

השולח בפועל הוא ה-shell.

4.8.2. שליחת סיגנלים ע"י מערכת ההפעלה

תרחיש טיפוסי:

  1. תהליך מבצע פעולה לא חוקית (למשל גישה לכתובת NULL).
  2. המעבד יוצר חריגה ועובר לשגרת טיפול במצב גרעין.
  3. מערכת ההפעלה מטפלת בחריגה ורושמת סיגנל לתהליך.
  4. בחזרה למצב משתמש, ה-signal handler (או טיפול ברירת מחדל) מופעל.

4.9. התמודדות עם EINTR בקריאות חוסמות

  • קריאה חוסמת שנקטעת ע"י סיגנל עשויה להחזיר -1 ו-errno == EINTR.
  • התבנית המקובלת היא לנסות מחדש בלולאה:
ssize_t n;

do {
  n = read(fd, buf, count);
} while (n == -1 && errno == EINTR);

if (n == -1) {
  perror("read");
}

4.10. סיגנלים נפוצים (Signals Enumeration)

סיגנל משמעות
SIGBUS חריגת חומרה: גישה לכתובת לא תקינה (bus error).
SIGILL פקודה לא חוקית.
SIGFPE שגיאה אריתמטית (כמו חלוקה באפס), למרות השם שמצביע על נקודה צפה.
SIGCHLD נשלח לאב כאשר בן מסתיים או נעצר.
SIGALRM נשלח לאחר זמן שהוגדר (alarm, setitimer).
SIGTRAP משמש לדיבוג והרצה צעד-אחר-צעד.
SIGUSR1, SIGUSR2 סיגנלים לשימוש חופשי של האפליקציה (למשל daemon).
SIGXCPU חריגה ממגבלת CPU "רכה" (setrlimit).
SIGPIPE כתיבה ל-pipe כשאין קורא.
SIGIO קלט/פלט מוכן; נפוץ ב-I/O אסינכרוני (ב-non-blocking לרוב מתקבלים EAGAIN/EWOULDBLOCK).

4.11. השוואה: פסיקות לעומת סיגנלים

מאפיין פסיקות (Interrupts) סיגנלים (Signals)
מי מעורר חומרה תוכנה/OS או תהליך
יעד CPU/ליבה תהליך
תזמון לרוב אסינכרוני אסינכרוני או סינכרוני (למשל בעקבות שגיאה בתהליך)
מי מגדיר משמעות חומרה/OS ברירת מחדל של OS, ניתן להחליף ע"י handler
היכן מטופל תמיד במצב גרעין במצב משתמש בעת חזרה מהגרעין
דוגמאות שעון מערכת, I/O SIGSEGV, SIGCHLD, SIGINT

4.12. מה צריך לזכור?

  • סיגנל הוא הודעה אסינכרונית קצרה, אין צורך "להאזין" לה.
  • שליחה מתבצעת בשני שלבים: רישום ואז טיפול בעת חזרה למצב משתמש.
  • kill שולחת סיגנל, ו-sig == 0 משמש רק לבדיקה.
  • אפשר להתעלם, לעצור, להמשיך, לסיים או לטפל באמצעות handler; SIGKILL ו-SIGSTOP אינם ניתנים ללכידה.
  • ניתן לחסום סיגנלים באמצעות מסכה (sigprocmask), ולנהל handlers באופן מדויק עם sigaction.
  • קריאות חוסמות עשויות להיקטע ולהחזיר EINTR, ולכן לעיתים עוטפים בלולאת retry.
  • בתוך handler עובדים בזהירות: לשמור אותו קצר ולהימנע מפונקציות לא בטוחות.
מקור הסיגנל זרימת האירועים בקצרה
OS → תהליך חריגה → רישום ב-PCB → טיפול בעת חזרה למצב משתמש
תהליך→תהליך kill → רישום ב-PCB → טיפול בעת חזרה למצב משתמש