לדלג לתוכן

פסיקת Page Fault – והקצאת Page-ים דינמית

1. למה צריך בכלל Page Fault?

עד עכשיו למדנו שה־paging מאפשר לכל תוכנה במערכת לרוץ כאילו יש לה RAM משלה, מבלי לראות את שאר התוכנות.
אבל בפועל, אנחנו לא באמת מקצים את כל הזיכרון הזה מראש – זה לא יעיל, וזה לא תמיד אפשרי.

אז מה עושים?

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

וזה בדיוק מה שנקרא:

2. Page Fault

מה זה?

כאשר תוכנה מנסה לגשת לכתובת שאין לה page מוגדר – או שאין לה הרשאות מתאימות – המעבד לא נותן לה לעשות זאת.
במקום זה, הוא קורא לפסיקה מיוחדת: int 14h – זו פסיקה פנימית של החומרה.

המעבד לא פשוט קורס – הוא עוצר את התהליך ועובר להריץ handler שהקרנל שלך מגדיר.


3. מתי קורה Page Fault?

מצב מה הסיבה לשגיאה?
Page לא קיים בטבלאות ה־bit של Present = 0
ניסיון כתיבה ל־Page שהוא לקריאה בלבד bit של RW = 0
ניסיון גישה מיוזר ל־Page קרנלי bit של User/Supervisor = 0, ו־CPL=3 (Ring 3)

4. איך המעבד מתאר את השגיאה?

המעבד שומר מידע על השגיאה כדי שתוכל להבין מה קרה בדיוק.

הוא דוחף ל־Stack:

  1. כתובת ה־EIP שממנה באנו

  2. קוד השגיאה – error code

  3. כתובת ה־CR2 – הכתובת הווירטואלית שגרמה ל־Page Fault!

CR2 = כתובת שהמעבד ניסה לגשת אליה ונכשל.


5. איך אנחנו מטפלים בזה?

אנחנו מגדירים ב־IDT שלנו את handler של הפסיקה int 14h – כלומר את המקום שבו ירוץ הקוד כשיש Page Fault.

idt_14:
    dw page_fault_handler
    dw 0x08                 ; Selector של קרנל
    db 0
    db 10001110b            ; Present, Ring 0
    dw page_fault_handler >> 16

6. כתיבת ה־Handler

page_fault_handler:
    pusha                      ; נשמור את הרגיסטרים
    push ds
    push es

    ; שלב 1 – נקבל את הכתובת שגרמה לשגיאה
    mov eax, cr2              ; הכתובת שהובילה ל־page fault
    mov [fault_address], eax  ; נשמור אותה במשתנה לקריאה בהמשך

    ; שלב 2 – נקבל את error code
    mov ebx, [esp + 32]       ; error code נמצא אחרי pusha + 2 pushes

    ; (אופציונלי) נוכל להדפיס מה קרה, או לבדוק סיבה לשגיאה

    ; שלב 3 – ננסה להקצות Page חדש לכתובת הזו
    call pagefault_resolve

    ; שלב 4 – נחזיר את כל הרגיסטרים ונמשיך
    pop es
    pop ds
    popa
    add esp, 4                ; ניפטר מה־error code
    iret

7. מה עושה pagefault_resolve?

המטרה: להוסיף Page חדש ל־Page Table, שמכסה את הכתובת שהובילה ל־Page Fault.

שלבים:

  1. נקבל את הכתובת שגרמה לשגיאה (cr2)

  2. נחשב את ה־Directory Index וה־Table Index

  3. נבדוק האם Page Table כבר קיימת (אם לא – ניצור אותה)

  4. ניצור Page חדש עם הרשאות מתאימות

  5. נעדכן את הטבלה

  6. נסיים – הפונקציה חוזרת וה־IRET משחזר את המצב


8. דוגמה לפונקציה pagefault_resolve

pagefault_resolve:
    mov eax, [fault_address]    ; נקבל את הכתובת
    shr eax, 12                 ; ניקח את המספר של הדף (Page Number)
    mov ebx, eax
    and ebx, 0x3FF              ; Table Index (10 ביט)
    shr eax, 10
    and eax, 0x3FF              ; Directory Index (10 ביט)

    ; מצא את כתובת הטבלה
    mov ecx, [cr3]              ; cr3 = כתובת page directory
    mov edx, [ecx + eax*4]      ; טבלת page table

    ; אם הטבלה לא קיימת – ניצור אחת חדשה
    test edx, 1                 ; Present?
    jnz .has_table

    ; אין טבלה? ניצור אחת
    call allocate_frame         ; תחזיר כתובת ל־4KB frame חדש
    or eax, 0x7                 ; Present, RW, User
    mov [ecx + eax*4], eax      ; נשים בטבלה

    call flush_tlb              ; ריענון טבלת page (נדבר בהמשך)
    mov edx, eax

.has_table:
    and edx, 0xFFFFF000         ; ננקה את הביטים הנמוכים (רק הכתובת)

    ; עכשיו ניצור Page חדש
    call allocate_frame
    or eax, 0x7                 ; Present, RW, User
    mov [edx + ebx*4], eax      ; נכניס לטבלת page table

    ret

9. שימושים מתקדמים

ברגע שיש לנו Page Fault:

  • נוכל להקצות זיכרון On Demand – רק כשצריך!

  • נוכל לממש malloc פשוט ביוזר מוד

  • נוכל לממש mmap – טעינה של קובץ לזיכרון לפי דרישה

  • נוכל לאכוף הרשאות זיכרון – דפים לקריאה בלבד / קרנל בלבד


שאלות למחשבה

מה יקרה אם הקרנל עצמו יקבל Page Fault?

אם הקרנל מקבל Page Fault – זה בדרך כלל באג חמור מאוד.
בניגוד ליוזר מוד, שם אנחנו מצפים ל־page faults כחלק מניהול זיכרון רגיל, הקרנל אמור לגשת רק לדפים שהוא הגדיר מראש.

אם בכל זאת מתקבל Page Fault בתוך הקרנל (למשל גישה ל־NULL, או גישה לדף שלא הוקצה), יש שתי אפשרויות:

  • הקרנל מקריס את עצמו (kernel panic) ומפסיק את כל המערכת.

  • או, אם יש לו handler מתוחכם – הוא יכול לנסות לשחזר את המצב (בדרך כלל בלינוקס זה ייגמר בקריסת תהליך או reboot).

האם אפשר לשתף דפים בין תוכנות שונות?

כן! וזה אפילו כלי חשוב במערכות הפעלה מתקדמות.

מערכת ההפעלה יכולה להגדיר דף מסוים כך שהוא יופיע בטבלאות של כמה תוכנות שונות – ואז הן כולן רואות את אותו זיכרון.

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

כמובן, צריך לשים לב להרשאות (קריאה בלבד, כתיבה וכו') כדי למנוע בעיות אבטחה.