- זיכרון וירטואלי (Virtual Memory): שכבת הפשטה שנותנת לכל תהליך אשליה של מרחב זיכרון רציף, פרטי וגדול מהזיכרון הפיזי הקיים בפועל.
- טבלת דפים (Page Table): מבנה נתונים למיפוי מספרי דפים וירטואליים (VPN) למספר מסגרות פיזיות (PFN).
- חריגת דף (Page Fault): פסיקה (Exception) המיוצרת על ידי ה-MMU כאשר תהליך מנסה לגשת לדף שאינו נמצא בזיכרון הפיזי (הביט
Presentכבוי) או בגישה ללא הרשאות. - TLB (Translation Lookaside Buffer): מטמון חומרה קטן ומהיר במעבד השומר את תרגומי הכתובות (VA ל-PA) האחרונים כדי לחסוך גישות איטיות לטבלת הדפים.
- מטמון הדפים (Page Cache): אזור בזיכרון הפיזי (DRAM) המנוהל על ידי מערכת ההפעלה, ושומר עותקים של דפים מהדיסק כדי להאיץ גישות חוזרות בזכות עקרון המקומיות.
- Copy-On-Write (COW): מנגנון עצל (Lazy) לשיתוף דפי זיכרון (למשל לאחר
fork). הדפים משותפים ומוגנים לקריאה בלבד, ורק בניסיון כתיבה ראשון הדף משוכפל לעותק פרטי. - Demand Paging: מנגנון להקצאת זיכרון פיזי "לפי דרישה". דפים מוקצים או נטענים מהדיסק רק ברגע הגישה הראשון אליהם בפועל (מעורר Page Fault).
ככל שיורדים במדרג הזיכרון, הזיכרון הופך לגדול, איטי וזול יותר.
- Cache (L1, L2, L3): זמני גישה בסדר גודל של 1 עד 20 ננו-שניות.
- זיכרון פיזי (DRAM): רוחב פס של כ-25.6 GB/s ושיהוי סביב ה-100 ננו-שניות. בגישה אקראית הוא מהיר פי 25,000 מ-HDD ופי 50 מ-NVMe. הזיכרון הוא נדיף (Volatile).
- אחסון קבוע (HDD / SSD): צורך עשרות מיקרו-שניות (SSD) עד מספר מילי-שניות (HDD). צוואר הבקבוק המרכזי ב-HDD הוא זמן הזזת הראש הקורא (Seek latency).
כדי לגשר על פער הביצועים העצום בין ה-DRAM לדיסק, מערכת ההפעלה מקצה מסגרות ב-DRAM כ"מטמון דפים". כל קריאה או כתיבה לדיסק עוברת דרכו, בהתבסס על עקרון המקומיות (Locality) במרחב ובזמן.
- כתיבה מושהית (Write-back): כתיבות לדיסק נעשות ל-Page Cache ונדחות כדי לחסוך כתיבות ביניים רבות. היתרון הוא שיפור ביצועים משמעותי, החיסרון הוא סכנת אובדן מידע בנפילת מתח.
- קריאה מראש (Read-ahead): אם מתגלית קריאה סדרתית מקובץ, הגרעין יקרא מסגרות נוספות ל-Page Cache עוד לפני שהתהליך מבקש אותן, כדי למנוע החטאות עתידיות.
אופני גישה למטמון הדפים:
read/write: הבאת המידע מהדיסק ל-Page Cache והעתקתו אל (או מ)חוצצים (Buffers) במרחב המשתמש.mmap: מיפוי ישיר של הקובץ למרחב הכתובות של התהליך (Zero copy I/O). גישה דחיינית - חריגת דף (Page Fault) היא זו שתביא את המידע מהדיסק. היתרון הוא חיסכון בהעתקה כפולה, חיסכון בזיכרון וממשק נוח. החיסרון הוא דרישה למרחב כתובות רציף, והקמת המיפוי עולה יותר מקריאה קטנה של מספר בתים בודדים. (דפים ממופים יכולים להיות כ-MAP_PRIVATEעם הגנת COW, או כ-MAP_SHAREDהמשותפים לאותם קבצים).
המניעים המרכזיים לשימוש בזיכרון וירטואלי:
- אשליה (Illusion): מתן מרחב זיכרון רציף וענק לכל תוכנית.
- בידוד (Isolation) ובקרת גישה: תהליך לא יכול לגשת לזיכרון של תהליך אחר.
- צמיחה דינמית (Dynamic Growth): הקצאה לפי דרישה (On-demand).
- Memory Overcommitment: אפשרות לנהל יותר זיכרון ממה שקיים בפועל באמצעות Swapping.
הזיכרון מחולק לבלוקים בגודל קבוע של לרוב 4KB. במרחב הווירטואלי הם נקראים דפים (Pages), ובמרחב הפיזי הם נקראים מסגרות (Frames). טבלת הדפים (Page Table) ממפה בין ה-Virtual Address (VA) ל-Physical Address (PA).
כאשר חומרה ניגשת ל-PTE והביט Present שווה 0, מתעוררת פסיקה ומערכת ההפעלה מתערבת (תחת פונקציית הגרעין do_page_fault). כדי שהקרנל ידע כיצד לטפל בתקלה, החומרה מספקת לו מידע קריטי:
- רגיסטר CR2: שומר את הכתובת הווירטואלית הספציפית שגרמה לתקלה.
- קוד שגיאה (Error Code): המעבד דוחף למחסנית קוד בן 3 ביטים:
- ביט 0 (Present): האם התקלה קרתה כי הדף חסר בזיכרון (0), או כי הוא בזיכרון אבל הייתה הפרת הרשאות (1)?
- ביט 1 (R/W): האם הגישה הייתה לקריאה/ביצוע קוד (0) או לכתיבה (1)? (קריטי כדי לזהות COW).
- ביט 2 (U/S): האם המעבד היה ב-User Mode או ב-Kernel Mode בזמן התקלה?
סיווג סוגי החריגות הראשיים:
- Major Page Fault: הדף לא בזיכרון ונמצא על הדיסק (דף Swap או קובץ Mmap). הגישה חוסמת: מערכת ההפעלה טוענת את הדף ל-DRAM (פעולת I/O איטית), מעדכנת טבלאות ומחדשת את התהליך.
- Minor Page Fault: דף שחסר בטבלה אך השלמתו לא דורשת גישה לדיסק, למשל הקצאת מסגרת אנונימית חדשה ב-Demand Paging, או שכפול מהיר ב-COW. הבעיה נפתרת בזיכרון באופן מיידי.
- אם הגישה לא חוקית (ניסיון כתיבה לקוד קריאה בלבד, או גישה לכתובת מחוץ לאזור הזיכרון המוקצה) - מערכת ההפעלה תשלח סיגנל
SIGSEGVוהתהליך יקרוס. לחילופין, תקלת גישה מקוד הגרעין תייצר "Kernel Oops".
💡 מעקב יזום אחר גישות לזיכרון: המעבד לא מודיע למערכת ההפעלה על גישות חוקיות לזיכרון. אם ה-OS רוצה לעקוב אחרי כל גישה לזיכרון (למשל כדי לממש מנגנון Prefetching חכם), הדרך היחידה היא לאלץ Page Fault על ידי כיבוי מכוון של ביט ה-
Present=0בטבלת הדפים (גם עבור דפים שנמצאים בזיכרון), תוך שימוש בדגל תוכנה אחר כדי לזכור שהדף בפועל ב-RAM.
- Swap Space: שטח מיוחד בדיסק (מחולק למגירות Slots של 4KB) שאליו מפונים דפים אנונימיים כשנגמר ה-DRAM.
- Working Set: אוסף הדפים שהתהליך עובד עליהם באופן פעיל כרגע.
- Thrashing: מצב קריסת ביצועים שקורה כשה-Working set הכללי של המערכת גדול מגודל הזיכרון הפיזי. המערכת עסוקה כל הזמן רק בהחלפת דפים מול הדיסק (Paging / Swapping הלוך ושוב) וזמן ה-CPU מתבזבז על ניהול במקום על עבודה אמיתית.
-
מיפוי היררכי (Radix Tree): במקום טבלה ליניארית עצומה שתדרוש גיגות של זיכרון למערך רציף, הטבלאות מתפצלות לרמות שמוקצות לפי דרישה.
- ב-32 ביט: 2 שלבי מיפוי מפוצלים ל-10-10-12 (DIR, TABLE, OFFSET).
- ב-64 ביט (x86_64): 4 שלבי מיפוי מפוצלים ל-9-9-9-9-12 (PML4
$\to$ PDP$\to$ DIR$\to$ TABLE). בפועל משתמשים ב-48 ביט מתוך מרחב ה-64.
- רגיסטר CR3: שומר את הכתובת הפיזית של בסיס הטבלה היררכית העליונה (השורש) עבור התהליך הנוכחי.
⚠️ TLB Miss אינו פסיקה (Interrupt/Fault)! בארכיטקטורת x86, כאשר יש החטאה ב-TLB, ה-MMU (חומרה) מבצע את ההליכה בטבלת הדפים (Table Walk) בצורה עצמאית. אם הוא מוצא את הדף (Present=1והרשאות תקינות), הוא פשוט טוען את התרגום ל-TLB והפקודה ממשיכה. מערכת ההפעלה כלל לא מעורבת ולא יודעת על זה. חריגת דף (Page Fault) מתרחשת רק אם ה-MMU מגלה במהלך ה-Table Walk שהדף חסר (Present=0) או שיש הפרת הרשאות. רק אז נוצרת פסיקה והשליטה עוברת למערכת ההפעלה.
- PCID (Process Context ID): מנגנון חומרתי ב-TLB שמונע את הצורך לרוקן (Flush) את כל ה-TLB בזמן החלפת הקשר (Context Switch), כיוון שכל כניסה במטמון מתויגת עם ה-ID הייחודי של התהליך.
- רשומת PTE (Page Table Entry): שומרת דגלים קריטיים:
Present- האם הדף במרחב הפיזי?R/W- הרשאות קריאה וכתיבה.U/S(User/Supervisor) - מאפשר למפות את זיכרון הקרנל לכל תהליך בבטחה, וכך מתבצע חיסכון יקר בהחלפות טבלה בעת System Calls.Accessedו-Dirty- ביטים המעודכנים ישירות על ידי החומרה במהלך Table Walk לעזרת מערכת ההפעלה באלגוריתמי פינוי דפים.
- Superpages (Hugepages): שימוש בדפים של 2MB או 1GB (במקום 4KB). היתרון הוא הגדלה דרמטית של כמות הזיכרון המכוסה על ידי ה-TLB (המכונה TLB Coverage), מאחר וגודלו הפיזי של ה-TLB במעבד מוגבל. עם זאת, לדפים גדולים יש חסרונות משמעותיים:
- פרגמנטציה פנימית (Internal Fragmentation): תהליך עשוי לבקש הקצאה קטנה, אך יוקצה לו דף שלם של 2MB, מה שיבזבז המון זיכרון יקר.
- תקורת I/O (Latency): במקרה של Page Fault, טעינת דף של 2MB מהדיסק לזיכרון הפיזי לוקחת זמן רב הרבה יותר מטעינת 4KB, מה שיתקע את התהליך לזמן ארוך.
- קושי בהקצאה: המערכת דורשת רצף של 2MB בזיכרון הפיזי. ככל שהמערכת עובדת ונוצרת פרגמנטציה חיצונית ברמת המסגרות, קשה יותר למצוא מקום פנוי רציף כזה.
בניגוד לעץ ההיררכי של x86, ארכיטקטורת PPC פועלת בצורה שונה:
- משתמשת בטבלת ערבול גלובלית (Hashed Page Table - HTAB).
- למה טבלה גלובלית אחת לכולם? מכיוון שהמרחב הווירטואלי הוא גלובלי ומשותף, כך שכל דף במערכת מקבל כתובת חד-ערכית. לכן אין כפילויות של דפים משותפים, ואפשר להגדיר את טבלת הערבול בגודל קבוע מראש שתלוי אך ורק בגודל הזיכרון הפיזי, ללא צורך בהרחבות דינמיות.
- מסלול התרגום המדויק: הכתובת האפקטיבית (Effective Address - 64 bit) מחולקת למזהה מקטע (Segment) וקיזוז. מזהה המקטע מתורגם דרך אוגר SLB למזהה וירטואלי גלובלי. יחד עם הקיזוז נוצרת הכתובת הווירטואלית הגלובלית (Virtual Address - 80 bit). היא זו שמחופשת ב-TLB או עוברת ערבול ל-HTAB כדי למצוא את הכתובת הפיזית (Real Address).
-
חישוב גודל ה-HTAB: כיוון שהטבלה מייצגת את הזיכרון הפיזי, גודלה נגזר מגודל ה-DRAM ומפקטור העומס (Load Factor - f) שאנו רוצים לאפשר. אם במחשב יש
$K$ מסגרות זיכרון פיזיות, ונרצה פקטור עומס של$f$ (למשל 0.5 כדי להקטין התנגשויות), ה-HTAB יוקצה מראש עם$\frac{K}{f}$ כניסות. -
אופן החיפוש: החומרה מחשבת באופן מקבילי שתי פונקציות ערבול (
hash1,hash2) כדי למצוא קבוצת כניסות (PTEG) הכוללת 8 PTEs, מה שפותר התנגשויות (Collisions) בחומרה במקביל. לטובת קיצור התהליך קיים גם מטמון מהיר הנקרא ERAT. - היתרון המובהק: ה-TLB וה-HTAB משותפים לכל התהליכים במחשב. אין צורך לרוקן את ה-TLB בעת מעבר תהליך.
מבנה מרחב הזיכרון בתהליך (x86_64, 48-bit): מרחב של
- 128TB עליונים - מרחב הגרעין (Kernel Space): ממופה במדויק לאותו אזור כתובות בכל התהליכים. לעולם אינו מפונה לדיסק (Never Swapped). חוטי גרעין (Kernel Threads כדוגמת
kswapd,ksoftirqd) רצים אך ורק שם. לחוטי גרעין אין מרחב משתמש כלל, לכן הגרעין פשוט רץ במרחב התהליך שהושעה לפניו מבלי לשנות את רגיסטר CR3 לחלוטין (חוסך Context Switch יקר). - 128TB תחתונים - מרחב המשתמש (User Space): מכיל את הקוד, המחסנית, הערימה ואזורי ה-Mmap.
כחלק מכל תהליך (בתוך מבנה ה-PCB / task_struct), הגרעין מנהל אובייקטים:
mm_struct(מתאר מרחב הזיכרון):pgd: הכתובת הפיזית של שורש טבלת הדפים (מוטען ל-CR3).mmap: רשימה מקושרת של כל אזורי הזיכרון של התהליך.mm_users: מונה כמה תהליכי משתמש חולקים את המרחב (כמו חוטים של התהליך). כאשר המונה מגיע ל-0, רק אזורי הזיכרון (VMA) מפונים.mm_count: מונה גם את חוטי הגרעין (Kernel Threads) ש"רוכבים" על המרחב הזה (לחוטי גרעין השדה הקבועmm = NULL). שורש טבלת הדפים (PGD) ומבנה ה-mm_structעצמו ישוחררו רק כאשר גם ה-mm_countיגיע ל-0.
vm_area_struct(מתאר אזור זיכרון - VMA):- מייצג רצף כתובות ייעודי אחד, כגון מחסנית או ערימה (
vm_startעדvm_end). - מחזיק את כל הרשאות החוקיות לאזור (
VM_READ,VM_WRITE,VM_EXEC,VM_SHARED). - הקצאות זיכרון על ידי פונקציית
mallocמובילות לקריאת המערכתbrkאוsbrkלהגדלת גבולות ה-VMA של אזור הערימה, או לקריאתmmapליצירת אזורי VMA אנונימיים חדשים.
- מייצג רצף כתובות ייעודי אחד, כגון מחסנית או ערימה (
כל מסגרת פיזית במחשב מיוצגת במערך גלובלי (Page Frame Array). לכל מסגרת יש שדות לניהול:
-
refcount- מונה תהליכים המצביעים אליה. אם המונה 0 הזיכרון פנוי. -
mappingו-index- שדות המרכיבים את המפתח שמחפשים ב-Page Hash Table כדי למצוא מסגרות ב-Page Cache בסיבוכיות של$O(1)$ . - Reverse Mapping (מיפוי הפוך): לינוקס מנהלת מבנה נתונים מיוחד שממפה כל מסגרת פיזית חזרה לכל ה-PTEs של התהליכים השונים שמצביעים עליה. זה חיוני עבור ה-PFRA כדי שיוכל לנתק מסגרת משותפת מכולם בבת אחת!
לינוקס דוגלת בניהול "עצל" ודחייני (Lazy).
קריאת המערכת fork דורשת תיאורטית העתקה של כל מרחב זיכרון האב. העתקה זו לרוב מיותרת (כי התהליך מיד יקרא ל-execv לטעינת תוכנית חדשה).
הפתרון העצל:
- הגנה:
forkמגדילה את מונה המסגרות (refcount) ומשתפת אותן בין האב לבן. אך עבור דפי נתונים שניתנים לכתיבה (כמו ערימה ומחסנית), מערכת ההפעלה מכבה במכוון את הרשאת ה-R/W=0ב-PTE של שניהם! - טיפול (Page Fault): כשאחד מהם ינסה לכתוב, תיווצר חריגת דף בגלל הפרת הרשאות.
- הגרעין יראה בפסיקה שדגל ה-
VM_WRITEדלוק ב-VMA ולכן זו כתיבה חוקית בגיבוי COW. הוא יעתיק מיידית את מסגרת הזיכרון למסגרת פיזית חדשה ופרטית, יעדכן את הטבלאות, ידליק לה הרשאתR/W=1, והתוכנה תוכל להמשיך להוראת הכתיבה במעבד (Restartable instruction) מבלי להרגיש דבר.
גם כשמשתמש מריץ malloc או mmap, לא מוקצה טיפת זיכרון פיזי אמיתי.
- הגרעין רק מעדכן את מתאר אזור הזיכרון (
vm_area_struct), והכניסות בטבלת הדפים מסומנות כ-Present=0. - רק כשקורית הגישה הממשית הראשונה, נוצרת פסיקת Minor Page Fault. מערכת ההפעלה תמפה באותו הרגע דף אפסים מיוחד (Zero Page), תעדכן את הטבלאות ותאפשר כתיבה.
לינוקס מאפשרת ל-Page Cache לגדול על חשבון ה-DRAM הפנוי מבלי חסם עליון. אך ברגע שכמות המסגרות הפנויות צונחת, הגרעין מפעיל פינוי חכם של מסגרות כדי לשרוד.
-
ספי פינוי מפלסי המים (Watermarks) ואסינכרוני מול סינכרוני:
- High Watermark: מעל סף זה, תהליך הרקע
kswapdהולך לישון ומפסיק לפנות דפים. - Low Watermark: כשהמערכת יורדת מתחת לסף זה, מתעורר ה-
kswapdברקע לפינוי דפים באופן אסינכרוני. - Min Watermark (מצב קריטי): אם הגענו לסף זה המערכת במצוקה. ההקצאות הרגילות נחסמות, ומבוצע פינוי סינכרוני וישיר (Direct Reclaim) על ידי התהליכים המבקשים.
- High Watermark: מעל סף זה, תהליך הרקע
-
אלו מסגרות מפונות ובאיזה סדר עדיפויות?
- מסגרות לא מוצבעות (Unmapped) יפונו תמיד לפני מסגרות מוצבעות (שכן פינוי מסגרת מוצבעת דורש גישה לטבלת הדפים, עדכון ה-PTE וביצוע מחיקה יקרה ב-TLB).
- מסגרות נקיות (Clean) יפונו לפני מלוכלכות (Dirty), כדי לחסוך עבודת I/O איטית וממושכת לדיסק לפני הפינוי. דפי קרנל לא יפונו לעולם.
-
אלגוריתם בחירת הקורבן המיטבי (PFRA): שימוש ב-LRU מדויק הוא יקר מדי. לכן המערכת מתפשרת על אלגוריתמים דמויי-שעון כמו NRU (Not Recently Used). היא מחלקת את הדפים ל-4 מחלקות על בסיס שני ביטי חומרה -
Referenced(בשימוש) ו-Dirty(מלוכלך). המערכת תמיד תחפש את הקורבן מהמחלקה הנמוכה ביותר האפשרית:- מחלקה 0 (R=0, D=0): לא בשימוש ונקי. הקורבן המושלם. הפינוי מיידי (ללא כתיבה לדיסק).
- מחלקה 1 (R=0, D=1): לא בשימוש אך מלוכלך. עדיף לפנות אותו על פני דף שנמצא כעת בשימוש, אך פינויו מצריך כתיבה מקדימה איטית לדיסק.
- מחלקה 2 (R=1, D=0): בשימוש ונקי.
- מחלקה 3 (R=1, D=1): בשימוש ומלוכלך. הדף האחרון שנרצה לפנות.
💡 הערת יישום (Zeroing round): כדי שדפים לא יישארו לנצח במחלקות הגבוהות ויסומנו כ"בשימוש", מערכת ההפעלה מבצעת סריקה מחזורית ומאפסת באופן יזום את ביט ה-
Referencedשל הדפים. -
מימוש מעשי בלינוקס: לינוקס מממשת תור סריקה כפול (Active list מול Inactive list) ומשתמשת בשני דגלי תוכנה -
PG_activeו-PG_referenced- כדי לתת לכל מסגרת "רמת פעילות" מספרית בין 0 ל-3 ולדמות את השעון. דפים בשימוש מקודמים ברמת הפעילות, בעוד אלו שלא דועכים מטה ונבחרים לפינוי מראש רשימת ה-Inactive.
מערכת הפעלה היא יישום תגובתי (Reactive) הממתין לאירועים להחזרת השליטה אליו ממרחב המשתמש.
| סוג פסיקה | תיאור כללי | דוגמאות בולטות |
|---|---|---|
| פסיקות אסינכרוניות (External) | פסיקות ממקור חיצוני שאינן תלויות בשורת הקוד המתבצעת במעבד כרגע. | מידע שמגיע מכרטיס רשת, לחיצות מקלדת/עכבר, ופסיקת שעון מחזורית שקוטעת ומעירה את מתזמן ה-OS. |
| פסיקות סינכרוניות (Internal / Exceptions) | נובעות ישירות מפעולת הקוד שמתבצע במעבד. | מפורשות: syscall לבקשת שירות מהקרנל, int3 לדיבאגר. משתמעות / שגיאות: חלוקה לא חוקית באפס, חריגת דף (Page fault). |
כאשר פסיקה מתקבלת, פונקציית הטיפול (Interrupt Handler) מזנקת. בזמן שגרה המטפלת בפסיקות חומרה חיצוניות, המעבד "אטום" ולא יכול לקבל פסיקות אחרות, דבר שעלול לתקוע את המערכת. כדי להימנע מזה, עבודת הטיפול מפוצלת לשניים:
- Top Half (FLIH - First Level): העבודה המינימלית, המיידית והקריטית ביותר לביצוע, שלא ניתן לדחות (למשל: העתקת המידע מחוצץ כרטיס הרשת לזיכרון והכנת חוצץ לקליטה הבאה). בזמן זה הפסיקות ממוסכות (Masked).
- Bottom Half (SLIH - Second Level): כל שאר העבודה ה"כבדה" והארוכה, שתדחה למועד מאוחר יותר בניהול ה-Scheduler (לדוגמה: פענוח ומיון הפקטה הווירטואלית בשכבות ה-TCP/IP ותקשור עם האפליקציה). זה מבוצע בזמן שהמערכת מאפשרת פסיקות באופן חופשי.
⚠️ סערת פסיקות (Interrupt Storm), דיגום (Polling) ו-DMA תעבורה כבדה של קלט/פלט (כמו כרטיסי רשת מהירים במיוחד) עשויה להציף את ה-OS ברבבות פסיקות בשנייה, מה שמבזבז את כל ה-CPU רק על מעברי הקשר. מנגנון ה-NAPI ודיגום קשוב (Polling): בלינוקס קיים מנגנון בשם NAPI (New API). כשהרשת עמוסה מאוד, המנגנון מחליף באופן דינמי את עבודת כרטיס הרשת למצב Polling המנחה אותו לחדול משליחת פסיקות. תחת זאת, הגרעין סורק בעצמו את החוצצים כשיש לו פנאי. זה נותן למעבד זמן לטפל ב-Bottom Halves בלי להיחנק. כשהעומס יורד, המערכת חוזרת למצב פסיקות רגיל. Direct Memory Access (DMA): חומרה (כמו דיסק או כרטיס רשת מתקדם) רשאית לכתוב המוני נתונים מהחוצצים הפנימיים שלה ישר אל תוך ה-RAM ללא מעורבות מלאה של המעבד במעבר על כל בית. רק בסיום כל העברת מסת המידע כולה – ההתקן יעורר פסיקה בודדת ל-OS כדי לדווח שהעבודה הושלמה.