לדלג לתוכן

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. היא מקצה בלוק רציף של זכרון - רציף גם וירטואלית וגם פיזית.

#include <linux/slab.h>

void *kmalloc(size_t size, gfp_t flags);
void kfree(const void *ptr);

דוגמה פשוטה:

void *ptr = kmalloc(1024, GFP_KERNEL);
if (!ptr)
    return -ENOMEM;

// שימוש בזכרון...

kfree(ptr);

שימו לב לכמה הבדלים מmalloc:

  1. אין צורך לבדוק errno - kmalloc מחזיר NULL אם ההקצאה נכשלה, ואנחנו מחזירים -ENOMEM (קוד שגיאה של "אין זכרון").

  2. יש פרמטר flags - הדגל השני אומר לקרנל איך להתנהג בזמן ההקצאה. זה קריטי.

  3. הזכרון רציף פיזית - בניגוד ל-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 מקצה זכרון שרציף וירטואלית אבל לא בהכרח פיזית.

#include <linux/vmalloc.h>

void *vmalloc(unsigned long size);
void vfree(const void *addr);

דוגמה:

void *buf = vmalloc(1024 * 1024);  // 1MB
if (!buf)
    return -ENOMEM;

// שימוש ב-buffer...

vfree(buf);

ההבדלים מ-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 הישן יותר).

איך זה עובד

  1. יצירת 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
);
  1. הקצאה - כשצריכים אובייקט חדש:
struct task_struct *task = kmem_cache_alloc(task_struct_cache, GFP_KERNEL);
if (!task)
    return -ENOMEM;
  1. שחרור - כשהאובייקט כבר לא נחוץ:
kmem_cache_free(task_struct_cache, task);

האובייקט לא באמת "משוחרר" - הוא חוזר ל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 ואת מצבם:

cat /proc/slabinfo

הפלט מראה כל cache - השם שלו, כמה אובייקטים פעילים, כמה מוקצים, גודל כל אובייקט, ומספר הדפים.

אפשר גם להשתמש בכלי slabtop שמציג את ה-slab caches בסדר יורד לפי שימוש בזכרון:

sudo slabtop

דוגמה לפלט (מקוצר):

 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 מכיל מידע מפורט על שימוש בזכרון:

cat /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.