לדלג לתוכן

בפרק 1.6 למדנו על פסיקות במצב 16 ביט - ראינו את טבלת הIVT, את הפקודה INT, ואיך הBIOS וDOS מספקים שירותים דרך פסיקות.
בפרק 2 ראינו בקצרה את הIDT - טבלת הפסיקות של Protected Mode, ואיך היא מחליפה את הIVT הישנה.
עכשיו הגיע הזמן להבין לעומק איך הקרנל של לינוקס מטפל בפסיקות וב-exceptions על ארכיטקטורת x86-64 המודרנית.

פסיקות ו-exceptions - interrupts and exceptions

מה זה בכלל פסיקות?

פסיקה (interrupt) היא אירוע שגורם למעבד להפסיק את מה שהוא עושה עכשיו ולקפוץ לפונקציית טיפול (handler) שהוגדרה מראש.

זוכרים את הרעיון מפרק 1.6? אותו רעיון בדיוק - רק שעכשיו אנחנו ב-64 ביט, בProtected Mode (ליתר דיוק - ב-Long Mode), עם קרנל שלם שמנהל את כל התהליך.

יש שני סוגים עיקריים של פסיקות:

פסיקות חומרה - hardware interrupts (IRQs)

אותות שמגיעים מהתקנים חיצוניים:
- מקלדת - לחצתם על מקש? המקלדת שולחת IRQ
- דיסק - הדיסק סיים לקרוא נתונים? הוא שולח IRQ
- כרטיס רשת - הגיעה חבילת רשת? כרטיס הרשת שולח IRQ
- טיימר - הטיימר של המערכת מתקתק ושולח IRQ בקצב קבוע (זה מה שמאפשר למתזמן לעבוד - זוכרים מפרק 6.3?)

חריגות - exceptions

נגרמות על ידי המעבד עצמו, כתוצאה ממשהו שקרה בזמן ריצת הקוד:
- שגיאת דף - page fault (#PF) - גישה לכתובת שלא ממופה (זוכרים מפרק 6.4?)
- חלוקה באפס - divide error (#DE) - ניסיון לחלק באפס
- הגנה כללית - general protection fault (#GP) - ניסיון לבצע פעולה אסורה
- נקודת עצירה - breakpoint (#BP) - פקודת int3 (ככה debuggers עובדים!)
- קוד לא חוקי - invalid opcode (#UD) - המעבד נתקל בפקודה שהוא לא מכיר


טבלת הIDT - Interrupt Descriptor Table

הIDT היא מערך של 256 כניסות (entries), כאשר כל כניסה מצביעה על פונקציית טיפול (handler) בקרנל.

בפרק 1.6 ראינו את הIVT (Interrupt Vector Table) שהיתה פשוט מערך של כתובות בזיכרון. הIDT היא הגרסה המתקדמת שלה - כל כניסה מכילה לא רק כתובת, אלא גם מידע על הרשאות, סוג הgate, וסגמנט היעד.

איך הIDT מוגדרת?

  • הקרנל בונה את הIDT בזמן אתחול המערכת (boot time)
  • הכתובת של הIDT נטענת לרגיסטר מיוחד של המעבד באמצעות הפקודה LIDT
  • כל כניסה היא מבנה בגודל 16 בתים (ב-64 ביט) שמכיל את כתובת הhandler, selector, ודגלים

חלוקת הכניסות

טווח שימוש
0-31 חריגות מעבד (exceptions) - מוגדרות על ידי אינטל, קבועות
32-255 זמינות לפסיקות חומרה (IRQs) ופסיקות תוכנה

חריגות המעבד החשובות (0-31)

מספר סימון שם מתי קורה
0 #DE שגיאת חילוק - Divide Error חלוקה באפס או overflow בחלוקה
1 #DB דיבוג - Debug צעד בודד (single step) או hardware breakpoint
3 #BP נקודת עצירה - Breakpoint הפקודה int3 הופעלה
4 #OF גלישה - Overflow הפקודה into כשדגל OF דלוק
6 #UD פקודה לא חוקית - Invalid Opcode המעבד נתקל בopcode שהוא לא מכיר
7 #NM התקן לא זמין - Device Not Available ניסיון להשתמש בFPU/SSE כשהם כבויים
8 #DF שגיאה כפולה - Double Fault exception קרה בזמן טיפול בexception אחר
13 #GP הגנה כללית - General Protection Fault הפרת הרשאה - ניסיון לבצע פעולה אסורה
14 #PF שגיאת דף - Page Fault גישה לדף שלא ממופה או שאין הרשאה אליו

שימו לב שהDouble Fault (#DF) מספר 8 הוא מצב מיוחד - הוא קורה כשexception נזרק בזמן שהמעבד מנסה לטפל בexception אחר. אם גם הDouble Fault נכשל, המעבד עושה Triple Fault ומאתחל את המחשב מחדש (ריסטרט).


זרימת פסיקת חומרה - מההתקן עד לקרנל

בואו נעקוב אחרי מה קורה כשהתקן חומרה צריך את תשומת הלב של המעבד. ניקח לדוגמה לחיצה על מקש במקלדת:

שלב 1 - ההתקן שולח אות לבקר הפסיקות

המקלדת שולחת אות חשמלי לבקר הפסיקות. במערכות מודרניות זה הAPIC (נדבר עליו בהמשך). במערכות ישנות זה היה הPIC (Programmable Interrupt Controller) - שבט 8259.

שלב 2 - בקר הפסיקות מעביר את הפסיקה למעבד

הAPIC מעביר את הפסיקה למעבד המתאים עם מספר vector (מספר הכניסה בIDT).

שלב 3 - המעבד שומר מצב וקופץ לhandler

המעבד עושה את הדברים הבאים אוטומטית (בחומרה, לא בתוכנה):
1. שומר את הרגיסטרים RFLAGS, CS, RIP על מחסנית הקרנל
2. אם היה מעבר מuser mode לkernel mode - שומר גם SS ו-RSP ומחליף מחסנית
3. מנקה את דגל הIF (Interrupt Flag) - כדי לחסום פסיקות נוספות
4. מחפש את הכניסה המתאימה בIDT
5. קופץ לכתובת הhandler

שלב 4 - הhandler רץ (החצי העליון - top half)

הקרנל מריץ את הhandler - שצריך להיות מהיר מאוד. הוא עושה את המינימום ההכרחי:
- מאשר לחומרה שקיבל את הפסיקה (acknowledge)
- שומר את המידע הדרוש
- מתזמן עבודה כבדה יותר שתרוץ מאוחר יותר (bottom half)

שלב 5 - החצי התחתון רץ מאוחר יותר (bottom half)

העיבוד הכבד של הפסיקה קורה בהמשך, כשפסיקות כבר מותרות שוב.

שלב 6 - חזרה מהפסיקה

הפקודה iretq (interrupt return) משחזרת את כל מה שהמעבד שמר בשלב 3, והריצה ממשיכה מאיפה שהופסקה.


חצי עליון מול חצי תחתון - top half vs bottom half

למה בכלל צריך את החלוקה הזאת? הסיבה פשוטה:

כשהhandler של פסיקה רץ, פסיקות אחרות חסומות (או לפחות הIRQ הנוכחי חסום). אם נעשה עיבוד כבד בhandler, אנחנו נפסיד פסיקות אחרות ומערכת תהפוך ללא רספונסיבית.

לכן עושים חלוקה:

החצי העליון - top half

  • רץ עם פסיקות חסומות (או לפחות הIRQ הנוכחי חסום)
  • חייב להיות מהיר - מיקרושניות בודדות
  • עושה רק את המינימום: מאשר לחומרה, קורא מידע קריטי, מתזמן את הbottom half
  • לא יכול לישון (אי אפשר לקרוא ל-sleep, ללכת לדיסק, להקצות זיכרון עם GFP_KERNEL)

החצי התחתון - bottom half

כאן מתבצע העיבוד האמיתי. יש שלושה מנגנונים שונים:

מנגנון softirqs

  • מוגדרים סטטית בזמן קומפילציה של הקרנל (לא ניתן להוסיף חדשים בזמן ריצה)
  • הכי מהירים ויעילים
  • משמשים למשימות קריטיות כמו עיבוד רשת (NET_TX_SOFTIRQ, NET_RX_SOFTIRQ) ובלוק I/O
  • יכולים לרוץ במקביל על מספר מעבדים (ולכן צריך להיזהר עם נעילות)
  • לא יכולים לישון

מנגנון tasklets

  • בנויים מעל softirqs
  • אפשר להקצות אותם דינמית
  • פשוטים יותר לשימוש - tasklet ספציפי לא ירוץ על שני מעבדים במקביל
  • לא יכולים לישון

מנגנון workqueues - תורי עבודה

  • רצים בהקשר של thread קרנל רגיל
  • יכולים לישון - ולכן מתאימים לעבודה כבדה
  • יכולים להקצות זיכרון, לגשת לדיסק, לבצע פעולות ארוכות
  • איטיים יותר מsoftirqs ומtasklets
  • משמשים למשימות שדורשות עבודה כבדה ולא דחופה

דוגמה - כרטיס רשת

כשכרטיס הרשת מקבל חבילה:
1. חצי עליון: הdriver קורא את החבילה מהחומרה, שם אותה בתור, מאשר את הפסיקה - סיים תוך מיקרושניות
2. חצי תחתון (softirq): הstack של הרשת בקרנל מעבד את החבילה - בודק headers, מחליט לאיזה socket להעביר, מטפל בTCP - יכול לקחת הרבה יותר זמן


הAPIC - Advanced Programmable Interrupt Controller

בפרק 1.6 הזכרנו בקצרה את הPIC (שבב 8259) שהיה בקר הפסיקות במערכות ישנות. במערכות מודרניות השתדרגנו לAPIC.

למה צריך APIC?

הPIC הישן תמך ב-15 קווי פסיקה בלבד, ותמיד שלח פסיקות למעבד אחד בלבד. במערכות מודרניות עם מספר מעבדים (וליבות), זה לא מספיק.

המבנה

מערכת הAPIC מורכבת משני חלקים:

Local APIC - לכל ליבה במעבד יש Local APIC משלה. היא:
- מקבלת פסיקות מהI/O APIC ומ-Local APICים אחרים
- מנהלת את הטיימר המקומי של הליבה
- יכולה לשלוח פסיקות לליבות אחרות (IPI - Inter-Processor Interrupt)

I/O APIC - יושב על לוח האם. הוא:
- מקבל פסיקות מהתקני חומרה חיצוניים
- ממפה כל פסיקת חומרה לvector בIDT
- מנתב את הפסיקה לליבה המתאימה

איזון פסיקות - interrupt balancing

הI/O APIC יכול להפיץ פסיקות בין הליבות השונות, כדי שליבה אחת לא תהיה עמוסה בכל הפסיקות. בלינוקס אפשר לשלוט בזה דרך /proc/irq/<N>/smp_affinity.


קובץ proc/interrupts/

אחד הכלים השימושיים ביותר לבדיקת פסיקות במערכת חיה הוא הקובץ /proc/interrupts:

cat /proc/interrupts

הפלט נראה משהו כזה:

           CPU0       CPU1       CPU2       CPU3
  0:         50          0          0          0   IO-APIC   2-edge      timer
  1:          0          0          0         23   IO-APIC   1-edge      i8042
  8:          0          0          1          0   IO-APIC   8-edge      rtc0
  9:          0          0          0          3   IO-APIC   9-fasteoi   acpi
 14:          0          0          0          0   IO-APIC  14-edge      ata_piix
 15:          0          0        231          0   IO-APIC  15-edge      ata_piix
 19:       1205          0          0          0   IO-APIC  19-fasteoi   eth0
NMI:          0          0          0          0   Non-maskable interrupts
LOC:      12504      11987      12133      12045   Local timer interrupts

מה רואים כאן:
- עמודה שמאלית - מספר הIRQ
- עמודות CPUn - כמה פסיקות כל ליבה טיפלה בIRQ הזה
- סוג הבקר - IO-APIC, MSI וכו'
- שם ההתקן - הdriver שרשום לIRQ הזה

שימו לב לLOC (Local timer interrupts) - זה הטיימר המקומי של כל ליבה, שאחראי על תזמון תהליכים (scheduler tick).


טיפול בחריגות - exceptions בפירוט

בואו נעקוב אחרי חריגה שכולנו מכירים - שגיאת דף - Page Fault.

דוגמה: מה קורה כשתוכנה ניגשת לכתובת לא ממופה?

int *ptr = (int *)0xDEADBEEF;  // כתובת שלא ממופה
*ptr = 42;                      // BOOM - page fault!

הנה מה שקורה צעד אחרי צעד:

שלב 1 - המעבד מזהה שגיאה

המעבד מנסה לתרגם את הכתובת הוירטואלית 0xDEADBEEF לכתובת פיזית דרך טבלאות הדפים. הוא מגלה שאין מיפוי - ומייצר exception מספר 14 (#PF).

שלב 2 - המעבד שומר מצב

המעבד דוחף על מחסנית הקרנל:
- קוד שגיאה (error code) - מכיל מידע על סוג הכשלון (קריאה/כתיבה, user/kernel, דף לא קיים/הפרת הרשאה)
- שומר את הכתובת הפוגעת ברגיסטר CR2

שלב 3 - קפיצה לhandler

הIDT כניסה מספר 14 מצביעה על הפונקציה page_fault בקרנל, שקוראת לפונקציה do_page_fault().

שלב 4 - הקרנל מנתח את המצב

הפונקציה do_page_fault() קוראת את CR2 כדי לדעת איזו כתובת גרמה לבעיה, ואז בודקת:

  1. האם הכתובת שייכת לVMA חוקי? (זוכרים VMAs מפרק 6.4?) - מחפשת את הכתובת בעץ הVMAs של התהליך
  2. אם כן - אולי צריך להקצות דף חדש (demand paging), או לטעון דף מהדיסק (swap), או לבצע copy-on-write
  3. אם לא - הכתובת לא חוקית, התהליך ניגש למקום שלא שייך לו

שלב 5 - תגובה

  • אם אפשר לטפל - הקרנל מקצה דף, ממפה אותו, וחוזר לתוכנה שממשיכה כרגיל (התוכנה אפילו לא יודעת שהיה page fault!)
  • אם לא אפשר לטפל - הקרנל שולח סיגנל SIGSEGV לתהליך (ה-Segmentation Fault המפורסם)

הקשר בין חריגות לסיגנלים

זוכרים סיגנלים מפרק 5.4? עכשיו אנחנו יכולים לסגור את המעגל ולהבין מאיפה הם באמת מגיעים:

חריגה (exception) מספר סיגנל שנשלח מה קורה
שגיאת חילוק - #DE 0 SIGFPE התוכנה חילקה באפס
נקודת עצירה - #BP 3 SIGTRAP הפקודה int3 הופעלה (debugger!)
פקודה לא חוקית - #UD 6 SIGILL המעבד נתקל בopcode שהוא לא מכיר
הגנה כללית - #GP 13 SIGSEGV הפרת הרשאה
שגיאת דף - #PF 14 SIGSEGV (אם לא ניתן לטפל) גישה לזכרון לא חוקי

איך GDB משתמש בbreakpoints

כשאתם שמים breakpoint בGDB, הנה מה שקורה מאחורי הקלעים:

  1. GDB כותב את הבית 0xCC (הopcode של int3) במקום הפקודה הראשונה בכתובת של הbreakpoint
  2. כשהתוכנה מגיעה לנקודה הזו, המעבד מבצע int3
  3. המעבד מייצר exception #BP (מספר 3)
  4. הקרנל מזהה breakpoint exception ושולח SIGTRAP לתהליך
  5. הsyscall ptrace (שGDB משתמש בו) לוכד את הSIGTRAP
  6. GDB מקבל שליטה - ואתם רואים שהתוכנה "עצרה" בbreakpoint

ועכשיו אתם מבינים את כל השרשרת - מהחומרה (exception), דרך הקרנל (IDT handler), ועד לתוכנה (סיגנל)!


סיכום

  • פסיקות חומרה מגיעות מהתקנים דרך הAPIC, חריגות נגרמות על ידי המעבד עצמו
  • הIDT היא טבלה של 256 כניסות שממפה כל מספר פסיקה לhandler בקרנל
  • הטיפול מחולק לtop half (מהיר, פסיקות חסומות) ולbottom half (softirq/tasklet/workqueue)
  • חריגות מעבד הופכות לסיגנלים שנשלחים לתהליך
  • כל הנושאים שלמדנו מתחברים: IDT מפרק 2, סיגנלים מפרק 5.4, VMAs מפרק 6.4, מתזמן מפרק 6.3