6.6 הקצאת זכרון בקרנל הרצאה
הקדמה¶
ב-user space יש לנו את malloc ו-free. קוראים ל-malloc, מקבלים פוינטר לזכרון, משתמשים בו, ובסוף קוראים ל-free. פשוט. אבל מה הקרנל עצמו עושה כשהוא צריך זכרון?
הקרנל לא יכול להשתמש ב-malloc. למה? כי malloc מסתמך על syscalls כמו mmap ו-brk - שירותים שהקרנל עצמו מספק. הקרנל לא יכול לקרוא לעצמו. הוא צריך מנגנונים ברמה נמוכה יותר.
בפרק הזה נלמד על המקצים של הקרנל: kmalloc, vmalloc, ה-slab allocator, ומקצה הדפים. ראינו בפרק 6.4 את ה-buddy allocator שמנהל דפים פיזיים - עכשיו נראה את כל מה שיושב מעליו.
ההיררכיה של הקצאת זכרון¶
לפני שנצלול לפרטים, בואו נראה את התמונה הגדולה. הקצאת זכרון בלינוקס היא שכבתית:
----------------------------------------------------------
| מרחב משתמש (user space) |
| malloc() / free() --> mmap() / brk() syscalls |
----------------------------------------------------------
| קרנל (kernel space) |
| kmalloc() / vmalloc() |
| | |
| מקצה ה-slab (SLUB) |
| | |
| מקצה הדפים - buddy allocator |
| | |
| זכרון פיזי |
----------------------------------------------------------
בuser space, הascall malloc קורא ל-mmap או brk, שהם syscalls. בkernel space, הכל מתחיל מ-kmalloc או vmalloc, שמשתמשים ב-slab allocator, שמשתמש ב-buddy allocator, שמנהל את הזכרון הפיזי ישירות.
kmalloc ו-kfree¶
הפונקציה kmalloc היא המקבילה הקרנלית של malloc. היא מקצה בלוק רציף של זכרון - רציף גם וירטואלית וגם פיזית.
דוגמה פשוטה:
שימו לב לכמה הבדלים מmalloc:
-
אין צורך לבדוק errno - kmalloc מחזיר NULL אם ההקצאה נכשלה, ואנחנו מחזירים -ENOMEM (קוד שגיאה של "אין זכרון").
-
יש פרמטר flags - הדגל השני אומר לקרנל איך להתנהג בזמן ההקצאה. זה קריטי.
-
הזכרון רציף פיזית - בניגוד ל-malloc שמחזיר זכרון רציף וירטואלית בלבד, kmalloc מחזיר זכרון שרציף גם פיזית. זה חשוב עבור חומרה שצריכה לגשת לזכרון ישירות (DMA).
דגלי GFP - GFP flags¶
הדגלים (GFP = Get Free Pages) הם אחד הדברים הכי חשובים בהקצאת זכרון בקרנל. הם אומרים לקרנל באילו תנאים הוא רשאי להקצות:
GFP_KERNEL - הדגל הנפוץ ביותר. אומר: "אני בהקשר של תהליך (process context) ואפשר לישון". אם אין זכרון פנוי, הקרנל יכול:
- להעביר דפים ל-swap
- לכתוב dirty pages לדיסק
- להמתין עד שיתפנה זכרון
משמשים אותו כשקוד רץ בהקשר של syscall, למשל.
GFP_ATOMIC - אומר: "אי אפשר לישון! תן לי זכרון עכשיו או אל תיתן בכלל". משמש בהקשרים שבהם אסור לישון:
- בתוך interrupt handlers
- בתוך spinlocks
- בתוך softirqs ו-tasklets
אם אין זכרון פנוי מיד, kmalloc עם GFP_ATOMIC פשוט מחזיר NULL. הוא לא מחכה.
GFP_DMA - מבקש זכרון מתוך ZONE_DMA (16MB הראשונים), עבור התקני DMA ישנים.
GFP_DMA32 - מבקש זכרון מתוך ZONE_DMA32 (עד 4GB), עבור התקנים שמוגבלים ל-32 ביט.
למה ההבדל בין GFP_KERNEL ל-GFP_ATOMIC כל כך חשוב? תחשבו על זה: interrupt handler קוטע את הCPU באמצע משהו. אם הinterrupt handler ינסה לישון (כי ממתין לIO של דיסק כדי לפנות זכרון) - הCPU ייתקע. אף אחד לא יעיר אותו כי הinterrupt עדיין לא טופל. לכן בinterrupt handler חובה להשתמש ב-GFP_ATOMIC.
מגבלות של kmalloc¶
ל-kmalloc יש מגבלה על גודל ההקצאה - בדרך כלל עד 4MB (תלוי בconfiguration). זה בגלל שהזכרון חייב להיות רציף פיזית, וככל שמבקשים בלוק גדול יותר, קשה יותר למצוא בלוק רציף. אם צריכים הקצאה גדולה יותר - משתמשים ב-vmalloc.
vmalloc ו-vfree¶
הפונקציה vmalloc מקצה זכרון שרציף וירטואלית אבל לא בהכרח פיזית.
דוגמה:
ההבדלים מ-kmalloc¶
| תכונה | kmalloc | vmalloc |
|---|---|---|
| רציפות פיזית | כן | לא |
| מהירות | מהיר | איטי יותר |
| גודל מקסימלי | מוגבל (בד"כ 4MB) | גדול (מוגבל רק ע"י זכרון פנוי) |
| שימוש ב-page tables | לא צריך (הזכרון כבר ממופה) | כן (צריך ליצור מיפויים) |
| מתאים ל-DMA | כן | לא |
למה vmalloc איטי יותר? כי הוא צריך:
1. להקצות דפים בודדים (שלא בהכרח רציפים פיזית).
2. ליצור ערכים חדשים בpage table של הקרנל כדי למפות את הדפים הלא-רציפים לכתובות וירטואליות רציפות.
מתי משתמשים ב-vmalloc¶
- כשצריכים buffer גדול ולא אכפת לנו מרציפות פיזית.
- טעינת מודולים של הקרנל (kernel modules) - הקוד של מודול נטען לזכרון שהוקצה עם vmalloc.
- יצירת buffers גדולים עבור מערכות קבצים או רשת.
מקצה ה-slab - SLUB¶
הבעיה¶
הקרנל יוצר ומשמיד מיליוני אובייקטים קטנים בכל שנייה. task_struct כשנוצר תהליך חדש. inode כשפותחים קובץ. dentry כשפותרים נתיב. sk_buff כשמגיע פקט רשת.
כל האובייקטים האלה הם בגודל קבוע. task_struct תמיד באותו גודל. inode תמיד באותו גודל. להשתמש ב-buddy allocator ישירות עבור כל אובייקט כזה זה בזבוז - buddy allocator עובד ביחידות של דפים (4KB), ו-inode הוא הרבה יותר קטן.
הפתרון - מטמוני slab¶
הרעיון: ליצור pool (בריכה) מוכנה מראש של אובייקטים מאותו סוג. במקום להקצות ולשחרר כל פעם מה-buddy allocator, שומרים אובייקטים משומשים בcache ומחזרים אותם.
בלינוקס מודרני, המימוש של slab נקרא SLUB (שהחליף את SLAB הישן יותר).
איך זה עובד¶
- יצירת cache - הקרנל יוצר slab cache לכל סוג אובייקט:
struct kmem_cache *task_struct_cache;
task_struct_cache = kmem_cache_create(
"task_struct", // שם (מופיע ב-/proc/slabinfo)
sizeof(struct task_struct), // גודל אובייקט
0, // alignment
SLAB_PANIC, // דגלים
NULL // constructor
);
- הקצאה - כשצריכים אובייקט חדש:
struct task_struct *task = kmem_cache_alloc(task_struct_cache, GFP_KERNEL);
if (!task)
return -ENOMEM;
- שחרור - כשהאובייקט כבר לא נחוץ:
האובייקט לא באמת "משוחרר" - הוא חוזר לcache ויהיה זמין להקצאה הבאה.
מה קורה מאחורי הקלעים¶
ה-slab cache מבקש דפים שלמים מה-buddy allocator ומחלק אותם לאובייקטים. למשל, אם כל אובייקט הוא 256 בתים ודף הוא 4096 בתים, אז כל דף מכיל 16 אובייקטים.
כשכל האובייקטים בcache תפוסים ומבקשים עוד, ה-slab מבקש עוד דפים מה-buddy allocator ומחלק גם אותם.
כש-kmalloc צריך להקצות 256 בתים, הוא לא הולך ישירות ל-buddy allocator. הוא הולך ל-slab cache שמוכן מראש עבור הקצאות בגודל 256 בתים. יש slab caches ייעודיים לגדלים 8, 16, 32, 64, 128, 256, 512, 1024, וכו'.
צפייה ב-slab caches¶
אפשר לראות את כל ה-slab caches ואת מצבם:
הפלט מראה כל cache - השם שלו, כמה אובייקטים פעילים, כמה מוקצים, גודל כל אובייקט, ומספר הדפים.
אפשר גם להשתמש בכלי slabtop שמציג את ה-slab caches בסדר יורד לפי שימוש בזכרון:
דוגמה לפלט (מקוצר):
Active / Total Objects (% used) : 1234567 / 1345678 (91.7%)
Active / Total Slabs (% used) : 45678 / 45678 (100.0%)
OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
89456 85432 95% 0.19K 4260 21 17040K dentry
67890 65432 96% 0.62K 2611 26 41776K inode_cache
45678 44567 97% 2.06K 2854 16 91328K task_struct
אפשר לראות שdentry ו-inode_cache (שלמדנו עליהם ב-6.5) הם מה-caches הכי פעילים - הקרנל כל הזמן יוצר ומשמיד dentries ו-inodes כחלק מפתרון נתיבים ופתיחת קבצים.
מקצה הדפים - page allocator¶
בתחתית ההיררכיה יושב מקצה הדפים, שמבוסס על ה-buddy allocator שלמדנו עליו ב-6.4. הפונקציות העיקריות:
// הקצאת דפים
struct page *alloc_pages(gfp_t gfp, unsigned int order);
unsigned long __get_free_pages(gfp_t gfp, unsigned int order);
// שחרור דפים
void __free_pages(struct page *page, unsigned int order);
void free_pages(unsigned long addr, unsigned int order);
הפרמטר order מציין כמה דפים להקצות כחזקה של 2:
- order 0 = דף אחד (4KB)
- order 1 = שני דפים (8KB)
- order 2 = ארבעה דפים (16KB)
- order 10 = 1024 דפים (4MB)
ההבדל בין alloc_pages ל-__get_free_pages:
- הפונקציה alloc_pages מחזירה מצביע ל-struct page (שלמדנו עליו ב-6.4).
- הפונקציה __get_free_pages מחזירה את הכתובת הוירטואלית של הדף.
בפועל, kmalloc ו-slab allocator קוראים לפונקציות האלה מאחורי הקלעים. מתכנתי קרנל משתמשים בהן ישירות רק כשצריכים דפים שלמים.
דיבוג זכרון בקרנל¶
באגים של זכרון (use-after-free, buffer overflow, memory leak) הם מהבאגים הכי מסוכנים. בkernel space הם עוד יותר מסוכנים - באג זכרון בקרנל יכול לגרום לkernel panic (מה שנקרא "מסך כחול" בWindows, אבל בלינוקס זה kernel panic).
KASAN - Kernel Address Sanitizer¶
כלי שמזהה גישה לזכרון לא חוקית בזמן ריצה:
- גישה לזכרון שכבר שוחרר (use-after-free)
- כתיבה מעבר לגבולות ה-buffer (buffer overflow)
- שימוש בזכרון לא מאותחל
הוא מוסיף תקורה משמעותית ולכן משתמשים בו רק בזמן פיתוח ובדיקות, לא ב-production.
kmemleak¶
כלי שמזהה דליפות זכרון בקרנל. הוא עוקב אחרי כל ההקצאות ובודק אם יש הקצאות שאף אחד לא מחזיק מצביע אליהן.
# הפעלה (דורש קרנל שקומפל עם CONFIG_DEBUG_KMEMLEAK)
echo scan > /sys/kernel/debug/kmemleak
cat /sys/kernel/debug/kmemleak
מידע על זכרון ב-procfs¶
הקובץ /proc/meminfo מכיל מידע מפורט על שימוש בזכרון:
שדות רלוונטיים:
- MemTotal - סך כל הRAM
- MemFree - זכרון חופשי (שלא משמש לשום דבר, גם לא cache)
- MemAvailable - זכרון זמין (כולל cache שאפשר לשחרר)
- Buffers - מטמון של block device
- Cached - page cache (מטמון קבצים)
- Slab - סך כל הזכרון שה-slab allocator משתמש בו
- SReclaimable - חלק מה-slab שאפשר לשחרר (בעיקר dcache ו-inode cache)
- SUnreclaim - חלק מה-slab שלא ניתן לשחרור
סיכום¶
למדנו על מנגנוני הקצאת הזכרון בתוך הקרנל:
- kmalloc / kfree - המקבילה של malloc בקרנל. מקצה זכרון רציף פיזית. צריך לציין דגלי GFP (GFP_KERNEL לcontexts רגילים, GFP_ATOMIC כשאסור לישון).
- vmalloc / vfree - מקצה זכרון רציף וירטואלית אבל לא פיזית. מתאים להקצאות גדולות. איטי יותר מ-kmalloc.
- מקצה ה-slab (SLUB) - pools של אובייקטים מאותו גודל. מייעל הקצאה ושחרור של אובייקטים נפוצים (task_struct, inode, dentry). אפשר לראות ב-/proc/slabinfo.
- מקצה הדפים - הרמה הכי נמוכה. alloc_pages מבוססת על ה-buddy allocator שלמדנו ב-6.4.
הכל מתחבר: כשתהליך קורא ל-malloc ב-user space, זה מגיע ל-mmap syscall, שיוצר VMA (6.4), ואז page fault מפעיל את מקצה הדפים, שמשתמש ב-buddy allocator כדי להקצות דף פיזי. כשהקרנל עצמו צריך זכרון (למשל ליצור task_struct עבור תהליך חדש), הוא משתמש ב-slab allocator שמבקש דפים מ-buddy allocator.