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). למשל:
כאן יש 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 (בקירוב). למשל:
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), אבל אם מקצים פעם אחת ומשתמשים הרבה פעמים - ההבדל זניח.