Shared memory — shm_open, mmap MAP_SHARED и /dev/shm
Каждый процесс в Linux живёт в своём виртуальном адресном пространстве. Процесс A не может прочитать переменную процесса B, даже если они на одной машине, потому что MMU не пускает — адреса разные, page tables не пересекаются. Это фундаментальная изоляция, и она хорошо защищает от багов и злоумышленников.
Но иногда два процесса хотят обмениваться большими объёмами данных. Передать гигабайт через pipe или TCP-сокет — это копировать его через kernel, минимум одно полное копирование. Долго. Shared memory — решение: kernel делает так, что одни и те же физические страницы видны в двух (или больше) виртуальных адресных пространствах. Процессы пишут и читают в общую область как в свою собственную память. Никаких копирований.
Используется везде, где нужна высокая пропускная: PostgreSQL делит shared_buffers через shared memory, redis-cluster для message-passing между шардами, X-server и клиенты для буферов экрана, multimedia-pipelines (gstreamer, ffmpeg) для frame-passing.
В этом уроке: POSIX shm_open vs SysV shmget, mmap MAP_SHARED, файловая система /dev/shm, и базовые вопросы синхронизации.
Зачем нужна shared memory
Подумайте о producer-consumer паттерне: процесс A читает с диска большой файл, обрабатывает, отдаёт процессу B для дальнейшей работы. Варианты:
- TCP-сокет — A пишет данные в сокет, kernel копирует их в buffer ядра, B читает из buffer-а в свой userspace. Минимум 2 копирования + handshake. Подходит для cross-machine, но overkill для same-machine.
- Pipe / FIFO — то же самое, через named pipe в файловой системе. Один копирование меньше (без сетевого стека), но всё равно копирование.
- Unix domain socket — 1 копирование. Быстрее TCP.
- Shared memory — 0 копирований. A пишет в общую область, B сразу её читает. Только синхронизация (mutex/semaphore) нужна.
Цена shared memory — синхронизация. Если оба процесса пишут одновременно в одну ячейку — race condition. Нужны mutex-ы, semaphores или atomic операции. И это уже не «kernel за вас всё делает», это userspace-задача.
POSIX shm_open + mmap — современный путь
POSIX-стандарт определяет API через два шага:
shm_open(name, flags, mode)— создаёт или открывает named shared memory объект. Возвращает file descriptor.mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)— маппит этот объект в адресное пространство.
«Файл» в shm_open — это запись в файловой системе tmpfs, монтированной в /dev/shm. Она целиком в RAM (не на диске), но выглядит как файл — можно ls-нуть, изменить permissions, удалить через rm.
cat > shm_writer.c << 'CEOF'
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
int main(void) {
int fd = shm_open("/my_shm", O_CREAT | O_RDWR, 0666);
ftruncate(fd, 4096);
char* ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
strcpy(ptr, "Hello from writer!");
printf("Wrote message to shared memory. Press Enter to exit.\n");
getchar();
munmap(ptr, 4096);
shm_unlink("/my_shm");
return 0;
}
CEOF
cat > shm_reader.c << 'CEOF'
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <stdio.h>
int main(void) {
int fd = shm_open("/my_shm", O_RDONLY, 0666);
char* ptr = mmap(NULL, 4096, PROT_READ, MAP_SHARED, fd, 0);
printf("Read from shared memory: %s\n", ptr);
munmap(ptr, 4096);
return 0;
}
CEOF
gcc shm_writer.c -o shm_writer -lrt
gcc shm_reader.c -o shm_reader -lrt
# Terminal 1:
./shm_writer
# Wrote message to shared memory. Press Enter to exit.
# Terminal 2 (параллельно):
ls /dev/shm/
# my_shm
./shm_reader
# Read from shared memory: Hello from writer!
Видите /my_shm в /dev/shm/? Это файл-представитель shared memory объекта. Имя начинается со слеша по конвенции POSIX. Размер — 4096 байт (одна страница).
Важно: shm_unlink удаляет имя из tmpfs, но не сами страницы, пока их кто-то держит mmap-нутыми. Это похоже на семантику обычных файлов в Unix — inode жив пока есть ссылки.
/dev/shm — tmpfs в виде файловой системы
/dev/shm — стандартная точка монтирования tmpfs в Linux. Все объекты POSIX shared memory лежат там как файлы.
df -h /dev/shm
# Filesystem Size Used Avail Use% Mounted on
# tmpfs 7.7G 120M 7.6G 2% /dev/shm
mount | grep /dev/shm
# tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev,inode64,size=7906112k)
Размер /dev/shm по умолчанию — половина RAM. Можно перенастроить:
# Изменить размер /dev/shm на лету:
sudo mount -o remount,size=4G /dev/shm
# Постоянно -- через /etc/fstab:
# tmpfs /dev/shm tmpfs defaults,size=4G 0 0
Поскольку /dev/shm — это просто файлы, можно использовать его как быстрое хранилище без shm_open API:
# Создать "файл" размером 100 МБ в /dev/shm:
dd if=/dev/zero of=/dev/shm/myfile bs=1M count=100
# Использовать как любой файл -- но в RAM, никакого диска:
echo "fast data" > /dev/shm/mydata
cat /dev/shm/mydata
# Удалить (вернёт RAM):
rm /dev/shm/myfile
Это используется для temporary-данных, которые нужно ультра-быстро прочитать/записать. PyTorch DataLoader использует /dev/shm для shared memory между worker-процессами. Redis может использовать /dev/shm для unix domain socket-а.
Файлы в /dev/shm живут в RAM. Если приложение создаст там много больших файлов и забудет удалить — съест всю память сервера. Мониторьте `df /dev/shm` или ставьте ограничение размером tmpfs.
Anonymous mmap MAP_SHARED — shared без имени
Если процессы родственные (fork от одного родителя), можно обойтись без shm_open. Просто mmap с MAP_SHARED | MAP_ANONYMOUS — получите кусок shared memory, который наследуется при fork.
cat > anon_shared.c << 'CEOF'
#include <sys/mman.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
#include <string.h>
int main(void) {
char* shared = mmap(NULL, 4096,
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
strcpy(shared, "from parent");
if (fork() == 0) {
printf("Child sees: %s\n", shared);
strcpy(shared, "child responded");
return 0;
}
wait(NULL);
printf("Parent now sees: %s\n", shared);
munmap(shared, 4096);
return 0;
}
CEOF
gcc anon_shared.c -o anon_shared
./anon_shared
# Вывод:
# Child sees: from parent
# Parent now sees: child responded
Это удобный паттерн для fork-based workers (как у gunicorn в pre-fork режиме) — общая память без файла, никаких пермиссий, никаких имён. Удаляется автоматически при exit.
SysV shared memory — историческое
Помимо POSIX, в Linux есть SysV-стиль API (исторический, из System V Unix): shmget, shmat, shmdt, shmctl. Концептуально то же самое, но через целочисленные ключи вместо имён.
# Посмотреть SysV-объекты в системе:
ipcs -m
# Типичный вывод:
# ------ Shared Memory Segments --------
# key shmid owner perms bytes nattch status
# 0x12345678 32768 postgres 600 144441344 4
# 0x00000000 65537 myuser 600 4096 2 dest
postgres хранит свои shared_buffers через SysV. Каждая запись — кусок shared memory, который видят все backend-процессы PostgreSQL.
В новом коде используйте POSIX — проще, чище, лучше интегрирован с mmap.
Berkeley sockets API: IPC через Unix domain socketsСинхронизация в shared memory
Самая болезненная часть. Два процесса видят одну и ту же память — если оба пишут в одну ячейку, исход непредсказуем. Нужна синхронизация.
Варианты:
- POSIX named semaphores (
sem_open) — семафоры, привязанные к имени. Видны как файлы в /dev/shm с префиксомsem.. - POSIX mutex-ы в shared memory — создать
pthread_mutex_tв общей области с атрибутомPTHREAD_PROCESS_SHARED. Любой процесс, маппящий область, получает доступ. - Atomic операции — если данные простые (счётчики, флаги), можно обойтись atomic-ами без mutex-а. C11
_Atomic, Ruststd::sync::atomic, Go-каналы (но они не shm-friendly). - Lock-free очереди — ring buffer + atomic head/tail. Очень быстро при правильной реализации, но сложно.
Пример с pthread mutex в shared memory:
cat > shm_mutex.c << 'CEOF'
#include <fcntl.h>
#include <pthread.h>
#include <sys/mman.h>
#include <unistd.h>
#include <stdio.h>
typedef struct {
pthread_mutex_t lock;
int counter;
} Shared;
int main(void) {
int fd = shm_open("/counter", O_CREAT | O_RDWR, 0666);
ftruncate(fd, sizeof(Shared));
Shared* s = mmap(NULL, sizeof(Shared),
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
pthread_mutex_init(&s->lock, &attr);
s->counter = 0;
for (int i = 0; i < 100000; i++) {
pthread_mutex_lock(&s->lock);
s->counter++;
pthread_mutex_unlock(&s->lock);
}
printf("counter = %d\n", s->counter);
shm_unlink("/counter");
return 0;
}
CEOF
gcc shm_mutex.c -o shm_mutex -lpthread -lrt
./shm_mutex
# counter = 100000
PTHREAD_PROCESS_SHARED — ключевой атрибут. Без него mutex работает только между потоками одного процесса.
Когда shared memory это плохая идея
Shared memory мощный, но не панацея. Минусы:
- Сложность синхронизации. Mutex-ы между процессами могут зависнуть, если один процесс умрёт держа lock. Нужны robust mutex-ы (
PTHREAD_MUTEX_ROBUST), и логика recovery. - Hard to debug. Race conditions в shared memory воспроизводятся плохо. valgrind не помогает — он работает с одним процессом.
- Жёсткая связь. Процессы должны договариваться о бинарном layout структур. Если один скомпилирован новой версией — невидимая несовместимость.
- Безопасность. Утечки информации между процессами разных пользователей. Permissions на shm нужно ставить аккуратно.
- Не работает кросс-машинно. Очевидно — общая RAM это одна машина.
Когда стоит:
- Высокая bandwidth. Большие данные между процессами на одной машине.
- Низкая latency. Микросекундные требования.
- DB engines. PostgreSQL, MySQL хранят буферный кэш в shm.
- GPU drivers, X-server. Big framebuffers + IPC.
Когда не стоит:
- Простые случаи. Pipe или unix socket проще и достаточно быстры.
- Cross-machine. Сразу TCP / message broker.
- Разные языки. Тяжело: binary layout зависит от компилятора, padding, endianness.
Попробуй сам
Создайте shared memory регион и посмотрите его в /dev/shm:
# В одном терминале:
python3 << 'EOF'
from multiprocessing import shared_memory
import time
shm = shared_memory.SharedMemory(name='mytest', create=True, size=4096)
shm.buf[:13] = b'Hello shared!'
print(f'Created /dev/shm/{shm.name}, size {shm.size}')
print('Sleeping 60 sec; check other terminal')
time.sleep(60)
shm.close()
shm.unlink()
EOF
# В другом терминале:
ls -la /dev/shm/mytest
# -rw-------. 1 myuser myuser 4096 May 18 10:00 /dev/shm/mytest
cat /dev/shm/mytest | head -c 13
# Hello shared!
# В третьем терминале -- читаем тем же Python API:
python3 -c "
from multiprocessing import shared_memory
shm = shared_memory.SharedMemory(name='mytest')
print('Reader sees:', bytes(shm.buf[:13]))
shm.close()
"
# Reader sees: b'Hello shared!'
В Python 3.8+ есть multiprocessing.shared_memory — обёртка над POSIX shm. Идеально для передачи numpy-arrays между процессами без копирования. Используется в PyTorch DataLoader, в data-pipelines с зерокопированием.