לדלג לתוכן

5.7 - מיפוי זכרון - פתרון

פתרונות - מיפוי זכרון - mmap

פתרון 1 - allocation זכרון עם mmap

#include <stdio.h>
#include <string.h>
#include <sys/mman.h>

int main() {
    // allocate 4096 bytes with mmap - private anonymous mapping
    void *ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
                     MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

    if (ptr == MAP_FAILED) {
        perror("mmap");
        return 1;
    }

    // write a string to memory
    char *msg = (char *)ptr;
    strcpy(msg, "hello from mmap!");

    // read and print
    printf("read from mmap: %s\n", msg);

    // free the memory
    if (munmap(ptr, 4096) < 0) {
        perror("munmap");
        return 1;
    }

    return 0;
}

פתרון 2 - קריאת קובץ עם mmap

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "usage: %s <filename>\n", argv[0]);
        return 1;
    }

    // open the file for reading
    int fd = open(argv[1], O_RDONLY);
    if (fd < 0) {
        perror("open");
        return 1;
    }

    // get the file size
    struct stat sb;
    if (fstat(fd, &sb) < 0) {
        perror("fstat");
        close(fd);
        return 1;
    }

    // map the file into memory
    char *mapped = mmap(NULL, sb.st_size, PROT_READ,
                        MAP_PRIVATE, fd, 0);
    if (mapped == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }

    close(fd); // the fd can be closed now - the mapping remains

    // print the content byte by byte
    for (size_t i = 0; i < (size_t)sb.st_size; i++) {
        putchar(mapped[i]);
    }

    // free the mapping
    munmap(mapped, sb.st_size);

    return 0;
}

פתרון 3 - זכרון משותף בין processes

#include <stdio.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
    // create a shared memory region
    int *shared = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE,
                       MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (shared == MAP_FAILED) {
        perror("mmap");
        return 1;
    }

    *shared = 0;

    pid_t pid = fork();

    if (pid == 0) {
        // child process - read the value
        printf("child reads: %d\n", *shared);
        _exit(0);
    } else if (pid > 0) {
        // parent process - write the value
        *shared = 1234;
        printf("parent wrote: 1234\n");

        // wait for the child
        wait(NULL);
    } else {
        perror("fork");
        return 1;
    }

    munmap(shared, sizeof(int));
    return 0;
}

הערה: בדוגמה הזו process האב כותב לזכרון לפני שprocess הבן קורא. בתרחיש אמיתי ייתכן שהבן ירוץ לפני שהאב יספיק לכתוב. כדי לפתור את זה צריך סנכרון (נלמד על זה בפרק 5.8 על threads). כאן זה עובד כי fork יוצר process חדש וזה לוקח זמן, כך שבדרך כלל האב מספיק לכתוב לפני שהבן מתחיל לרוץ.


פתרון 4 - שינוי קובץ דרך mmap

#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

int main() {
    // open the file for reading and writing
    int fd = open("testfile.txt", O_RDWR);
    if (fd < 0) {
        perror("open");
        return 1;
    }

    // get the file size
    struct stat sb;
    if (fstat(fd, &sb) < 0) {
        perror("fstat");
        close(fd);
        return 1;
    }

    // map the file with MAP_SHARED - changes will affect the file on disk
    char *mapped = mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE,
                        MAP_SHARED, fd, 0);
    if (mapped == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }

    close(fd);

    // change the first byte
    printf("before: first byte = '%c'\n", mapped[0]);
    mapped[0] = 'X';
    printf("after: first byte = '%c'\n", mapped[0]);

    // free the mapping - the changes are written to the file
    munmap(mapped, sb.st_size);

    printf("file modified! check with: cat testfile.txt\n");
    return 0;
}

כדי לבדוק, צרו קובץ testfile.txt עם תוכן כלשהו ואז הריצו את התוכנית:

echo "Hello World" > testfile.txt
gcc solution4.c -o solution4
./solution4
cat testfile.txt

תראו שהתו הראשון השתנה ל-X, כך שהתוכן הוא Xello World.


פתרון 5 - שאלה תיאורטית

הקרנל משתמש ב-mmap לטעינת קבצי ELF מכמה סיבות:

  1. ביצועים עם demand paging - כשממפים קובץ עם mmap, הpages לא נטענים מיד לזכרון. הקרנל יוצר את ערכי ה-page table, אבל הpages עצמם נטענים רק כשהprocess ניגש אליהם (page fault). בקובץ הרצה גדול, ייתכן שחלקים ממנו לעולם לא ירוצו (למשל קוד טיפול בשגיאות נדירות), אז אין סיבה לטעון אותם. עם read() היינו חייבים להעתיק את הכל מראש.

  2. שיתוף pages בין processes - אם 100 processes מריצים את אותה תוכנית, הpages של קטע הקוד (שהוא read-only) קיימים פעם אחת בRAM ומשותפים בין כל הprocesses. עם read() כל process היה מקבל עותק פרטי, מה שבזבוז של זכרון.

  3. אין צורך בהעתקה כפולה - עם read(), הקרנל קורא מהדיסק ל-page cache, ואז מעתיק מה-page cache לbuffer של הprocess. עם mmap, הקרנל פשוט ממפה את pages ה-page cache ישירות למרחב הכתובות של הprocess - חיסכון של העתקה שלמה.

  4. ניהול זכרון יעיל - כשהמערכת צריכה זכרון, הקרנל יכול לזרוק pages של קבצים ממופים (כי הם קיימים על הדיסק ואפשר לטעון אותם מחדש). עם read() הזכרון הוא "אנונימי" ואי אפשר לזרוק אותו - צריך לרשום אותו ל-swap.