לדלג לתוכן

בונים malloc משלכם ב-C ומבינים סוף סוף מה קורה בזיכרון

malloc ו-free הם מהפונקציות הראשונות שלומדים ב-C, אבל כמעט אף אחד לא עוצר לשאול איך הן בעצם עובדות מתחת למכסה המנוע. הפרויקט הזה - מימוש מנהל זיכרון פשוט משלכם - הוא אחד הפרויקטים שמלמדים הכי הרבה על ניהול זיכרון, בדיוק כי הוא מכריח אתכם לפתור בעצמכם את הבעיה ש-malloc פותרת בלי שאתם רואים.

הבנת הבסיס - Heap מול Stack

לפני שבונים משהו, צריך להבין את הבמה שעליה משחקים. ה-Stack הוא אזור זיכרון שמנוהל אוטומטית - כל פעם שקוראים לפונקציה, נפתח עליו "מסגרת" חדשה למשתנים המקומיים שלה, ואיפה שהפונקציה מסתיימת המסגרת נעלמת אוטומטית. ה-Heap הוא שונה לגמרי - זה אזור זיכרון שאתם, המתכנתים, אחראים עליו ידנית. כשקוראים ל-malloc, מבקשים בלוק זיכרון מה-Heap, וזה נשאר תפוס עד שקוראים ל-free באופן מפורש. שום דבר לא קורה אוטומטית שם, וזה בדיוק המקום שבו הפרויקט הזה חי.

הרעיון המרכזי - free list

הגישה הפשוטה והנפוצה ביותר למימוש מנהל זיכרון היא free list - רשימה מקושרת של "חורים" פנויים בזיכרון שאפשר להקצות מהם. בכל בלוק זיכרון, שומרים גם כותרת קטנה (header) שמתעדת את גודל הבלוק ומצביע לבלוק החופשי הבא ברשימה:

typedef struct block {
    size_t size;
    int free;
    struct block *next;
} block_t;

כשמבקשים זיכרון, ה-malloc שלכם עובר על הרשימה ומחפש בלוק חופשי גדול מספיק. כשקוראים ל-free, פשוט מסמנים את הבלוק בחזרה כפנוי, בלי להחזיר אותו בפועל למערכת ההפעלה - זה מה שהופך אותו לזמין להקצאה הבאה.

מימוש בסיסי של malloc

גרסה ראשונית ופשוטה של החיפוש נראית כך - עוברים על הרשימה ומחפשים את הבלוק החופשי הראשון שמתאים בגודלו (גישה שנקראת first-fit):

void *my_malloc(size_t size) {
    block_t *curr = free_list;
    while (curr) {
        if (curr->free && curr->size >= size) {
            curr->free = 0;
            return (void *)(curr + 1);
        }
        curr = curr->next;
    }
    // לא נמצא בלוק מתאים - צריך להרחיב את ה-heap
    return request_more_memory(size);
}

כשאין בלוק פנוי מתאים ברשימה, מנהל הזיכרון מבקש עוד זיכרון מהמערכת ההפעלה בעצמו, בדרך כלל עם קריאת מערכת כמו sbrk או mmap.

מימוש free ובעיית הפיצול

free בגרסה הבסיסית ביותר רק מסמן את הבלוק כפנוי מחדש. אבל אם עוצרים שם, נתקלים מהר מאוד בבעיה שנקראת פיצול (fragmentation) - הרבה בלוקים קטנים פנויים מפוזרים בזיכרון, כשאף אחד מהם לא גדול מספיק להקצאה חדשה, למרות שסך כל הזיכרון הפנוי מספיק בקלות. הפתרון הנפוץ הוא מיזוג (coalescing) - כשמשחררים בלוק, בודקים אם הבלוקים השכנים לו בזיכרון גם פנויים, ואם כן, ממזגים אותם לבלוק גדול אחד.

הבאגים הקלאסיים שהפרויקט חושף

אחרי שבניתם את זה בעצמכם, כל אחד מהבאגים הקלאסיים של C מפסיק להיות מושג מופשט. דליפת זיכרון (memory leak) - קוראים ל-malloc ושוכחים לקרוא ל-free המתאים, וכל בלוק כזה נשאר תפוס לנצח. שחרור כפול (double free) - קוראים ל-free פעמיים על אותו מצביע, מה שמשבש את מבנה הרשימה הפנימית ויכול לגרום להתנהגות בלתי צפויה. שימוש אחרי שחרור (use-after-free) - ממשיכים להשתמש בזיכרון אחרי ששחררתם אותו, בזמן שהוא כבר יכול היה להיות מוקצה מחדש למשהו אחר לגמרי. כשמימשתם את מנהל הזיכרון בעצמכם, קל הרבה יותר להבין למה כל אחת מהטעויות האלה מסוכנת כל כך.

מה מרוויחים מהפרויקט

מעבר להבנת malloc עצמה, הפרויקט הזה נותן הבנה עמוקה יותר של איך כל תוכנית - בכל שפה, לא רק C - מנהלת זיכרון בפועל. גם בשפות עם garbage collector כמו Python או Java, מתחת למכסה המנוע קורה גרסה מורכבת יותר של בדיוק אותו רעיון.

אם הפרויקט הזה עורר לכם עניין בעולם שמתחת לשפות תכנות ברמה גבוהה, קורס ליבת המחשב שלנו מעמיק עוד יותר בניהול זיכרון, קריאות מערכת, והבנה אמיתית של איך מחשב עובד.

נתקעתם בפיצול, בדליפת זיכרון, או סתם רוצים לשתף את הקוד שכתבתם? יש לנו קהילה בדיוק לזה.

הצטרפו לקהילה בדיסקורד