לדלג לתוכן

6.6 הקצאת זכרון בקרנל פתרון

פתרונות - הקצאת זכרון בקרנל

פתרון 1 - חקירת /proc/slabinfo

א. הערכים ישתנו בין מערכות. דוגמה:
- task_struct - בערך 200-500 אובייקטים פעילים (תלוי בכמות התהליכים)
- dentry - עשרות אלפים עד מאות אלפים (תלוי בכמה קבצים נפתחו)
- inode_cache - אלפים עד עשרות אלפים

ב. גדלים טיפוסיים (תלוי בגרסת הקרנל והconfiguration):
- task_struct - בערך 2-6KB (struct גדול מאוד)
- dentry - בערך 192 בתים
- inode_cache - בערך 600-700 בתים

שימו לב ש-task_struct הוא הגדול ביותר - הוא מכיל המון מידע על התהליך.

ג. ברוב המערכות, dentry או inode_cache תופסים הכי הרבה זכרון (בגלל הכמות הגדולה שלהם). במערכות עם הרבה תהליכים, task_struct יכול גם לתפוס הרבה.

כדי לבדוק, מריצים slabtop ומסתכלים על העמודה CACHE SIZE (או מחשבים: num_objs * objsize).


פתרון 2 - ניתוח /proc/buddyinfo

א. צריך להסתכל על השורה של ZONE_NORMAL ובמספר הראשון (עמודת order 0). למשל:

Node 0, zone   Normal   1234   567   234   89   34   12   5   2   1   0   0

כאן יש 1234 דפים בודדים חופשיים.

ב. בלוק רציף של 1MB = 256 דפים. 256 = 2^8, אז זה order 8. צריך להסתכל בעמודה התשיעית (ספירה מ-0). בדוגמה למעלה יש 1 בלוק כזה.

ג. הפונקציה kmalloc(8192, GFP_KERNEL) מבקשת 8192 בתים = 2 דפים = order 1. אבל kmalloc לא הולך ישירות ל-buddy allocator - הוא הולך ל-slab allocator. ה-slab allocator מנהל cache של הקצאות בגודל 8192 (kmalloc-8192), ו-slab allocator הוא זה שמבקש דפים מה-buddy allocator כשצריך. בפועל, ה-slab ישתמש ב-order 1 (2 דפים, 8KB) או יותר עבור ה-slab שלו.


פתרון 3 - הבנת דגלי GFP

א. GFP_KERNEL - זו הקצאה רגילה בהקשר של תהליך (process context, בתוך syscall). אפשר לישון - אם אין זכרון פנוי, הקרנל יכול לעשות IO (לכתוב dirty pages, להעביר דפים ל-swap) ולהמתין עד שיתפנה זכרון.

ב. GFP_ATOMIC - אנחנו בinterrupt handler, אסור לישון! אם אין זכרון פנוי ברגע ההקצאה, הפונקציה מחזירה NULL. הדרייבר צריך לטפל במצב הזה (למשל לזרוק את הפקט).

ג. GFP_KERNEL | GFP_DMA - צריכים זכרון מ-ZONE_DMA (16MB הראשונים) עבור התקן DMA ישן. הדגלים משולבים כי אנחנו גם בprocess context (אפשר לישון) וגם צריכים את הzone הספציפי.

ד. אם בטעות נשתמש ב-GFP_KERNEL בתוך interrupt handler, הקרנל עלול לנסות לישון (להמתין ל-IO, למשל) בזמן שהinterrupt עדיין לא טופל. זה יגרום ל:
- BUG או scheduling while atomic - הקרנל מזהה שניסו לישון בהקשר אטומי ומדפיס אזהרה חמורה (או kernel panic במצבים מסוימים).
- deadlock - אם הinterrupt handler ישן, הCPU ייתקע כי אף אחד לא יעיר אותו (הinterrupt עדיין פעיל).

זו סיבה למה חשוב מאוד לדעת באיזה הקשר הקוד רץ ולבחור את הדגל הנכון.


פתרון 4 - ניתוח שדות /proc/meminfo

א. כן, SReclaimable + SUnreclaim = Slab (בקירוב). למשל:

Slab:          350000 kB
SReclaimable:  280000 kB
SUnreclaim:     70000 kB

280000 + 70000 = 350000.

ב. SReclaimable - זכרון slab שהקרנל יכול לשחרר אם צריך זכרון. בעיקר ה-dentry cache (dcache) ו-inode cache. הקרנל שומר את ה-dentries וה-inodes ב-slab caches כדי לזרז פתרון נתיבים, אבל הוא יכול לזרוק אותם ולקרוא אותם מחדש מהדיסק אם צריך.

SUnreclaim - זכרון slab שלא ניתן לשחרור. למשל, task_struct של תהליכים שרצים, sk_buff של חיבורי רשת פעילים. אי אפשר לשחרר אותם כי הם בשימוש פעיל.

ג. VmallocTotal - הגודל הכולל של מרחב הכתובות הוירטואלי שזמין ל-vmalloc. במערכת 64 ביט זה בדרך כלל מספר ענקי (TB) כי מרחב הכתובות של הקרנל ב-64 ביט הוא עצום.

VmallocUsed - כמה מתוך המרחב הזה בשימוש. בדרך כלל מספר קטן יחסית - זכרון שהוקצה עם vmalloc עבור מודולי קרנל, buffers גדולים, וכו'.


פתרון 5 - מודול קרנל מושגי

א. קוד (פסאודו-קוד):

#include <linux/slab.h>
#include <linux/list.h>

static struct kmem_cache *my_item_cache;
static LIST_HEAD(item_list);

// יצירת ה-cache (בפונקציית init של המודול)
static int __init my_module_init(void)
{
    my_item_cache = kmem_cache_create("my_item",
                                       sizeof(struct my_item),
                                       0,
                                       0,
                                       NULL);
    if (!my_item_cache)
        return -ENOMEM;

    // הקצאת 3 אובייקטים
    int i;
    for (i = 0; i < 3; i++) {
        struct my_item *item = kmem_cache_alloc(my_item_cache, GFP_KERNEL);
        if (!item)
            goto cleanup;
        item->id = i;
        snprintf(item->name, 64, "item_%d", i);
        list_add(&item->list, &item_list);
    }

    return 0;

cleanup:
    // שחרור האובייקטים שכבר הוקצו
    struct my_item *item, *tmp;
    list_for_each_entry_safe(item, tmp, &item_list, list) {
        list_del(&item->list);
        kmem_cache_free(my_item_cache, item);
    }
    kmem_cache_destroy(my_item_cache);
    return -ENOMEM;
}

// שחרור (בפונקציית exit של המודול)
static void __exit my_module_exit(void)
{
    struct my_item *item, *tmp;
    list_for_each_entry_safe(item, tmp, &item_list, list) {
        list_del(&item->list);
        kmem_cache_free(my_item_cache, item);
    }
    kmem_cache_destroy(my_item_cache);
}

ב. היתרונות של slab cache ייעודי:

  • מהירות - ה-slab cache שומר אובייקטים משומשים. כש-kmem_cache_free נקרא, האובייקט חוזר לcache ולא ל-buddy allocator. ההקצאה הבאה לוקחת אובייקט מוכן מה-cache - הרבה יותר מהיר מ-kmalloc שצריך לחפש slab cache מתאים לגודל.
  • פחות פרגמנטציה - כל האובייקטים באותו גודל, אז אין "חורים" של גדלים שונים.
  • אפשר לראות ב-/proc/slabinfo - ה-cache מופיע בשם שנתנו לו ("my_item"), מה שמקל על דיבוג ומעקב אחרי שימוש בזכרון.
  • אופציונלי: constructor - אפשר להגדיר פונקציית אתחול שרצה כשאובייקט מוקצה, מה שחוסך קוד אתחול חוזר.

ג. עבור buffer של 2MB - vmalloc הוא הבחירה הנכונה.

הסיבות:
- kmalloc מקצה זכרון רציף פיזית. 2MB = 512 דפים רציפים. בגלל פרגמנטציה של זכרון פיזי, ייתכן שאין בלוק רציף כזה גדול, ואז ההקצאה תיכשל.
- vmalloc מקצה דפים לא-רציפים וממפה אותם לכתובות וירטואליות רציפות. הסיכוי להצלחה הרבה יותר גבוה כי לא צריך רציפות פיזית.
- אנחנו לא צריכים רציפות פיזית (אנחנו רוצים buffer לעיבוד נתונים, לא ל-DMA), אז vmalloc מספיק.
- vmalloc איטי יותר מ-kmalloc (צריך לעדכן page tables), אבל אם מקצים פעם אחת ומשתמשים הרבה פעמים - ההבדל זניח.