Операционные системы
Межпроцессное взаимодействие (IPC)
Процессы изолированы друг от друга - это фундамент безопасности OS. Но что, если им нужно сотрудничать? Browser передаёт URL renderer процессу, database отправляет результаты query executor'у, Docker CLI управляет daemon'ом. Все современные системы - это ансамбль взаимодействующих процессов. IPC (Inter-Process Communication) - язык, на котором они общаются. Понимание IPC - это понимание архитектуры реального ПО.
- **Chrome multi-process architecture:** Каждая вкладка - отдельный renderer process (изоляция). Browser process координирует через **shared memory** (для bitmap'ов страниц) и **message passing** (для команд). Crash одной вкладки не роняет браузер.
- **Docker daemon communication:** `docker run` использует **Unix domain socket** (/var/run/docker.sock) для отправки команд daemon'у. Это безопаснее TCP (file permissions), быстрее (нет network stack), и стандартный паттерн для system services.
- **PostgreSQL shared buffer pool:** Все backend процессы (обслуживающие клиентов) работают с единым **shared memory** сегментом (gigabytes). Одна страница данных, загруженная с диска, мгновенно доступна всем - критично для производительности.
Цели урока
- Применять механизмы IPC: pipes (anonymous/named), shared memory, message queues, sockets
- Понимать когда какой: shared memory быстрее, sockets для cross-host, pipes для shell
- Знать SysV vs POSIX IPC; ftok, shmget, shm_open
- Оценить cost: pipe ~1µs, shared memory ~50ns, Unix domain socket ~5µs
- Применять Unix domain socket для local IPC вместо TCP loopback
Pipes - конвейеры данных
**Pipe (конвейер)** - простейший механизм IPC, позволяющий передавать данные между процессами как поток байтов. Pipe - это однонаправленный канал: один процесс пишет, другой читает. В UNIX pipes - фундамент философии композиции программ.
Shell pipes - конвейер команд
При выполнении `cat file.txt | grep error | wc -l` shell создаёт **три процесса** и **два pipe**. Stdout процесса `cat` соединён pipe'ом с stdin процесса `grep`, а его stdout - с stdin процесса `wc`. Данные текут как по трубопроводу, процессы работают параллельно.
Pipe создаётся системным вызовом `pipe()`, который возвращает **два файловых дескриптора**: один для чтения (pipe[0]), другой для записи (pipe[1]). После `fork()` оба процесса получают копии этих дескрипторов - и могут общаться.
**Важные свойства pipes:** - **FIFO порядок:** данные читаются в той же последовательности, в которой записаны - **Атомарность:** запись < PIPE_BUF байт (обычно 4096) атомарна - не перемешается с другими записями - **Блокирующий I/O:** `read()` ждёт данных, `write()` ждёт места в буфере - **Закрытие:** если все write ends закрыты, `read()` вернёт EOF (0 байт)
**Named pipes (FIFO)** - расширение концепции. Обычный pipe существует только между родственными процессами (parent-child). FIFO - это **файл в файловой системе**, но работает как pipe. Любые процессы могут открыть его и обмениваться данными.
Redis использует pipes для worker communication
В Redis, когда main thread получает команду BGSAVE (background save), он создаёт child process через `fork()`. Child пишет snapshot RDB на диск, а progress сообщает родителю через **pipe**. Main thread читает pipe без блокировки (O_NONBLOCK), обновляет статистику. Это позволяет асинхронно сохранять данные без остановки сервера.
Что произойдёт, если процесс попытается прочитать из pipe, в который никто не пишет (все write ends закрыты)?
Shared Memory - общая память
**Shared Memory** - самый быстрый механизм IPC. Идея проста: два процесса **отображают один и тот же участок физической памяти** в свои виртуальные адресные пространства. Запись в этой области одним процессом мгновенно видна другому - без копирования через ядро.
В POSIX есть два API для shared memory: 1. **System V IPC** (`shmget()`, `shmat()`) - старый интерфейс, используется в legacy системах 2. **POSIX shared memory** (`shm_open()`, `mmap()`) - современный, файл-ориентированный подход
**Производительность:** shared memory избегает копирования данных между user space и kernel space. В pipe или socket данные копируются дважды: из процесса A в kernel buffer, затем из kernel buffer в процесс B. В shared memory - ноль копирований.
**Проблема: синхронизация!** Shared memory даёт скорость, но не защищает от race conditions. Если два процесса одновременно пишут в одну ячейку, результат непредсказуем. Нужна синхронизация через **мьютексы (pthread_mutex)** или **семафоры (sem_t)**.
PostgreSQL использует shared memory для buffer cache
PostgreSQL создаёт большой shared memory сегмент (gigabytes) для **shared buffer pool** - кеша страниц данных. Все backend процессы (обслуживающие клиентов) отображают этот сегмент в свою память. Когда один процесс загружает страницу с диска в buffer, она мгновенно доступна всем остальным. Это даёт огромный прирост производительности по сравнению с per-process кешами.
Redis shared memory в master-replica communication
В Redis replication, когда master получает WRITE команду, он добавляет её в **replication buffer** в shared memory. Replica процессы читают этот buffer и применяют изменения. Использование shared memory вместо сокетов даёт latency < 1μs для локальных реплик.
Почему shared memory считается самым быстрым механизмом IPC, но требует дополнительной синхронизации?
Message Queues - очереди сообщений
**Message Queue** - структурированный IPC механизм, где процессы обмениваются **сообщениями** (не просто байтами). Каждое сообщение имеет тип и данные. Очередь хранится в ядре, процессы могут читать/писать сообщения асинхронно, независимо друг от друга.
POSIX message queues (`mq_open()`, `mq_send()`, `mq_receive()`) предоставляют более богатый интерфейс, чем System V. Поддерживают **приоритеты сообщений** и **асинхронные уведомления** (через сигналы или threads).
**Отличия message queues от pipes:** - **Структура:** message queue передаёт дискретные сообщения, pipe - поток байтов - **Приоритеты:** message queue поддерживает приоритеты, pipe - строго FIFO - **Блокировка:** message queue может работать в неблокирующем режиме (O_NONBLOCK), как и pipe, но также поддерживает timeouts (mq_timedreceive) - **Размер:** message queue ограничена по количеству/размеру сообщений, pipe имеет буфер фиксированного размера
**Асинхронные уведомления** - мощная фича POSIX message queues. Процесс может зарегистрировать callback, который вызовется, когда в пустую очередь придёт сообщение. Это избавляет от активного polling.
systemd использует message queues для service communication
В systemd каждый сервис может посылать structured messages (с метаданными: timestamp, priority, service name) в центральный journal daemon (systemd-journald). Это реализовано через POSIX message queues. Journald обрабатывает сообщения асинхронно, сервисы не блокируются на логировании.
Real-world: task dispatcher pattern
Классический паттерн: **producer-consumer с приоритетами**. Web-сервер получает HTTP запросы, каждый оборачивает в сообщение с приоритетом (зависит от URL: /api/critical - высокий, /api/batch - низкий) и кладёт в message queue. Worker процессы читают из очереди - критичные задачи обрабатываются первыми, даже если пришли позже.
В чём ключевое преимущество POSIX message queues перед обычными pipes для реализации task queue с приоритетами?
Unix Domain Sockets
**Unix Domain Sockets** - самый гибкий механизм IPC. Это сокеты (как для TCP/IP), но работающие **локально, внутри одной машины**. Они поддерживают как потоковую передачу (SOCK_STREAM, аналог TCP), так и датаграммы (SOCK_DGRAM, аналог UDP), но без overhead сетевого стека.
Unix domain sockets адресуются через **файлы в файловой системе** (например, `/var/run/docker.sock`). Это даёт file permissions для контроля доступа: `chmod 600 my.sock` - только владелец может подключиться.
**Почему Unix sockets быстрее TCP loopback (127.0.0.1)?** - **Нет сетевого стека:** пропускаются TCP checksum, routing, congestion control - **Zero-copy в kernel:** современные ядра используют sendfile() под капотом - **Меньше контекстных переключений:** оптимизированный путь в ядре Benchmark: Unix socket ~2x быстрее TCP loopback для локального IPC
**Передача файловых дескрипторов** - уникальная фича Unix domain sockets. Процесс может передать открытый файловый дескриптор другому процессу через socket. Это позволяет, например, master процессу принимать connections, а затем "передавать" сокет worker процессу для обработки.
Docker использует Unix socket для API
Docker daemon слушает на `/var/run/docker.sock` (Unix domain socket). При запуске `docker run` CLI подключается к этому сокету и отправляет JSON команды через HTTP over Unix socket. Это безопаснее TCP (нет доступа извне), быстрее (нет сетевого overhead), и проще контролировать через file permissions.
Nginx master-worker architecture
Nginx master процесс создаёт listening socket (например, на порту 80), затем `fork()` создаёт worker процессы. Все worker'ы **наследуют** этот socket FD. Когда приходит connection, ядро будит одного из workers (через accept() mutex). Альтернативно, master может передать новый connection FD конкретному worker через Unix domain socket (SO_REUSEPORT стратегия).
X11 window system и Wayland
Графический сервер X11 слушает на `/tmp/.X11-unix/X0` (Unix socket). Каждое GUI приложение подключается к этому сокету, отправляет команды рисования ("draw window", "update pixel"), получает события (клики мыши, нажатия клавиш). Wayland использует аналогичный подход через Unix sockets для compositor-client communication.
Shared memory всегда лучший выбор для IPC, т.к. самая быстрая - нужно использовать её везде
Выбор IPC механизма зависит от требований: скорость vs структура vs надёжность. Нет универсального решения
Shared memory действительно самая быстрая (zero-copy), но требует сложной синхронизации (мьютексы, семафоры) и подвержена race conditions. Для простых pipe-линий (cat | grep) pipes идеальны. Для структурированных задач с приоритетами - message queues. Для client-server архитектуры - Unix sockets (+ file permissions). Для огромных объёмов данных между тесно связанными процессами - shared memory. **Пример из практики:** PostgreSQL использует shared memory для buffer pool (миллионы обращений в секунду), но Unix sockets для client connections (структурированный протокол, authentication). Правильный IPC - это trade-off между производительностью, простотой, надёжностью и безопасностью.
Ключевые идеи
- **Pipes - простота и композиция.** Однонаправленный поток байтов, FIFO порядок. Идеален для pipeline (cat | grep | wc). Named pipes (FIFO) работают между любыми процессами. Базовый блок UNIX философии.
- **Shared Memory - максимальная скорость.** Одна физическая память, отображённая в адресные пространства процессов. Zero-copy, но требует синхронизации (мьютексы, семафоры). Для высоконагруженных систем (PostgreSQL buffer pool, Redis replication buffer).
- **Message Queues - структурированность.** Передача дискретных сообщений с приоритетами. POSIX mq поддерживает асинхронные уведомления. Паттерн producer-consumer с task priorities.
- **Unix Domain Sockets - гибкость и мощь.** Client-server архитектура через файловую систему. File permissions для контроля доступа. Уникальная фича: передача файловых дескрипторов между процессами. Быстрее TCP loopback на ~2x. Docker, X11, systemd - все используют Unix sockets.
Связанные темы
IPC - пересечение многих областей Computer Science:
- Процессы и потоки — IPC нужен для коммуникации между изолированными процессами. Threads в одном процессе используют shared memory напрямую (без IPC)
- Синхронизация — Shared memory требует мьютексов/семафоров для защиты от race conditions. Pipes/sockets - kernel гарантирует атомарность операций
- Файловые системы — Named pipes и Unix domain sockets адресуются через FS. POSIX shared memory использует tmpfs (/dev/shm). File permissions контролируют доступ
- Системные вызовы — Все IPC операции - системные вызовы: pipe(), mmap(), mq_send(), sendmsg(). Переключение в kernel mode для безопасности
Вопросы для размышления
- Система real-time video processing (низкая latency критична): какой IPC механизм предпочесть для передачи frames между capture процессом и encoder процессом? Почему?
- Docker daemon может слушать на TCP порту (tcp://0.0.0.0:2375) или Unix socket (/var/run/docker.sock). В production почти всегда используют Unix socket. Какие security risks несёт TCP вариант?
- Chrome использует shared memory для передачи rendered bitmap'ов от renderer процесса к browser процессу (для отображения на экране). Почему нельзя использовать pipes или sockets для этой задачи? (Hint: размер данных, latency)
- В каких сценариях message queue с приоритетами лучше, чем простой pipe? Приведи пример системы, где это критично.
Связанные уроки
- os-03-threads — IPC решает проблемы коммуникации между процессами, а не потоками
- os-05-sync — Примитивы синхронизации используются внутри механизмов IPC
- net-15-tcp-basics — Сокеты - IPC через сеть, та же семантика
- os-17-locks-advanced — Продвинутые lock-механизмы строятся поверх IPC примитивов
- net-54-rpc
- net-55-message-queues