לדלג לתוכן

6.4 ניהול זכרון פתרון

פתרונות - ניהול זכרון

פתרון 1 - חקירת buddy allocator

א. מספר ה-zones תלוי במערכת. במערכת 64 ביט טיפוסית תראו שניים עד שלושה: DMA, DMA32, ו-Normal. במערכת 32 ביט ייתכן גם HighMem.

ב. המספר הראשון מייצג כמה בלוקים חופשיים יש בגודל של דף אחד (4KB, order 0). המספר האחרון (עמודה 11) מייצג כמה בלוקים חופשיים יש בגודל של 1024 דפים (4MB, order 10).

ג. אם המספרים בעמודות הגבוהות הם 0, זה אומר שאין בלוקים רציפים גדולים של זכרון פיזי חופשי. הזכרון מפוצל (fragmented). ייתכן שיש הרבה דפים בודדים חופשיים (עמודות נמוכות), אבל הם לא רציפים. זה יגרום לכישלון של הקצאות שדורשות זכרון פיזי רציף (כמו kmalloc גדול).


פתרון 2 - חקירת /proc/meminfo

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

MemTotal:       16384000 kB
MemFree:          512000 kB
MemAvailable:    8192000 kB
Cached:          6400000 kB
SwapTotal:       4096000 kB
SwapFree:        4096000 kB

ב. MemFree מראה רק דפים שלא בשימוש כלל. MemAvailable כולל גם את MemFree וגם זכרון שניתן לשחרר בקלות - בעיקר page cache (קבצים שנקראו ונשמרו בזכרון כcache) וחלק מה-slab cache (SReclaimable). הקרנל יכול לשחרר את ה-cache הזה מיידית אם תהליך צריך זכרון, ולכן הוא נחשב "זמין".

ג. לא, זו לא בעיה. Cached גבוה זה דבר טוב! זה אומר שהקרנל משתמש בRAM הפנוי כדי לשמור קבצים שנקראו, מה שמאיץ גישה חוזרת לקבצים. ברגע שתהליך צריך זכרון, הקרנל פשוט מפנה דפים מה-cache. "RAM פנוי הוא RAM מבוזבז" - יותר טוב להשתמש בו ל-cache מאשר לא לעשות איתו כלום.


פתרון 3 - ניתוח /proc/[pid]/maps

א. הנה הדרך לזהות את האזורים:

  • heap_mem (שהוקצה עם malloc) - malloc להקצאה של 1MB בדרך כלל משתמש ב-mmap (כי זה מעל 128KB). מחפשים אזור אנונימי (ללא שם קובץ) עם הרשאות rw-p שהכתובת שלו מתאימה לפוינטר שהודפס.
  • anon_mem (שהוקצה עם mmap ישירות) - גם אזור אנונימי rw-p. הכתובת מתאימה לפוינטר שהודפס.

שניהם ייראו דומה ב-maps כי שניהם מיפויים אנונימיים. ההבדל הוא שmalloc מנהל את הזכרון דרך libc, ו-mmap ישיר נותן לנו שליטה מלאה.

ב. ה-stack נמצא בכתובות גבוהות (ב-64 ביט מתחיל ב-7ff...) ומסומן כ-[stack]. ההרשאות שלו הן rw-p (קריאה, כתיבה, פרטי). אין הרשאת execute - זה חלק מהגנת NX/DEP שמונעת הרצת קוד מה-stack.

ג. ל-libc.so יש לפחות 3-4 VMAs:
- r--p - אזור read-only (ELF headers ו-metadata)
- r-xp - אזור קוד (read + execute) - הפונקציות של libc
- r--p - נתונים read-only (קבועים ו-relocations)
- rw-p - נתונים read-write (משתנים גלובליים של libc)

ההפרדה נועדה לאבטחה: קוד הוא execute-only (אי אפשר לכתוב עליו), ונתונים הם non-executable (אי אפשר להריץ מהם קוד).


פתרון 4 - צפייה ב-Copy-on-Write

לפני fork - יש רק תהליך אחד. ב-/proc/<parent_pid>/status השדה RssAnon מראה בערך 100MB (ה-buffer שהקצנו).

אחרי fork, לפני כתיבה - עכשיו יש שני תהליכים. אם נבדוק את /proc/<child_pid>/status:
- RssAnon של הבן יהיה קטן מאוד (רק דפי stack ומעט metadata).
- למרות שה-maps של הבן מראה את אותם אזורים כמו האב, הדפים הפיזיים משותפים. הקרנל לא העתיק שום דבר.

אחרי שהבן כותב - הבן שינה בית אחד ב-buffer. זה גורם ל-COW fault על דף אחד (4KB). הקרנל מעתיק את הדף הזה בלבד. RssAnon של הבן יגדל ב-4KB. שאר 99.99MB+ עדיין משותפים.

אפשר לראות את זה גם עם:

cat /proc/<pid>/smaps | grep -A 20 "<address of buffer>"

השדה Shared_Clean או Shared_Dirty מראה כמה זכרון משותף עם תהליכים אחרים.


פתרון 5 - שאלה תיאורטית

הזרימה המלאה עבור גישה ראשונה לכתובת של mmap אנונימי:

  1. הCPU מנסה לגשת לכתובת הוירטואלית - למשל, התהליך כותב ערך לכתובת שחזרה מ-mmap.

  2. הMMU עובר על page table - הוא מחפש את הערך (PTE - Page Table Entry) עבור הכתובת. הוא מוצא שהPTE ריק (present bit = 0) - אין מיפוי.

  3. הMMU מפעיל חריגת page fault (#PF, exception 14). הCPU שומר את ה-RIP (כתובת הפקודה שנכשלה) ואת הכתובת שגרמה לfault (ברגיסטר CR2), ועובר לhandler של הקרנל.

  4. הקרנל מריץ את do_page_fault - הפונקציה מקבלת את הכתובת (מCR2) ואת סיבת הfault.

  5. חיפוש VMA - הקרנל קורא ל-find_vma כדי למצוא את ה-vm_area_struct שמכיל את הכתובת. הוא מחפש בעץ ה-maple של ה-mm_struct.

  6. נמצא VMA - הכתובת בטווח [vm_start, vm_end). ההרשאות מתאימות (VM_WRITE מוגדר כי בקשנו PROT_WRITE ב-mmap).

  7. הקרנל קורא ל-handle_mm_fault - שקובע את סוג ה-fault. במקרה שלנו זה מיפוי אנונימי ללא דף - minor fault.

  8. הקצאת דף פיזי - הקרנל קורא ל-alloc_pages (buddy allocator) כדי להקצות דף פיזי אחד (4KB). הדף מאותחל לאפסים (עבור מיפוי אנונימי).

  9. עדכון page table - הקרנל כותב PTE חדש בטבלת הדפים של התהליך. ה-PTE מכיל את הכתובת הפיזית של הדף החדש, עם ההרשאות המתאימות (read/write).

  10. חזרה לuser space - הקרנל חוזר מהexception handler. הCPU חוזר לפקודה שנכשלה ומריץ אותה שוב. הפעם ה-MMU מוצא PTE תקף, מתרגם את הכתובת, והגישה מצליחה.

ההבדל עבור file-backed mmap: בשלב 8, במקום להקצות דף ריק, הקרנל:
- בודק אם הדף כבר ב-page cache (אולי תהליך אחר כבר קרא את אותו חלק מהקובץ).
- אם כן - ממפה את הדף מה-page cache ישירות (minor fault).
- אם לא - קורא את הדף מהדיסק, שומר אותו ב-page cache, וממפה אותו (major fault). זה הרבה יותר איטי כי כולל I/O לדיסק.