6.7 פסיקות ו exceptions פתרון
תרגיל 1 - בדיקת פסיקות במערכת חיה¶
-
מספר שורות הפסיקות משתנה ממערכת למערכת, תלוי בחומרה ובדרייברים הטעונים. בדרך כלל נראה 10-30 שורות מספריות (IRQs).
-
תשובות:
- שורת הטיימר: בדרך כלל IRQ 0 (timer). ברוב המערכות הפסיקה מטופלת בליבה 0 (CPU0) כי הטיימר הגלובלי מנותב לליבה הראשונה.
- ההבדל בין timer לLOC: הtimer (IRQ 0) הוא הטיימר הגלובלי של המערכת (PIT או HPET). הLOC (Local timer interrupts) הוא הטיימר המקומי של כל ליבה (Local APIC timer). כל ליבה צריכה טיימר מקומי משלה כדי שהscheduler שלה יוכל לעבוד. לכן LOC מחולק בערך שווה בין כל הליבות.
-
כרטיס רשת: צריך לחפש שמות כמו eth0, enp3s0, ens33 וכדומה. שם הדרייבר מופיע בעמודה האחרונה (למשל e1000, virtio, igb).
-
כשלוחצים מקשים, נראה שIRQ 1 (i8042 - בקר מקלדת PS/2) או IRQ של בקר USB (אם זו מקלדת USB) עולה.
תרגיל 2 - המסע של לחיצת מקש¶
-
חומרה: המקלדת מזהה שמקש נלחץ. בקר המקלדת (i8042 ל-PS/2, או בקר USB) מכין את קוד המקש (scan code) ושולח אות חשמלי לבקר הפסיקות.
-
מבקר הפסיקות למעבד: הI/O APIC מקבל את האות מהמקלדת (IRQ 1 ל-PS/2) ומעביר את הפסיקה לאחת הליבות עם vector number שמתאים לכניסה בIDT.
-
המעבד מקבל פסיקה:
- שומר את RFLAGS, CS, RIP על מחסנית הקרנל
- אם היה ב-user mode - שומר גם SS, RSP ומחליף מחסנית
- מנקה את דגל IF (חוסם פסיקות)
- מחפש בIDT את הכניסה המתאימה לvector
-
קופץ לhandler
-
Top half: הhandler של דרייבר המקלדת:
- קורא את הscan code מהבקר (פקודת IN מפורט 0x60)
- מאשר את הפסיקה לAPIC
- שם את הscan code בתור ומתזמן bottom half (tasklet או workqueue)
-
חוזר מהירות - כמה מיקרושניות
-
Bottom half:
- ממיר את הscan code לkeycode
- מטפל בkeymap (מיפוי לתו בפועל, כולל shift, caps lock וכו')
- שולח אירוע input למערכת הinput של הקרנל
-
מערכת הinput מעבירה את האירוע לדרייבר הTTY המתאים
-
הגעה לתהליך:
- דרייבר הTTY שם את התו בבאפר הקלט של הטרמינל
- אם התהליך (למשל shell) ממתין בread() על stdin, הקרנל מעיר אותו
- התו מועתק ליוזר (copy_to_user) וread() חוזר
- אם הטרמינל במצב echo, הTTY driver גם שולח את התו חזרה למסך
תרגיל 3 - דיבאגר ו-breakpoints¶
- ההבדל בין hardware breakpoint ל-software breakpoint:
- Software breakpoint: GDB כותב
0xCC(int3) במקום פקודה בזיכרון. אפשר לשים כמה שרוצים (כל עוד יש פקודות להחליף). חייב לשנות את הזיכרון של התוכנה. -
Hardware breakpoint: משתמש ברגיסטרים מיוחדים של המעבד (DR0-DR3 ב-x86). המעבד בודק את הכתובות האלה אוטומטית בכל פקודה. מוגבל ל-4 breakpoints בלבד, אבל יכול לעצור גם על גישה לזיכרון (לא רק על ביצוע קוד) - watchpoints.
-
תשובות:
- למה שומר את הבית המקורי: כי GDB צריך לשחזר אותו כשמסירים את הbreakpoint, או כשממשיכים ריצה. בלי הבית המקורי, הפקודה שהיתה שם נהרסת.
- השרשרת: המעבד מבצע
int3-> exception #BP (מספר 3) -> המעבד קופץ לכניסה 3 בIDT -> הhandler בקרנל מזהה breakpoint exception -> הקרנל שולח SIGTRAP לתהליך -> הsyscall ptrace (שGDB הפעיל מראש עם PTRACE_TRACEME או PTRACE_ATTACH) לוכד את הסיגנל -> GDB מקבל שליטה ומציג את הprompt -
מה GDB עושה ב-continue: GDB משחזר את הבית המקורי -> מפעיל single step (דגל TF ב-RFLAGS) כדי לבצע את הפקודה המקורית -> אחרי פקודה אחת מגיע עוד SIGTRAP (מhardware single step) -> GDB שם בחזרה את 0xCC -> ממשיך ריצה רגילה
-
למה SIGTRAP: הסיגנל SIGTRAP מיועד ספציפית לדיבוג (trap). ברירת המחדל שלו היא לעצור את התהליך ולייצר core dump. הוא לא משמש לשום דבר אחר במהלך ריצה רגילה (בניגוד לSIGSEGV או SIGFPE שמגיעים משגיאות אמיתיות), ולכן הוא הבחירה הטבעית - אם תהליך מקבל SIGTRAP, זה כמעט בוודאות בגלל debugger.
תרגיל 4 - חצי עליון מול חצי תחתון¶
- כרטיס רשת - softirq: הכי מתאים כי:
- צריך ביצועים גבוהים מאוד (מיליון חבילות בשנייה)
- לא צריך לישון (אין גישה לדיסק)
- בדיוק בשביל זה softirqs נוצרו - NET_RX_SOFTIRQ ו-NET_TX_SOFTIRQ הם softirqs ייעודיים לרשת
-
softirqs יכולים לרוץ במקביל על כמה ליבות, מה שחיוני לביצועי רשת
-
מקלדת USB - tasklet: הכי מתאים כי:
- הקצב נמוך (עשרות לחיצות בשנייה) - לא צריך את הביצועים של softirq
- העיבוד פשוט ולא דורש שינה
- tasklet פשוט יותר לשימוש וקל לתחזוקה
-
tasklet לא ירוץ במקביל על שתי ליבות, מה שמפשט את הקוד (לא צריך נעילות)
-
דרייבר דיסק - workqueue: הכי מתאים כי:
- צריך להקצות זיכרון (עשוי לדרוש GFP_KERNEL שיכול לישון)
- עשוי לדרוש גישה לדיסק (שינה)
- softirqs ו-tasklets לא יכולים לישון - אם ננסה לגשת לדיסק מתוכם, המערכת תתקע
- workqueue רץ בהקשר של kernel thread ולכן יכול לישון בביטחון
תרגיל 5 - חריגות וסיגנלים¶
- ניתוח השורות:
שורה A (int x = 1 / 0;):
- חריגה: #DE - Divide Error
- מספר: 0
- סיגנל: SIGFPE (מספר 8)
- הערה: למרות שהשם הוא "Floating Point Exception", הסיגנל נשלח גם על חלוקה שלמה באפס
שורה B (*ptr = 42; עם ptr = 0xDEADBEEF):
- חריגה: #PF - Page Fault
- מספר: 14
- סיגנל: SIGSEGV (מספר 11)
- הסיבה: הכתובת 0xDEADBEEF לא ממופה בpage tables של התהליך, ואין VMA שמכיל אותה, אז הקרנל לא יכול לטפל וזורק SIGSEGV
שורה C (קפיצה לכתובת 0x12345678):
- חריגה: תלוי - יכולה להיות #PF (אם הכתובת לא ממופה) או #UD (אם הכתובת ממופה אבל מכילה בתים שאינם opcode חוקי) או #GP (אם יש בעיית הרשאות)
- סיגנל: SIGSEGV (אם #PF או #GP) או SIGILL (אם #UD)
- בפועל, סביר שנקבל #PF ואז SIGSEGV כי הכתובת כנראה לא ממופה
- תוכנית עם handler לSIGFPE:
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
void sigfpe_handler(int signum) {
printf("caught SIGFPE - division by zero!\n");
exit(1);
}
int main() {
signal(SIGFPE, sigfpe_handler);
volatile int a = 1;
volatile int b = 0;
int c = a / b; // חלוקה באפס - תגרום לSIGFPE
return 0;
}
הערה: השימוש ב-volatile מונע מהcompiler לבצע את החלוקה בזמן קומפילציה (אופטימיזציה). בלי volatile, הcompiler עלול לזהות חלוקה באפס ולדווח על שגיאה בזמן קומפילציה.
- למה page fault לא תמיד גורם לSIGSEGV:
שתי דוגמאות לpage fault "רגיל":
-
הקצאת עמודים עצלנית - demand paging: כשתוכנה מקצה זיכרון עם malloc (שקורא ל-mmap/brk), הקרנל לא באמת מקצה דפים פיזיים מייד. הוא רק יוצר VMA. כשהתוכנה ניגשת לזיכרון בפעם הראשונה, קורה page fault. הקרנל רואה שיש VMA חוקי, מקצה דף פיזי, ממפה אותו, וחוזר לתוכנה - שממשיכה בלי לדעת שהיה page fault.
-
החזרת עמוד מהswap: אם הקרנל העביר דף לדיסק (swap) כי נגמר הRAM, והתוכנה ניגשת לאותו דף, קורה page fault. הקרנל מזהה שהדף ב-swap, קורא אותו חזרה מהדיסק לRAM, ממפה אותו, וחוזר לתוכנה.
דוגמה נוספת: Copy-on-Write (COW) - אחרי fork, האב והבן חולקים אותם דפים פיזיים (מסומנים read-only). כשאחד מהם מנסה לכתוב, קורה page fault. הקרנל מעתיק את הדף, נותן לכותב עותק פרטי, וחוזר - בלי SIGSEGV.