לדלג לתוכן

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

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

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

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

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

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


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

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

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

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

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


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

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

  • heap_mem (שהוקצה עם malloc) - malloc לallocation של 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 - יש רק process אחד. ב-/proc/<parent_pid>/status השדה RssAnon מראה בערך 100MB (ה-buffer שהקצנו).

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

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

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

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

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


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

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

  1. הCPU מנסה לגשת לכתובת הוירטואלית - למשל, הprocess כותב ערך לכתובת שחזרה מ-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. במקרה שלנו זה מיפוי אנונימי ללא page - minor fault.

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

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

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

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