לדלג לתוכן

2.4 - הcontext switch - הרצאה

1. מבוא

הcontext switch הוא תהליך שקורה לעיתים במעבד שלנו.
פעולה זו קורת כאשר אנחנו צריכים לשמור את כל האוגרים לפני שאנחנו מבצעים קטע קוד מסוים. למשל, כשאנחנו קופצים לinterrupt קרנלית מהיוזר מוד, אנחנו נעשה context switch כדי לשמור את כל האוגרים של היוזר מוד בצד, לאחר מכן- נבצע את הפעולה הקרנלית, בסיומה נחזור ליוזר מוד ונשחזר את המצב הקודם של האוגרים.
למעשה, ביצענו context switch- שנינו את ה"קונטקסט" (האוגרים) כדי לבצע פעולה מסוימת- ואז שחזרנו את ה"קונטקסט" הקודם.

אז במה עוזר לנו מגנון הcontext switch?
1. ריבוי‑משימות (Multitasking): נוכל לגרום למעבד יחיד להריץ מספר תוכנות במקביל באמצעות הcontext switch. בכך שהמעבד “יזגזג” בין כמה תוכנות (קטעי קוד) מהר מאוד, כדי לתת אשליה של ריצה בו‑זמנית.

  1. בידוד: תוכנה A לא רשאית להשפיע על תוכנה B – לכן חייבים לשמור בנפרד את כל הרגיסטרים והזיכרון שלו.

  2. תגובה לפעילות חיצונית: הinterrupts חומרה (מקלדת, דיסק, טיימר) יכולות לקטוע process בכל רגע; הקרנל חייב לעצור‑לשמור‑להמשיך בצורה חלקה (בדומה למה שקורה עם interrupt קרנלית שהסברתי למעלה).


2. מהו “הקשר” (Context)?

קטגוריה דוגמאות מפורטות
רגיסטרי CPU EAX‑EDX, ESI/EDI, ESP, EBP, EIP, EFLAGS
רשמי סגמנטים CS, DS, ES, FS, GS, SS
אוגרים מיוחדים (נדבר בהמשך) CR3 (Catalyst Paging), DR0‑DR7 (Debug), FLAGS
Stack כתובת מחסנית נוכחית (ESP + SS)

3. התמיכה החומרתית – TSS: Task State Segment

הTSS = Task State Segment
זה מבנה בזיכרון שהמעבד יודע לעבוד איתו בעצמו.
המטרה שלו: לאפשר למעבד לנהל משימות (Tasks) ולעבור ביניהן באופן אוטומטי, כולל שמירה וטעינה של כל הרגיסטרים של משימה אחת, והעברה למשימה הבאה.
הTSS שומר את הקונטקסט של התוכנה הקודמת (האוגרים וכולי) לפני שהוא עובר להריץ את הקוד השני.
למשל, דמיינו כשאנחנו עושים interrupt מיוזר מוד לקרנל, אז כל האוגרים היוזרמודים נשמרים בTSS (כל הקונטקסט), וכשהקרנל חוזר ליוזר מוד- הוא משתמש בTSS כדי לשחזר את הקונטקסט

המעבר מ־Ring 3 ל־Ring 0 בinterrupt (int)

המעבד לא סומך על תוכנה!
אז ברגע שאתה עושה int 0x80 מתוך User Mode (Ring 3), המעבד בעצמו:

  1. בודק את ה־IDT ורואה שאתה מנסה לקפוץ ל־Ring 0 (קרנל).

  2. שואל: "אבל איפה הקרנל ישים את המחסנית שלו (stack)?"

  3. התשובה: ב־TSS יש שדות מיוחדים:

  4. הesp0: כתובת המחסנית של הקרנל

  5. הss0: סגמנט הנתונים של הקרנל

המעבד לוקח את הערכים האלה – ועובר למחסנית הזו אוטומטית!

  1. אחרי שעבר למחסנית החדשה (הקרנלית), המעבד:

    • דוחף את הרגיסטרים מ־User Mode (EIP, CS, EFLAGS, ESP, SS)

    • ואז קופץ ל־handler שלך – הקוד של הinterrupt

שורה תחתונה: המעבד משתמש ב־TSS כדי לדעת לאן לעבור כשמתבצע מעבר הרשאה.

ומה יש בתוך TSS?

ה־TSS הוא מבנה מסודר בזיכרון – בגודל כמה עשרות בתים – עם שדות קבועים:

שדה תפקיד
esp0 ה־stack pointer של הקרנל – כשהמעבד עובר מ־Ring 3 ל־0
ss0 סגמנט הנתונים של הקרנל
eip כתובת הקוד
eax… כל הרגיסטרים הרגילים
ועוד...

אבל- שיקרתי לכם.
למעשה, רוב מערכות ההפעלה לא משתמשות בכל השדות בTSS- רובן לא משתמשות בTSS כדי לשמור אוגרים, אלה בדרך כלל רק את האוגרים של המחסנית (הESP והSS).
ולרוב- מערכות הפעלה לא משתמשות בפיצ'ר התוכנתי הזה כדי להעביר אוגרים, ויש להם דרכים תוכנתיים משלהם.

במקום זה עושים Software Context Switch – הקרנל שומר/טוען ידנית את הרגיסטרים. אבל עדיין משתמשים ב־TSS בשביל לדעת מה ה־stack של הקרנל כשעושים int.

דוגמה מוחשית:

נניח שהגדרת את השדות האלה ב־TSS:

tss.esp0 = 0x7C00
tss.ss0  = 0x10   ; kernel segment

והמשתמש עכשיו עושה:

int 0x80

מה המעבד עושה?
1. הוא בודק – אה, אנחנו מ־Ring 3 עוברים ל־Ring 0.
2. הוא אומר: רגע, אני לא יכול להשתמש ב־ESP הקיים! צריך Stack של הקרנל!
3. הולך ל־TSS:
- טוען את SS ← 0x10
- טוען את ESP ← 0x7C00
4. דוחף לשם את הרגיסטרים הקודמים
5. קופץ ל־handler

ואתם אפילו לא כתבתם את זה – המעבד עשה את זה לבד לגמרי.

נגדיר את הTSS במקום מיוחד בזכרון- בסגמנט משלו, באמצעות הGDT, ניצור Descriptor מסוג 0x9.
לאחר שהגדרנו את הdescriptor של הtss, כשנרצה לטעון את הselector למעבד נשתמש בפעולת ה ltr ax (‘Load Task Register’) כאשר ax יכיל את הסלקטור.

;‑‑‑‑ GDT Descriptor for TSS (32‑bit) ‑‑‑‑
gdt_tss:                ; Index 6 for example
    dw  sizeof_tss-1            ; Limit
    dd  tss32                   ; Base 0‑31
    db  0                       ; Base 32‑39
    db  10001001b               ; Type = 0x9 (32‑bit TSS), Present
    db  00000000b               ; Flags=0, LimitHi=0
    db  0                       ; Base 24‑31
mov  ax, 6*8      ; Selector for TSS (Index×8)
ltr  ax           ; the CPU remembers it in TR

4. איך מריצים כמה תוכנות במקביל? (Multitasking בסיסי)

נניח שאנחנו רוצים להריץ שתי תוכנות על מעבד אחד – איך נעשה את זה?

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

הפתרון: Multitasking מדומה

המחשב פשוט מחליף בין התוכנות ממש מהר – כל כמה מילי-שניות הוא שומר את מצב התוכנה הנוכחית (כל האוגרים, הזיכרון, ועוד), טוען את המצב של תוכנה אחרת – וממשיך לרוץ משם.
ככה נוצרת אשליה של ריבוי משימות.

אבל איך מחליפים בין תוכנות?

4.1 שימוש ב־טיימר (Timer Interrupt / IRQ0)

לכל מחשב יש טיימר חומרתי – רכיב קטן שקיים בתוך הלוח־אם.
הוא מוגדר כך שיקטע (interrupt) את המעבד בדיוק כל X זמן – למשל כל 10ms.

כשהטיימר פועל, הוא יוצר פסיקת חומרה (Hardware Interrupt) שנקראת IRQ0.
בinterrupt הזו, אנחנו יכולים לקפוץ לקוד מסוים – ולבצע מה שנרצה.

אז אם נגדיר שהinterrupt הזאת תריץ קוד שלנו (עם handler) נוכל לממש "Scheduler".

הScheduler היא מערכת שאחראית לדאוג שמערכת ההפעלה שלנו תדע להריץ כמה תוכנות במקביל.
נוכל להגדיר רשימה של תוכנות שירצו במקביל.
הScheduler פעם ב10 מילי שניות, יעשה context switch לתוכנה הנוכחית ויריץ קוד של תוכנה מסוימת מתוך רשימת התוכנות שרצות במקביל.
כך, פעם ב10 מילי שניות- נוכל להחליף את התוכנה שאנחנו מריצים- וליצור אשליה שיש כמה תוכנות שרצות במקביל.


5. איך עובד ה־Scheduler?

ה־Scheduler הוא רכיב בתוך מערכת ההפעלה שמחליט איזה תוכנה לרוץ עכשיו.

אז מה בדיוק קורה בזמן הinterrupt?

  1. הטיימר פועל: כל 10 מילי־שניות, הטיימר שולח interrupt (IRQ0).

  2. המעבד עובר לקוד הinterrupt – בדיוק כמו שהוא עובר לקוד כשיש הקלדה במקלדת.

  3. הhandler של הinterrupt- (בקרנל) שומר את המצב של התוכנה הנוכחית – כלומר את כל הרגיסטרים שכרגע במעבד (כי אנחנו תכף נחליף תוכנה).

  4. קוראים ל־Scheduler – והוא בוחר process אחר מתוך רשימה של processes מוכנים (ready).

  5. טוענים את המצב של הprocess החדש – כל הרגיסטרים שהיו לו, ה־stack, ועוד.

  6. חוזרים להריץ אותו – כאילו לא קרה כלום.

ככה מתבצע context switch באופן מחזורי.


6. דוגמת קוד: איך נראה באמת ה־Scheduler?

כדי להבין איך כל זה קורה בפועל, נכתוב דוגמה פשוטה של קוד הinterrupt:
דיאגרמה: הקרנל מחליף (context switch) בין תוכנה A ותוכנה B, כל אחת עם האוגרים, הזכרון והמידע שלה

שלב ראשון: שמירת מצב התוכנה הנוכחית

כאשר מתבצע interrupt מהIRQ (פעם בכמה מילי שניות), התוכנה היוזרמודית נעצרת- ומתחילה לרוץ הsecheduler שהגדרנו בקרנל.
הscheduler קודם כל שומר את כל האוגרים של התוכנה הנוכחית.
קודם נוודא שלא יהיה interrupt בזמן שמירת האוגרים.

cli

דחיפת כל האוגרים של התוכנה היוזר מודית. (לשמור את הקונטקסט שלה)
pushad                 ; saves all the registers (EAX, EBX, ECX, …)
push ds
push es

שלב שני: שמירה לזיכרון

חוץ מאוגרים הscheduler שומר עוד הגדרות שנלמד עליהם בהמשך שקשורים לתוכנה הנוכחית שרצה בתוך מבנה נתונים בשם "PCB"

mov  eax, [current_pcb]   ; pointer to the current program's structure
mov  [eax].regs, esp      ; save the entire state on the process's stack

כאן אנחנו "משתילים" את מצב המעבד בתוך מבנה זיכרון מיוחד לכל תוכנה – זה ה־PCB (Process Control Block)

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


שלב שלישי: בחירת תוכנה חדשה

לאחר ששמרנו את כל המידע של התוכנה הנוכחית, אנו מוכנים לקונטקסט סוויץ-
; אל תפריע בזמן שמירת מצב
כדי לדעת לאיזה תוכנה לקפוץ מרשימת התוכנות הרצות- נקרא לפונקצית "scheduler_pick_next",

call scheduler_pick_next   ; function that chooses the next program
mov  [current_pcb], eax    ; remembers which program is next

למעשה, הפונקציה החזירה לנו את הPCB של התוכנה השניה שאלייה נרצה לקפוץ.

תוכלו לממש את הפונקציה scheduler_pick_next עם איזה אלגוריתם שתרצו, למעשה קיימות המון שיטות ואלגוריתמים שונים למימוש scheduler- כדי לדעת איזה תוכנה להריץ תלוי בחשיבותה במערכת ההפעלה.-


שלב רביעי: טעינת התוכנה החדשה

השורה הבאה טוענת את הזכרון של התוכנה הבאה (שנבחרה)- נדבר בהמשך על cr3 ואיך זה עובד.

mov  ebx, [eax].cr3
mov  cr3, ebx              

השורה הבאה טוענת את כתובת הבסיס של המחסנית של הקרנל לתוך הtss.
מזכיר: אחרי שנקפוץ לתוכנה החדשה, כאשר התוכנה תקפוץ לקרנל ותבצע קונטקסט סוויץ, חומרתית המעבד ישתמש בTSS כדי לקבל את כתובת המחסנית של הקרנל.

mov  [tss32.esp0], dword [eax].kernel_stack_top

שלב חמישי: שחזור המצב וחזרה

mov  esp, [eax].regs    ; restores the stack that belonged to the process
pop  es
pop  ds
popad
sti                    ; re-enables interrupts
iret                   ; returns to run the new process

בשלב הזה המעבד "מתעורר" בתוך תוכנה אחרת.


מבט רחב יותר – איך עושים את זה במערכות הפעלה ישנות וחדשות?

מערכת הפעלה איך בוצע ה־context switch? הערות
DOS אין Multitasking בכלל רק תוכנה אחת בכל רגע
Windows 3.1 Hardware Task Gate – המעבד מחליף לבד TSS איטי, לא גמיש
Linux / Windows מודרניים Software Switch – הקרנל שומר/טוען בעצמו הרבה יותר גמיש ומהיר

סיכום כולל עד כו

  • מערכת הפעלה מורכבת מהקרנל והיוזר מוד. כאשר לקרנל יש גישה לחומרה עם IN ו- OUT ועוד, וליוזר מוד אין.
  • כאשר אנחנו מריצים את מערכת ההפעלה לראשונה, קודם הקרנל רץ כדי להגדיר את כל ההגדרות של מערכת ההפעלה.
  • אחת ההגדרות של הקרנל היא הGDT (כדי להגדיר אזור זכרון עם הרשאות נמוכות ואזור עם הרשאות גבוהות)
  • עוד הגדרה היא הIDT- כדי שהקוד היוזר מודי יוכל לבצע interrupts ולקפוץ לקוד קרנלי מוגדר מראש, וכו.
  • לאחר שהקרנל סיים להגדיר את המערכת, הוא קופץ ליוזר מוד לתוכנה שמוגדרת לרוץ ראשונה (אמרנו שבדרך כלל התוכנה הזו היא תוכנה שאחראית להריץ את כל התוכנות האחרות במערכת ההפעלה)
  • אמרנו, שהקפיצות בין היוזר מוד לקרנל עם הinterrupts קורות עם הTSS- שמאפשר לנו לשמור את הקונטקסט של כל התוכנה היוזר מודית בקלות לפני שקופצים לקרנל.
  • אמרנו גם שלרוב במערכות הפעלה לא משתמשים בכל הTSS, ורק באוגרי המחסנית.
  • בנוסף, טיימר הIRQ קורה פעם בכמות מוגדרת של מילי שניות לinterrupt שהגדרנו בIDT שאחראית לעצור את התוכנה היוזר מודית הנוכחית שרצה, ולהריץ תוכנה אחרת שנמצאת ברשימת התוכנות.
  • בזכרון של הקרנל קיים רשימה עם המון PCB-ים שמגדירים בדיוק איזה תוכנות רצות כרגע במערכת ומה הקונטקסט שלהן- כך הקרנל יודע איזה תוכנות רצות והscheduler בכל interrupt של הטיימר בוחר את אחת התוכנות על סמך פרמטרים כלשהן ומריץ אותן.
  • הטיימר עושה המון interrupts, (כל כמה מילי שניות)- ומרוב מהירות החישוב הגבוהה של המעבדים המודרנים- גם אם למעבד יש ליבה אחת המעבד באמצעות הטיימר יודע להריץ תוכנות "במקביל", בצורה שנראית לעין כיאלו שהתוכנות באמת רצות במקביל.

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