2.4 הcontext switch הרצאה
1. מבוא¶
הcontext switch הוא תהליך שקורה לעיתים במעבד שלנו.
פעולה זו קורת כאשר אנחנו צריכים לשמור את כל האוגרים לפני שאנחנו מבצעים קטע קוד מסוים. למשל, כשאנחנו קופצים לפסיקה קרנלית מהיוזר מוד, אנחנו נעשה context switch כדי לשמור את כל האוגרים של היוזר מוד בצד, לאחר מכן- נבצע את הפעולה הקרנלית, בסיומה נחזור ליוזר מוד ונשחזר את המצב הקודם של האוגרים.
למעשה, ביצענו context switch- שנינו את ה"קונטקסט" (האוגרים) כדי לבצע פעולה מסוימת- ואז שחזרנו את ה"קונטקסט" הקודם.
אז במה עוזר לנו מגנון הcontext switch?
1. ריבוי‑משימות (Multitasking): נוכל לגרום למעבד יחיד להריץ מספר תוכנות במקביל באמצעות הcontext switch. בכך שהמעבד “יזגזג” בין כמה תוכנות (קטעי קוד) מהר מאוד, כדי לתת אשליה של ריצה בו‑זמנית.
-
בידוד: תוכנה A לא רשאית להשפיע על תוכנה B – לכן חייבים לשמור בנפרד את כל הרגיסטרים והזיכרון שלו.
-
תגובה לפעילות חיצונית: פסיקות חומרה (מקלדת, דיסק, טיימר) יכולות לקטוע תהליך בכל רגע; הקרנל חייב לעצור‑לשמור‑להמשיך בצורה חלקה (בדומה למה שקורה עם פסיקה קרנלית שהסברתי למעלה).
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 שומר את הקונטקסט של התוכנה הקודמת (האוגרים וכולי) לפני שהוא עובר להריץ את הקוד השני.
למשל, דמיינו כשאנחנו עושים פסיקה מיוזר מוד לקרנל, אז כל האוגרים היוזרמודים נשמרים בTSS (כל הקונטקסט), וכשהקרנל חוזר ליוזר מוד- הוא משתמש בTSS כדי לשחזר את הקונטקסט
המעבר מ־Ring 3 ל־Ring 0 בפסיקה (int)¶
המעבד לא סומך על תוכנה!
אז ברגע שאתה עושה int 0x80 מתוך User Mode (Ring 3), המעבד בעצמו:
-
בודק את ה־IDT ורואה שאתה מנסה לקפוץ ל־Ring 0 (קרנל).
-
שואל: "אבל איפה הקרנל ישים את המחסנית שלו (stack)?"
-
התשובה: ב־TSS יש שדות מיוחדים:
-
ה
esp0: כתובת המחסנית של הקרנל - ה
ss0: סגמנט הנתונים של הקרנל
המעבד לוקח את הערכים האלה – ועובר למחסנית הזו אוטומטית!
-
אחרי שעבר למחסנית החדשה (הקרנלית), המעבד:
-
דוחף את הרגיסטרים מ־User Mode (EIP, CS, EFLAGS, ESP, SS)
-
ואז קופץ ל־handler שלך – הקוד של הפסיקה
-
שורה תחתונה: המעבד משתמש ב־TSS כדי לדעת לאן לעבור כשמתבצע מעבר הרשאה.
ומה יש בתוך TSS?¶
ה־TSS הוא מבנה מסודר בזיכרון – בגודל כמה עשרות בתים – עם שדות קבועים:
| שדה | תפקיד |
|---|---|
esp0 |
ה־stack pointer של הקרנל – כשהמעבד עובר מ־Ring 3 ל־0 |
ss0 |
סגמנט הנתונים של הקרנל |
eip |
כתובת הקוד |
eax… |
כל הרגיסטרים הרגילים |
| ועוד... |
אבל- שיקרתי לכם.
למעשה, רוב מערכות ההפעלה לא משתמשות בכל השדות בTSS- רובן לא משתמשות בTSS כדי לשמור אוגרים, אלה בדרך כלל רק את האוגרים של המחסנית (הESP והSS).
ולרוב- מערכות הפעלה לא משתמשות בפיצ'ר התוכנתי הזה כדי להעביר אוגרים, ויש להם דרכים תוכנתיים משלהם.
במקום זה עושים Software Context Switch – הקרנל שומר/טוען ידנית את הרגיסטרים. אבל עדיין משתמשים ב־TSS בשביל לדעת מה ה־stack של הקרנל כשעושים int.
דוגמה מוחשית:¶
נניח שהגדרת את השדות האלה ב־TSS:
והמשתמש עכשיו עושה:
מה המעבד עושה?
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 עבור TSS (32‑bit) ‑‑‑‑
gdt_tss: ; Index 6 למשל
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
4. איך מריצים כמה תוכנות במקביל? (Multitasking בסיסי)¶
נניח שאנחנו רוצים להריץ שתי תוכנות על מעבד אחד – איך נעשה את זה?
המחשב הרי לא באמת יכול להריץ שני דברים בדיוק באותו זמן (לפחות לא במעבד יחיד). אז מה עושים?
הפתרון: Multitasking מדומה¶
המחשב פשוט מחליף בין התוכנות ממש מהר – כל כמה מילי-שניות הוא שומר את מצב התוכנה הנוכחית (כל האוגרים, הזיכרון, ועוד), טוען את המצב של תוכנה אחרת – וממשיך לרוץ משם.
ככה נוצרת אשליה של ריבוי משימות.
אבל איך מחליפים בין תוכנות?
4.1 שימוש ב־טיימר (Timer Interrupt / IRQ0)¶
לכל מחשב יש טיימר חומרתי – רכיב קטן שקיים בתוך הלוח־אם.
הוא מוגדר כך שיקטע (interrupt) את המעבד בדיוק כל X זמן – למשל כל 10ms.
כשהטיימר פועל, הוא יוצר פסיקת חומרה (Hardware Interrupt) שנקראת IRQ0.
בפסיקה הזו, אנחנו יכולים לקפוץ לקוד מסוים – ולבצע מה שנרצה.
אז אם נגדיר שהפסיקה הזאת תריץ קוד שלנו (עם handler) נוכל לממש "Scheduler".
הScheduler היא מערכת שאחראית לדאוג שמערכת ההפעלה שלנו תדע להריץ כמה תוכנות במקביל.
נוכל להגדיר רשימה של תוכנות שירצו במקביל.
הScheduler פעם ב10 מילי שניות, יעשה context switch לתוכנה הנוכחית ויריץ קוד של תוכנה מסוימת מתוך רשימת התוכנות שרצות במקביל.
כך, פעם ב10 מילי שניות- נוכל להחליף את התוכנה שאנחנו מריצים- וליצור אשליה שיש כמה תוכנות שרצות במקביל.
5. איך עובד ה־Scheduler?¶
ה־Scheduler הוא רכיב בתוך מערכת ההפעלה שמחליט איזה תוכנה לרוץ עכשיו.
אז מה בדיוק קורה בזמן הפסיקה?¶
-
הטיימר פועל: כל 10 מילי־שניות, הטיימר שולח פסיקה (IRQ0).
-
המעבד עובר לקוד הפסיקה – בדיוק כמו שהוא עובר לקוד כשיש הקלדה במקלדת.
-
הhandler של הפסיקה- (בקרנל) שומר את המצב של התוכנה הנוכחית – כלומר את כל הרגיסטרים שכרגע במעבד (כי אנחנו תכף נחליף תוכנה).
-
קוראים ל־Scheduler – והוא בוחר תהליך אחר מתוך רשימה של תהליכים מוכנים (ready).
-
טוענים את המצב של התהליך החדש – כל הרגיסטרים שהיו לו, ה־stack, ועוד.
-
חוזרים להריץ אותו – כאילו לא קרה כלום.
ככה מתבצע context switch באופן מחזורי.
6. דוגמת קוד: איך נראה באמת ה־Scheduler?¶
כדי להבין איך כל זה קורה בפועל, נכתוב דוגמה פשוטה של קוד הפסיקה:

שלב ראשון: שמירת מצב התוכנה הנוכחית¶
כאשר מתבצע פסיקה מהIRQ (פעם בכמה מילי שניות), התוכנה היוזרמודית נעצרת- ומתחילה לרוץ הsecheduler שהגדרנו בקרנל.
הscheduler קודם כל שומר את כל האוגרים של התוכנה הנוכחית.
קודם נוודא שלא יהיה פסיקה בזמן שמירת האוגרים.
דחיפת כל האוגרים של התוכנה היוזר מודית. (לשמור את הקונטקסט שלה)
שלב שני: שמירה לזיכרון¶
חוץ מאוגרים הscheduler שומר עוד הגדרות שנלמד עליהם בהמשך שקשורים לתוכנה הנוכחית שרצה בתוך מבנה נתונים בשם "PCB"
mov eax, [current_pcb] ; מצביע למבנה של התוכנה הנוכחית
mov [eax].regs, esp ; שמירת כל המצב במחסנית של התהליך
כאן אנחנו "משתילים" את מצב המעבד בתוך מבנה זיכרון מיוחד לכל תוכנה – זה ה־PCB (Process Control Block)
עכשיו, בזכרון בקרנל (איפשהו בסגמנט הדאטה) אנחנו שומרים PCB שמכיל את כל המידע והקונטקסט של התוכנה היוזר מודית שרצה.
שלב שלישי: בחירת תוכנה חדשה¶
לאחר ששמרנו את כל המידע של התוכנה הנוכחית, אנו מוכנים לקונטקסט סוויץ-
; אל תפריע בזמן שמירת מצב
כדי לדעת לאיזה תוכנה לקפוץ מרשימת התוכנות הרצות- נקרא לפונקצית "scheduler_pick_next",
call scheduler_pick_next ; פונקציה שבוחרת את התוכנה הבאה
mov [current_pcb], eax ; זוכר מי התוכנה הבאה
למעשה, הפונקציה החזירה לנו את הPCB של התוכנה השניה שאלייה נרצה לקפוץ.
תוכלו לממש את הפונקציה scheduler_pick_next עם איזה אלגוריתם שתרצו, למעשה קיימות המון שיטות ואלגוריתמים שונים למימוש scheduler- כדי לדעת איזה תוכנה להריץ תלוי בחשיבותה במערכת ההפעלה.-
שלב רביעי: טעינת התוכנה החדשה¶
השורה הבאה טוענת את הזכרון של התוכנה הבאה (שנבחרה)- נדבר בהמשך על cr3 ואיך זה עובד.
השורה הבאה טוענת את כתובת הבסיס של המחסנית של הקרנל לתוך הtss.
מזכיר: אחרי שנקפוץ לתוכנה החדשה, כאשר התוכנה תקפוץ לקרנל ותבצע קונטקסט סוויץ, חומרתית המעבד ישתמש בTSS כדי לקבל את כתובת המחסנית של הקרנל.
שלב חמישי: שחזור המצב וחזרה¶
mov esp, [eax].regs ; מחזיר את המחסנית שהייתה לתהליך
pop es
pop ds
popad
sti ; מחזיר את הפסיקות
iret ; חוזר להריץ את התהליך החדש
בשלב הזה המעבד "מתעורר" בתוך תוכנה אחרת.
מבט רחב יותר – איך עושים את זה במערכות הפעלה ישנות וחדשות?¶
| מערכת הפעלה | איך בוצע ה־context switch? | הערות |
|---|---|---|
| DOS | אין Multitasking בכלל | רק תוכנה אחת בכל רגע |
| Windows 3.1 | Hardware Task Gate – המעבד מחליף לבד TSS | איטי, לא גמיש |
| Linux / Windows מודרניים | Software Switch – הקרנל שומר/טוען בעצמו | הרבה יותר גמיש ומהיר |
סיכום כולל עד כו¶
- מערכת הפעלה מורכבת מהקרנל והיוזר מוד. כאשר לקרנל יש גישה לחומרה עם IN ו- OUT ועוד, וליוזר מוד אין.
- כאשר אנחנו מריצים את מערכת ההפעלה לראשונה, קודם הקרנל רץ כדי להגדיר את כל ההגדרות של מערכת ההפעלה.
- אחת ההגדרות של הקרנל היא הGDT (כדי להגדיר אזור זכרון עם הרשאות נמוכות ואזור עם הרשאות גבוהות)
- עוד הגדרה היא הIDT- כדי שהקוד היוזר מודי יוכל לבצע פסיקות ולקפוץ לקוד קרנלי מוגדר מראש, וכו.
- לאחר שהקרנל סיים להגדיר את המערכת, הוא קופץ ליוזר מוד לתוכנה שמוגדרת לרוץ ראשונה (אמרנו שבדרך כלל התוכנה הזו היא תוכנה שאחראית להריץ את כל התוכנות האחרות במערכת ההפעלה)
- אמרנו, שהקפיצות בין היוזר מוד לקרנל עם הפסיקות קורות עם הTSS- שמאפשר לנו לשמור את הקונטקסט של כל התוכנה היוזר מודית בקלות לפני שקופצים לקרנל.
- אמרנו גם שלרוב במערכות הפעלה לא משתמשים בכל הTSS, ורק באוגרי המחסנית.
- בנוסף, טיימר הIRQ קורה פעם בכמות מוגדרת של מילי שניות לפסיקה שהגדרנו בIDT שאחראית לעצור את התוכנה היוזר מודית הנוכחית שרצה, ולהריץ תוכנה אחרת שנמצאת ברשימת התוכנות.
- בזכרון של הקרנל קיים רשימה עם המון PCB-ים שמגדירים בדיוק איזה תוכנות רצות כרגע במערכת ומה הקונטקסט שלהן- כך הקרנל יודע איזה תוכנות רצות והscheduler בכל פסיקה של הטיימר בוחר את אחת התוכנות על סמך פרמטרים כלשהן ומריץ אותן.
- הטיימר עושה המון פסיקות, (כל כמה מילי שניות)- ומרוב מהירות החישוב הגבוהה של המעבדים המודרנים- גם אם למעבד יש ליבה אחת המעבד באמצעות הטיימר יודע להריץ תוכנות "במקביל", בצורה שנראית לעין כיאלו שהתוכנות באמת רצות במקביל.
מדהים! יש לנו מערכת הפעלה שיודעת להריץ כמה תוכנות במקביל, כאשר לתוכנות אין הרשאות גבוהות- אך יכולות לבצע פעולות מוגדרות מראש (עם הרשאות גבוהות) שמוגדרות בפסיקות.
יש עדיין כמה דברים שחסרים, תוכנות יוזרמודיות עדיין נמצאות באותו מרחב זכרון. הן יכולות לגשת לזכרון אחת של השניה. (עם הן ידעו מה הכתובת בזכרון של השניה)
על זה נדבר בשיעור הבא :)