Learning Platform
Глоссарий Troubleshooting
Урок 08.04 · 22 мин
Начальный
IPCShared MemorymmapPOSIXshm_open

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) нужна.
Сравнение IPC по копированиям
TCP socketProcess A пишет в socket -> kernel buffer (1 копирование) -> сетевой стек обрабатывает -> kernel buffer B (или тот же на same-host) -> Process B читает (2 копирование). Big overhead
Pipe / FIFOA пишет в pipe -> kernel ring buffer (1 копирование) -> B читает из buffer (2 копирование). Меньше overhead чем TCP, но всё ещё копируется
Unix domain socketA пишет -> kernel routes по namespace -> B читает. Одно копирование, без сетевого стека. Лучший выбор для большинства same-host IPC
Shared memoryОбщая физическая страница маплена в адресные пространства A и B. A пишет -- B уже видит. Нулевое копирование. Цена -- synchronization on user side

Цена shared memory — синхронизация. Если оба процесса пишут одновременно в одну ячейку — race condition. Нужны mutex-ы, semaphores или atomic операции. И это уже не «kernel за вас всё делает», это userspace-задача.


POSIX shm_open + mmap — современный путь

POSIX-стандарт определяет API через два шага:

  1. shm_open(name, flags, mode) — создаёт или открывает named shared memory объект. Возвращает file descriptor.
  2. 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-а.

WARNING

Файлы в /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. Концептуально то же самое, но через целочисленные ключи вместо имён.

POSIX vs SysV shared memory
POSIX: shm_openСовременный API. Имена в стиле файлов ('/myshm'). Объекты видны в /dev/shm. Интегрируется с mmap. Рекомендуется для нового кода
ftruncate + mmapПосле shm_open -- ftruncate задаёт размер, mmap MAP_SHARED маппит. Точно такая же mmap-семантика, как для файлов
SysV: shmget(key)Старый API. Ключи -- целые числа (часто генерируются ftok из путей). Объекты видны в ipcs. Используется в PostgreSQL, Oracle, sysV-IPC библиотеках
shmat / shmdtshmat (attach) присоединяет к адресному пространству, shmdt (detach) отсоединяет. shmctl(IPC_RMID) удаляет
# Посмотреть 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

Самая болезненная часть. Два процесса видят одну и ту же память — если оба пишут в одну ячейку, исход непредсказуем. Нужна синхронизация.

Варианты:

  1. POSIX named semaphores (sem_open) — семафоры, привязанные к имени. Видны как файлы в /dev/shm с префиксом sem..
  2. POSIX mutex-ы в shared memory — создать pthread_mutex_t в общей области с атрибутом PTHREAD_PROCESS_SHARED. Любой процесс, маппящий область, получает доступ.
  3. Atomic операции — если данные простые (счётчики, флаги), можно обойтись atomic-ами без mutex-а. C11 _Atomic, Rust std::sync::atomic, Go-каналы (но они не shm-friendly).
  4. 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 с зерокопированием.


Проверка знанийKnowledge check
Архитектор предлагает использовать shared memory для общения между двумя сервисами на одной машине -- producer пишет batch данных, consumer обрабатывает. Какие операционные риски нужно учесть, прежде чем согласиться?
ОтветAnswer
Shared memory мощный, но риски нетривиальные: 1) Crash producer-а: если producer умер посреди записи batch-а, consumer может прочитать частично записанные данные (torn write). Решение -- атомарные swap-ы через double-buffering или версионирование с CAS. 2) Crash consumer-а с локом: если в shared memory есть pthread mutex и consumer crashed с locked mutex -- producer заблокируется навсегда. Решение -- PTHREAD_MUTEX_ROBUST: если процесс умер с lock, следующий lock возвращает EOWNERDEAD и можно сделать pthread_mutex_consistent для recovery. 3) Versioning: если в формате структуры что-то изменится между релизами -- бинарная несовместимость. Нужна версия в заголовке shm и graceful upgrade. 4) Permissions: /dev/shm с режимом 0666 = доступен всем пользователям. Нужен 0600 + правильный owner. 5) Размер /dev/shm: убедиться, что tmpfs хватит -- иначе ftruncate fails с ENOSPC. 6) Cleanup: при abnormal exit shm-объект остаётся в /dev/shm. Нужна систематическая очистка (например, через systemd-tmpfiles или PrivateTmp). 7) Debug: race conditions воспроизводятся плохо, valgrind не работает между процессами -- ставить большое количество integration-тестов с stress-load. 8) Cross-language: если producer/consumer на разных языках (Python+Rust), нужен общий binary contract -- лучше использовать формат типа FlatBuffers/Cap'n Proto, который не зависит от padding и endian. Если требования к latency не критичны (< 1ms) -- начать с unix domain socket: проще, дешевле в эксплуатации, легче отлаживать.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. В чём принципиальное преимущество shared memory над TCP-сокетом для IPC между двумя процессами на одной машине?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4