ניהול זיכרון בקרנל מוסבר - איך הקרנל מקצה זיכרון לעצמו, לא רק לתהליכים¶
כשמדברים על ניהול זיכרון, בדרך כלל מתכוונים לאיך תהליכים מקבלים זיכרון וירטואלי דרך malloc. אבל יש שאלה קודמת יותר, שלרוב לא נשאלת - מי בכלל מנהל את הזיכרון הפיזי עצמו, ואיך הקרנל, שהוא בעצמו תוכנה שרצה וזקוקה לזיכרון, מקצה משאבים לעצמו בלי להסתמך על אף אחד מעליו. התשובה כוללת כמה מנגנונים ייחודיים שלא קיימים בכלל בעולם התוכנה הרגיל.
החלוקה הבסיסית - עמודי זיכרון וה-buddy allocator¶
הזיכרון הפיזי במחשב מחולק ליחידות קבועות בגודלן, עמודי זיכרון - pages, בדרך כלל בגודל של ארבעה קילובייט כל אחד. כשהקרנל צריך להקצות זיכרון פיזי, הוא לא עושה זאת ביחידות שרירותיות - הוא עובד תמיד במונחים של עמודים שלמים, ולפעמים בלוקים של כמה עמודים רצופים ביחד.
המנגנון שמנהל את ההקצאה הזאת נקרא buddy allocator, מקצה החברים. הרעיון שלו הוא לשמור רשימות של בלוקים פנויים בגדלים שהם חזקות של שתיים - בלוק אחד, שני עמודים, ארבעה עמודים, וכך הלאה. כשמבקשים הקצאה, המקצה מחפש את הבלוק הקטן ביותר שעדיין מספיק גדול לבקשה. אם בלוק גדול מתפצל לשניים כדי לספק בקשה קטנה יותר, שני החצאים נקראים "חברים" - buddies, וכשהם משתחררים שוב, המקצה בודק אם החבר שלהם פנוי גם הוא, ואם כן, מאחד אותם בחזרה לבלוק גדול יותר, כדי למנוע פיצול זיכרון לאורך זמן.
למה זה לא מספיק - הבעיה עם אובייקטים קטנים¶
ה-buddy allocator מצוין להקצאות בגודל עמוד ומעלה, אבל הקרנל צריך כל הזמן להקצות אובייקטים קטנים בהרבה מעמוד שלם - מבנה נתונים שמייצג תהליך, מתאר קובץ, או חבילת רשת. הקצאת עמוד שלם לכל אובייקט קטן כזה תבזבז המון זיכרון.
הפתרון הוא מקצה slab, שעובד מעל ה-buddy allocator. הרעיון הוא לקחת עמודי זיכרון גדולים, ולחלק אותם מראש למאגרים - caches, של אובייקטים בגודל קבוע, מתאימים בדיוק לסוגי המבנים שהקרנל מקצה שוב ושוב. כשמבקשים אובייקט חדש מסוג מסוים, הקרנל פשוט לוקח משבצת פנויה מהמאגר המתאים, במקום לעבור שוב את כל תהליך ההקצאה מאפס. זה גם מהיר משמעותית, וגם חוסך פיצול זיכרון, כי כל מאגר מכיל רק אובייקטים מאותו גודל בדיוק. הגרסה המודרנית והנפוצה של המנגנון הזה נקראת SLUB.
kmalloc מול vmalloc - שתי דרכים להקצות זיכרון בקרנל¶
כשקוד בתוך הקרנל צריך זיכרון, יש שתי אפשרויות עיקריות. kmalloc מקצה זיכרון שרציף גם פיזית וגם וירטואלית - הכתובות בזיכרון האמיתי עוקבות זו אחר זו בדיוק כמו הכתובות הווירטואליות. זה מהיר, אבל מוגבל בגודל, כי קשה למצוא בלוקים פיזיים גדולים ורציפים כשהזיכרון מפוצל אחרי זמן ריצה ארוך.
vmalloc לעומת זאת מקצה זיכרון שרציף רק וירטואלית - מבחינת הכתובות שהקוד רואה, זה נראה כמו בלוק אחד רציף, אבל מתחת למכסה הוא יכול להיות מורכב מעמודים פיזיים מפוזרים לגמרי, שמחוברים יחד רק דרך טבלאות הזיכרון הווירטואלי. זה מאפשר הקצאות גדולות יותר בהרבה, במחיר קטן של ביצועים, כי כל גישה דורשת תרגום כתובת נוסף.
אזורי זיכרון - zones¶
לא כל הזיכרון הפיזי שווה מבחינת החומרה. חלק מהחומרה הישנה יכולה לגשת רק לטווח כתובות מוגבל, ולכן הקרנל מחלק את הזיכרון הפיזי לאזורים - zones, כמו אזור DMA לחומרה עם מגבלות כתובות, ואזור רגיל לשאר השימושים. ההקצאה תמיד מתחשבת באילו אזורים מתאימים לבקשה הספציפית, כדי לוודא שהתקן שמבקש זיכרון בכלל יכול לגשת אליו.
למה כדאי להכיר את השכבה הזאת¶
מי שמבין את שכבת ניהול הזיכרון של הקרנל עצמו, מבין הרבה יותר טוב למה ביצועי מערכת יכולים להידרדר לאורך זמן בגלל פיצול זיכרון, ולמה כלים כמו slabtop נותנים תמונה מדויקת של איפה בדיוק הזיכרון של הקרנל הולך.
זה בדיוק סוג ההבנה שאנחנו בונים בקורס ליבת המחשב - לא רק איך תהליכים מקבלים זיכרון, אלא איך הקרנל עצמו מנהל את המשאב הזה מהיסוד.
ואם ניהול זיכרון ברמת הקרנל נשמע לכם מופשט מדי, יש קהילה שתשמח לעזור לפרק את זה יחד.
לסיכום¶
הקרנל מנהל את הזיכרון הפיזי שלו עצמו בשכבות - buddy allocator לעמודים שלמים, מקצי slab לאובייקטים קטנים וחוזרים, ו-kmalloc או vmalloc תלוי אם צריך רציפות פיזית או רק וירטואלית. כל שכבה נבנתה כדי לפתור בעיה ספציפית של יעילות ופיצול, וביחד הן מאפשרות לקרנל להריץ מיליוני הקצאות זיכרון בשנייה בלי לאבד שליטה על המשאב היקר ביותר במערכת.