IPC в C++: как выбрать механизм и не наступить на грабли
Разбираем четыре способа обмена данными между процессами в C++ на Linux: pipe, разделяемая память, сокеты и очереди сообщений. У каждого своя цена — задержка, объём кода и режимы отказа.

Задача звучит просто: два процесса на одной машине должны обмениваться данными. Один пишет, другой читает. Но между «работает на демо» и «работает под нагрузкой в проде» лежит выбор механизма межпроцессного взаимодействия (IPC), и цена ошибки здесь — не переписанная функция, а зависшие процессы, потерянные сообщения и утечка сегментов разделяемой памяти, которые переживают перезапуск сервиса. Ниже — четыре рабочих подхода на Linux с C++17 и старше, с ценой каждого.
Статья про выбор и типичные ошибки, а не про полное API каждого механизма. Сетевое взаимодействие между машинами — отдельная тема, здесь только локальный IPC на одном хосте.
Что вообще выбирается
Механизмов в POSIX больше, чем кажется, но на практике вы почти всегда упираетесь в один из четырёх вариантов. Выбор определяют три вопроса: сколько данных, какая допустимая задержка и переживёт ли канал падение одного из процессов.
- Анонимный/именованный pipe (FIFO) — поток байтов, односторонний, простой. Хорош для «родитель запустил ребёнка и читает его вывод».
- Разделяемая память (POSIX shm) — самый быстрый способ, оба процесса видят один участок памяти. Но синхронизацию вы пишете сами.
- Unix domain socket — как TCP, но локально: двусторонний, с границами сообщений (в режиме SOCK_DGRAM/SOCK_SEQPACKET), передаёт файловые дескрипторы.
- POSIX message queue — очередь сообщений с приоритетами и фиксированным максимальным размером сообщения.
Разделяемая память быстрее сокета не потому, что «шустрее работает», а потому что данные не копируются в ядро и обратно. За эту скорость вы платите тем, что синхронизацию — кто пишет, кто читает, где граница сообщения — пишете руками.
Pipe: когда обмен односторонний и потоковый
FIFO создаётся один раз, дальше это обычный файл, в который один процесс пишет, другой читает. Подходит для логов, стриминга, конвейеров. Не подходит, если нужны границы сообщений: pipe отдаёт байты, и склеивание двух записей в одно чтение — это норма, а не баг.
// C++17, Linux. Писатель в именованный канал.
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <cstring>
#include <string>
int main() {
const char* fifo_path = "/tmp/demo_fifo";
// Создаём FIFO, если его ещё нет. EEXIST — не ошибка.
if (mkfifo(fifo_path, 0666) == -1 && errno != EEXIST) {
perror("mkfifo");
return 1;
}
// open заблокируется, пока читатель не откроет канал.
int fd = open(fifo_path, O_WRONLY);
if (fd == -1) { perror("open"); return 1; }
std::string msg = "hello from writer\n";
ssize_t written = write(fd, msg.data(), msg.size());
if (written == -1) perror("write");
close(fd);
return 0;
}
Проверка: запустите cat /tmp/demo_fifo в одном терминале, программу — в другом. В первом появится строка hello from writer.
Главная ловушка pipe: если писатель шлёт в канал, у которого никто не открыт на чтение, процесс получит сигнал SIGPIPE и по умолчанию упадёт. Игнорируйте сигнал через signal(SIGPIPE, SIG_IGN) и проверяйте, что write вернул -1 с errno == EPIPE.
Разделяемая память: скорость ценой ручной синхронизации
POSIX shared memory открывается через shm_open, размер задаётся ftruncate, отображается в адресное пространство через mmap. Оба процесса работают с одним участком RAM без копирования — отсюда минимальная задержка. Но два процесса пишут в одну память одновременно, и без синхронизации вы получите гонку данных.
Синхронизировать нужно межпроцессным примитивом. Обычный std::mutex здесь не работает — он живёт в памяти одного процесса. Нужен либо POSIX-семафор (sem_open), либо pthread_mutex с атрибутом PTHREAD_PROCESS_SHARED, размещённый внутри того же сегмента shm.
// C++17, Linux. Создание и отображение сегмента разделяемой памяти.
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <cstdio>
#include <cstring>
struct SharedData {
int ready;
char payload[256];
};
int main() {
const char* shm_name = "/demo_shm";
int fd = shm_open(shm_name, O_CREAT | O_RDWR, 0666);
if (fd == -1) { perror("shm_open"); return 1; }
// Задаём размер сегмента до отображения.
if (ftruncate(fd, sizeof(SharedData)) == -1) {
perror("ftruncate");
return 1;
}
void* addr = mmap(nullptr, sizeof(SharedData),
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED) { perror("mmap"); return 1; }
auto* data = static_cast<SharedData*>(addr);
std::strncpy(data->payload, "shared payload", sizeof(data->payload) - 1);
data->ready = 1; // синхронизация опущена ради краткости примера
// shm_unlink удаляет имя; сегмент живёт, пока открыт хоть один mmap.
// Не вызывайте его тут, если читатель ещё не подключился.
munmap(addr, sizeof(SharedData));
close(fd);
return 0;
}
Проверка: после запуска сегмент виден в /dev/shm/demo_shm. Это же — источник самой частой ошибки: если процесс упал, не вызвав shm_unlink, сегмент остаётся висеть и займёт место после перезапуска. Разделяемая память переживает смерть процесса, в отличие от pipe.
В примере выше data->ready = 1 без барьера и без семафора — это гонка. В боевом коде читатель должен ждать на семафоре или на pthread_cond с process-shared атрибутом, иначе компилятор и процессор переупорядочат записи, и читатель увидит ready == 1 раньше, чем в payload окажутся данные.
Unix domain socket: двусторонний обмен с границами сообщений
Если нужен диалог в обе стороны, а не поток в одну, и вы хотите сохранить границы сообщений — берите Unix domain socket. По сравнению с pipe он двусторонний, поддерживает несколько клиентов и умеет передавать открытые файловые дескрипторы между процессами через вспомогательные сообщения (SCM_RIGHTS). Задержка выше, чем у shm, но кода на синхронизацию писать не нужно: ядро гарантирует целостность.
Режим SOCK_STREAM ведёт себя как pipe — поток байтов без границ. Если хотите, чтобы одно отправленное сообщение читалось как одно, используйте SOCK_SEQPACKET или SOCK_DGRAM. Это частая причина «а почему у меня два JSON склеились в один буфер» — выбран stream, а рассчитывали на датаграммы.
POSIX message queue: очередь с приоритетами
Очередь сообщений (mq_open, mq_send, mq_receive) подходит, когда сообщения дискретны, у них есть приоритет и вы хотите, чтобы система буферизовала их без вашего участия. Максимальный размер сообщения и глубина очереди фиксируются при создании. Ограничения по умолчанию жёсткие: и максимум сообщений в очереди, и максимальный размер сообщения задаются системными лимитами, которые видны в /proc/sys/fs/mqueue/. Точные значения зависят от дистрибутива и настроек — сверяйтесь на своей машине, не закладывайте константу вслепую.
Сравнение: что выбрать под задачу
| Механизм | Направление | Границы сообщений | Скорость | Переживает падение процесса |
|---|---|---|---|---|
| Pipe / FIFO | Одностороннее | Нет (поток байтов) | Средняя | Нет |
| Разделяемая память | Двустороннее | Пишете сами | Максимальная | Да, имя висит до shm_unlink |
| Unix domain socket | Двустороннее | Есть в SEQPACKET/DGRAM | Ниже shm | Нет |
| POSIX message queue | Двустороннее | Есть | Средняя | Да, до mq_unlink |
Когда что выбирать
- Родитель читает вывод дочернего процесса, поток данных. Pipe. Ничего сложнее не нужно.
- Десятки тысяч сообщений в секунду, задержка критична, оба процесса на одной машине. Разделяемая память с семафором. Готовьтесь писать синхронизацию и уборку сегментов.
- Двусторонний диалог, несколько клиентов, нужны границы сообщений или передача дескрипторов. Unix domain socket. Плюс: код почти совпадает с сетевым, потом легко вынести на TCP.
- Дискретные сообщения с приоритетами, буферизация со стороны системы. POSIX message queue.
Критерий грубо такой: если задержка не в топе требований — не связывайтесь с разделяемой памятью. Сокет даёт 90% удобства при 10% сложности синхронизации.
Что ломается в проде
- SIGPIPE роняет процесс. Писатель в закрытый pipe или сокет получает сигнал. Игнорируйте
SIGPIPEглобально и обрабатывайтеEPIPEизwrite. - Утечка именованных объектов. Сегменты shm и очереди сообщений переживают процесс. Если сервис падает без
shm_unlink/mq_unlink, объекты копятся. Вешайте уборку на обработчик сигналов и на старте чистите объекты с известным именем. - Гонка данных в разделяемой памяти. Запись без барьера памяти и синхронизации: читатель видит флаг готовности раньше самих данных. Всегда синхронизируйтесь process-shared примитивом.
- Частичное чтение/запись.
writeиreadмогут обработать меньше байтов, чем запрошено. Оборачивайте в цикл, пока не передадите весь буфер. - Права доступа. Если процессы под разными пользователями, маска
0666и umask решают, откроется ли объект. Нехватка прав выглядит какEACCESпри открытии, а не как ошибка логики.
Что делать дальше
Определите профиль нагрузки прежде, чем писать код: объём одного сообщения, частота, допустимая задержка. Под большинство задач начинайте с Unix domain socket — он даёт двусторонний обмен без ручной синхронизации, а код переносим на сеть. К разделяемой памяти переходите, только когда замеры показали, что копирование через ядро — узкое место.
Измеряйте задержку между отправкой и приёмом под реальной параллельностью, а не на одном сообщении. Смотрите за размером /dev/shm и содержимым ls /dev/mqueue в проде — рост означает утечку. Для деталей API сверяйтесь с man-страницами shm_open(3), mq_overview(7), unix(7): сигнатуры и флаги стабильны, но лимиты зависят от системы.
Создатель презентаций: Структура и текст каждого слайда: заголовки-утверждения, одна мысль на слайд, заметки докладчику.
Спросить за 150 ₽Источники: Linux man-pages: shm_open(3), Linux man-pages: unix(7)
Частые вопросы
У меня не Linux, а Windows — этот код подойдёт?
Можно ли передавать между процессами std::string или std::vector напрямую через разделяемую память?
Что делать, если процесс упал и оставил висеть сегмент shm?
Сокет медленнее разделяемой памяти — насколько это критично?
Почему мои сообщения через сокет склеиваются в одно чтение?
Нужен ли отдельный примитив синхронизации, если через shm пишет только один процесс, а читает один?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.