mmap, разделяемая память и копирование при записи

⏱ 35 мин · 🧩 заданий: 5

Системный вызов mmap

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
Аргумент Значение
addr желаемый адрес; NULL — ядро выберет само
length размер (округляется вверх до целого числа страниц)
prot права: PROT_READ, PROT_WRITE, PROT_EXEC, PROT_NONE
flags MAP_SHARED или MAP_PRIVATE, плюс MAP_ANONYMOUS, MAP_FIXED…
fd, offset файл и смещение в нём; для анонимной памяти -1 и 0

Возвращает адрес начала отображения или MAP_FAILED ((void *)-1) при ошибке — не NULL!

Четыре вида отображений

MAP_PRIVATE MAP_SHARED
Файл код и данные программ и библиотек; изменения — личные (COW), в файл не попадают «файл в памяти»: изменения видны всем и записываются в файл
Анонимная (MAP_ANONYMOUS) обычная память: большие блоки malloc, стеки потоков общая память родителя и потомков после fork

Разделяемая память для родственных процессов

Строки из задачи 8:

mem = mmap(NULL, sizeof(Mem), PROT_READ | PROT_WRITE,
           MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (mem == MAP_FAILED) _exit(1);
mem->counter = 0;
mem->state = 0;
pid_t pid = fork();
  • MAP_ANONYMOUS — память без файла, заполнена нулями.
  • MAP_SHARED — после fork у родителя и потомка в таблицах страниц будут ссылки на одни и те же физические кадры, без copy-on-write. Запись одного процесса сразу видна другим.
  • Потомки потомков тоже наследуют отображение — все процессы цепочки работают с одной структурой Mem.
родитель C0 C1 ... C4 C5 mem = 0x7f..000 mem = 0x7f..000 (тот же виртуальный адрес у всех) │ │ │ └──────────────────┴──────┬──────────┴────────────────────┘ ▼ один физический кадр: { state, counter, pid, ppid }

Общая память — общие проблемы

Раз все пишут в одну структуру, возникают гонки: mem->counter++ у двух процессов одновременно, перезапись mem->pid и mem->ppid разными процессами вперемешку. Для корректной работы нужна синхронизация — модуль «Взаимодействие процессов и синхронизация».

Что делает ядро при mmap

  1. Проверяет аргументы и права (для файла — открыт ли он на чтение/запись).
  2. Выбирает свободный диапазон виртуальных адресов нужного размера (в страницах).
  3. Создаёт новую VMA с правами и флагами; для файла — ссылку на файл (его inode и кеш страниц); для MAP_SHARED|MAP_ANONYMOUS — внутренний объект разделяемой памяти (shmem).
  4. Таблицы страниц не заполняет — физическая память выделяется лениво, при первом обращении (страничный отказ).
  5. Возвращает адрес.

Отображение файлов

int fd = open("data.bin", O_RDWR);
struct stat st;
fstat(fd, &st);
char *p = mmap(NULL, st.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);                      /* отображение остаётся! */
p[0] = 'X';                     /* изменяем файл как массив */
msync(p, st.st_size, MS_SYNC);  /* принудительно записать грязные страницы */
munmap(p, st.st_size);

Чтение страницы файла — страничный отказ и чтение с диска в кеш страниц; запись — бит D, и позже writeback запишет страницу в файл.

Copy-on-write ещё раз

MAP_PRIVATE и fork используют один механизм:

  1. страница помечается «только чтение» в таблицах всех, кто её разделяет;
  2. попытка записи → страничный отказ защиты;
  3. ядро видит, что VMA доступна для записи, а страница — COW: копирует кадр, ставит новую ссылку с правом записи;
  4. остальные процессы продолжают видеть старый кадр.

Другие виды разделяемой памяти

Для неродственных процессов анонимная память не подходит — нужно имя:

/* POSIX */
int fd = shm_open("/myshm", O_CREAT | O_RDWR, 0600);
ftruncate(fd, sizeof(Mem));
Mem *m = mmap(NULL, sizeof(Mem), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
/* ... */
shm_unlink("/myshm");

/* System V */
int id = shmget(key, sizeof(Mem), IPC_CREAT | 0600);
Mem *m2 = shmat(id, NULL, 0);
shmdt(m2);
shmctl(id, IPC_RMID, NULL);

POSIX-объекты видны в /dev/shm, System V — в ipcs -m.

Итог

  • mmap создаёт область адресного пространства; ошибка — MAP_FAILED; размер кратен странице.
  • MAP_SHARED — изменения общие (и в файл), MAP_PRIVATE — копирование при записи.
  • MAP_SHARED | MAP_ANONYMOUS + fork — общая память родителя и всех потомков.
  • Ядро при mmap только создаёт VMA; кадры выделяются при страничных отказах.
  • Общая память требует синхронизации.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Написать код

Выделите разделяемую анонимную память под структуру Mem (чтение и запись) — так, чтобы её видели родитель и потомки после fork. Проверьте результат на MAP_FAILED (при ошибке — _exit(1)).

Задание 2 Ввод ответа

Что напечатает родитель?

int *p = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE,
              MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
*p = 1;
if (fork() == 0) { *p = 2; _exit(0); }
wait(NULL);
printf("%d\n", *p);
Задание 3 Заполнить таблицу

MAP_SHARED или MAP_PRIVATE?

MAP_SHAREDMAP_PRIVATE
Изменения видны другим процессам, отобразившим тот же объект
Изменения файла записываются обратно в файл
Запись создаёт личную копию страницы (copy-on-write)
Так загрузчик отображает секцию данных .data программы
Подходит для обмена данными между родителем и потомками
Отметьте ячейку — «+», пустая — «−».
Задание 4 Несколько вариантов

Что делает ядро при вызове mmap(NULL, 16, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0)?

Задание 5 Один вариант ответа

Сколько байт физической памяти реально займёт mmap(NULL, 16, ...), после того как в неё записали?

Войдите, чтобы отмечать пройденные уроки.