5.7 מיפוי זכרון פתרון
פתרונות - מיפוי זכרון - mmap¶
פתרון 1 - הקצאת זכרון עם mmap¶
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
int main() {
// הקצאת 4096 בתים עם mmap - מיפוי אנונימי פרטי
void *ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (ptr == MAP_FAILED) {
perror("mmap");
return 1;
}
// כתיבת מחרוזת לזכרון
char *msg = (char *)ptr;
strcpy(msg, "hello from mmap!");
// קריאה והדפסה
printf("read from mmap: %s\n", msg);
// שחרור הזכרון
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;
}
// פתיחת הקובץ לקריאה
int fd = open(argv[1], O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}
// קבלת גודל הקובץ
struct stat sb;
if (fstat(fd, &sb) < 0) {
perror("fstat");
close(fd);
return 1;
}
// מיפוי הקובץ לזכרון
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); // אפשר לסגור את הfd - המיפוי נשאר
// הדפסת התוכן בית אחרי בית
for (size_t i = 0; i < (size_t)sb.st_size; i++) {
putchar(mapped[i]);
}
// שחרור המיפוי
munmap(mapped, sb.st_size);
return 0;
}
פתרון 3 - זכרון משותף בין תהליכים¶
#include <stdio.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
// יצירת אזור זכרון משותף
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) {
// תהליך הבן - קוראים את הערך
printf("child reads: %d\n", *shared);
_exit(0);
} else if (pid > 0) {
// תהליך האב - כותבים את הערך
*shared = 1234;
printf("parent wrote: 1234\n");
// ממתינים לבן
wait(NULL);
} else {
perror("fork");
return 1;
}
munmap(shared, sizeof(int));
return 0;
}
הערה: בדוגמה הזו תהליך האב כותב לזכרון לפני שתהליך הבן קורא. בתרחיש אמיתי ייתכן שהבן ירוץ לפני שהאב יספיק לכתוב. כדי לפתור את זה צריך סנכרון (נלמד על זה בפרק 5.8 על threads). כאן זה עובד כי fork יוצר תהליך חדש וזה לוקח זמן, כך שבדרך כלל האב מספיק לכתוב לפני שהבן מתחיל לרוץ.
פתרון 4 - שינוי קובץ דרך mmap¶
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
int main() {
// פתיחת הקובץ לקריאה וכתיבה
int fd = open("testfile.txt", O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}
// קבלת גודל הקובץ
struct stat sb;
if (fstat(fd, &sb) < 0) {
perror("fstat");
close(fd);
return 1;
}
// מיפוי הקובץ עם MAP_SHARED - שינויים ישפיעו על הקובץ בדיסק
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);
// שינוי הבית הראשון
printf("before: first byte = '%c'\n", mapped[0]);
mapped[0] = 'X';
printf("after: first byte = '%c'\n", mapped[0]);
// שחרור המיפוי - השינויים נכתבים לקובץ
munmap(mapped, sb.st_size);
printf("file modified! check with: cat testfile.txt\n");
return 0;
}
כדי לבדוק, צרו קובץ testfile.txt עם תוכן כלשהו ואז הריצו את התוכנית:
תראו שהתו הראשון השתנה ל-X, כך שהתוכן הוא Xello World.
פתרון 5 - שאלה תיאורטית¶
הקרנל משתמש ב-mmap לטעינת קבצי ELF מכמה סיבות:
-
ביצועים עם demand paging - כשממפים קובץ עם mmap, הדפים לא נטענים מיד לזכרון. הקרנל יוצר את ערכי ה-page table, אבל הדפים עצמים נטענים רק כשהתהליך ניגש אליהם (page fault). בקובץ הרצה גדול, ייתכן שחלקים ממנו לעולם לא ירוצו (למשל קוד טיפול בשגיאות נדירות), אז אין סיבה לטעון אותם. עם
read()היינו חייבים להעתיק את הכל מראש. -
שיתוף דפים בין תהליכים - אם 100 תהליכים מריצים את אותה תוכנית, הדפים של קטע הקוד (שהוא read-only) קיימים פעם אחת בRAM ומשותפים בין כל התהליכים. עם
read()כל תהליך היה מקבל עותק פרטי, מה שבזבוז של זכרון. -
אין צורך בהעתקה כפולה - עם
read(), הקרנל קורא מהדיסק ל-page cache, ואז מעתיק מה-page cache לbuffer של התהליך. עם mmap, הקרנל פשוט ממפה את דפי ה-page cache ישירות למרחב הכתובות של התהליך - חיסכון של העתקה שלמה. -
ניהול זכרון יעיל - כשהמערכת צריכה זכרון, הקרנל יכול לזרוק דפים של קבצים ממופים (כי הם קיימים על הדיסק ואפשר לטעון אותם מחדש). עם
read()הזכרון הוא "אנונימי" ואי אפשר לזרוק אותו - צריך לרשום אותו ל-swap.