IPC: Pipes, FIFO, Разделяемая память¶
Межпроцессное взаимодействие (IPC — Inter-Process Communication) в Linux реализуется несколькими механизмами. В этой статье рассмотрим три «классических» вида IPC на основе файловых абстракций и памяти: анонимные pipe, именованные FIFO и разделяемую память.
Сравнение механизмов¶
| Механизм | Именован? | Между любыми процессами? | Скорость | Синхронизация | Передача данных |
|---|---|---|---|---|---|
| Pipe | Нет | Только родственные (fork) | Средняя | Встроена (блокировки) | Поток байт |
| FIFO | Да (файл) | Любые | Средняя | Встроена | Поток байт |
| Unix socket | Да (файл/abstract) | Любые | Средняя | Встроена | Stream / datagram, можно передать FD |
socketpair |
Нет | Только родственные (fork) | Средняя | Встроена | Двунаправленный stream |
| eventfd | Нет (fd) | Через fork / pidfd_getfd |
Очень высокая | Встроена в счётчик | 8-байтный счётчик |
| POSIX mqueue | Да (/name) |
Любые | Средняя | Встроена, приоритеты | Сообщения с приоритетом |
| Shared memory | Да (/dev/shm) |
Любые | Высокая | Нужна явная | Произвольная структура |
| System V IPC | Да (key/id) | Любые | Средняя | Встроена для msg/sem | msg / sem / shm |
Анонимные Pipes¶
Pipe (конвейер) — однонаправленный канал связи между двумя процессами.
Данные, записанные в один конец (fd[1]), читаются с другого (fd[0]).
Ключевые свойства:
- буфер в ядре — обычно 64 KB;
- создаётся через
pipe(2), наследуется дочерними процессами черезfork(2); - при закрытии всех читателей — запись даёт
SIGPIPE/EPIPE.
Pipe: буфер в ядре
Parent process Kernel Child process
┌─────────────┐ ┌────────────────────┐ ┌─────────────┐
│ │ │ pipe buffer │ │ │
│ fd[1] ─────┼──▶ write│ ┌──────────────┐ │ │ │
│ (write end)│ │ │ data data data│──┼──┼──▶ fd[0] │
│ │ │ │ (max ~64 KB) │ │ │ (read end) │
│fd[0]: закрыт│ │ └──────────────┘ │ │fd[1]: закрыт│
└─────────────┘ └────────────────────┘ └─────────────┘
Однонаправленный поток: fd[1] ──▶ буфер ──▶ fd[0]
При пустом буфере: read() блокируется
При полном буфере: write() блокируется
fd[1] закрыт у всех: read() вернёт 0 (EOF)
fd[0] закрыт у всех: write() → SIGPIPE / EPIPE
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
int main() {
int fd[2]; // fd[0] = чтение, fd[1] = запись
pipe(fd);
pid_t pid = fork();
if (pid == 0) {
// Дочерний — читатель
close(fd[1]);
char buf[100];
ssize_t n = read(fd[0], buf, sizeof(buf));
printf("Child received: %.*s\n", (int)n, buf);
close(fd[0]);
return 0;
}
// Родитель — писатель
close(fd[0]);
const char *msg = "Hello from parent!";
write(fd[1], msg, strlen(msg));
close(fd[1]);
waitpid(pid, NULL, 0);
}
Аналог оператора | в bash¶
int fd[2];
pipe(fd);
pid_t pid1 = fork();
if (pid1 == 0) {
close(fd[0]);
dup2(fd[1], STDOUT_FILENO); // stdout → pipe
close(fd[1]);
execlp("echo", "echo", "Hello CAOS", NULL);
}
pid_t pid2 = fork();
if (pid2 == 0) {
close(fd[1]);
dup2(fd[0], STDIN_FILENO); // stdin ← pipe
close(fd[0]);
execlp("wc", "wc", "-w", NULL);
}
close(fd[0]); close(fd[1]);
waitpid(pid1, NULL, 0);
waitpid(pid2, NULL, 0);
// Эквивалентно: echo "Hello CAOS" | wc -w
Broken pipe¶
Ошибка возникает при записи в pipe, у которого нет открытых читателей:
signal(SIGPIPE, SIG_IGN); // игнорировать сигнал
ssize_t n = write(fd[1], buf, size);
if (n == -1 && errno == EPIPE) {
// все читатели закрыты
}
FIFO (именованный pipe)¶
FIFO — именованный pipe, видный как специальный файл в ФС (тип p).
Позволяет общаться любым процессам, не только родственным.
Писатель:
Читатель (блокируется на open, пока нет писателя):
int fd = open("/tmp/my_fifo", O_RDONLY);
char buf[100];
ssize_t n = read(fd, buf, sizeof(buf));
printf("Received: %.*s\n", (int)n, buf);
close(fd);
unlink("/tmp/my_fifo");
Особенности нескольких писателей:
записи размером ≤ PIPE_BUF (обычно 4 KB в Linux) атомарны.
При нескольких читателях каждый байт уходит только одному из них.
Разделяемая память (Shared Memory)¶
Shared memory — самый быстрый IPC: процессы напрямую обращаются к одному физическому региону. Не требует копирования данных через ядро, но требует явной синхронизации.
Shared memory: два процесса видят один физический регион
Process A Physical RAM Process B
┌─────────────────────┐ ┌──────────────────────┐
│ virtual address │ │ virtual address │
│ space │ │ space │
│ ┌───────────────┐ │ ┌─────────────────┐ │ ┌───────────────┐ │
│ │ mmap region │──┼──▶ │ shared memory │ ◀────┼──│ mmap region │ │
│ │ 0x7f... │ │ │ /dev/shm/name │ │ │ 0x7f... │ │
│ └───────────────┘ │ │ (физ. страницы)│ │ └───────────────┘ │
│ │ └─────────────────┘ │ │
│ запись сюда │ ──────────────────────────▶ │видна здесь немедленно│
└─────────────────────┘ └──────────────────────┘
shm_open() + mmap() ──▶ MAP_SHARED mapping ──▶ одни и те же физ. страницы
Синхронизация: mutex / semaphore / atomic — обязательна!
POSIX API (рекомендуется)¶
#include <sys/mman.h>
#include <fcntl.h>
// Писатель
int fd = shm_open("/my_shm", O_CREAT | O_RDWR, 0666);
ftruncate(fd, 1024);
void *addr = mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
strcpy(addr, "Hello!");
munmap(addr, 1024); close(fd);
// Читатель
int fd2 = shm_open("/my_shm", O_RDONLY, 0666);
void *addr2 = mmap(NULL, 1024, PROT_READ, MAP_SHARED, fd2, 0);
printf("%s\n", (char *)addr2);
munmap(addr2, 1024); close(fd2);
shm_unlink("/my_shm");
Компиляция: gcc ... -lrt
Shared memory объекты живут в /dev/shm/.
System V API (устаревший)¶
#include <sys/shm.h>
key_t key = ftok("/tmp/shm_file", 'A');
int shmid = shmget(key, 1024, IPC_CREAT | 0666);
void *addr = shmat(shmid, NULL, 0);
// ... работа с addr ...
shmdt(addr);
shmctl(shmid, IPC_RMID, NULL);
Просмотр состояния¶
ls -la /dev/shm/ # POSIX shared memory
ipcs -m # System V shared memory
cat /proc/<pid>/maps # все маппинги процесса
pmap <pid> # удобный вывод
Unix domain sockets (AF_UNIX)¶
Unix domain sockets — двунаправленный socket-based IPC внутри одного хоста. В отличие от TCP/UDP через 127.0.0.1,
трафик не идёт через сетевой стек: нет IP-заголовков, нет TCP checksum, нет routing, нет фильтрации netfilter уровня IP.
Ядро просто копирует данные из буфера одного socket'а в буфер другого, что даёт ощутимо более низкие latency и более
высокий throughput, чем loopback TCP.
TCP loopback (127.0.0.1) Unix domain socket
┌──────────────┐ ┌──────────────┐
│ application │ │ application │
├──────────────┤ ├──────────────┤
│ socket layer │ │ socket layer │
├──────────────┤ ├──────────────┤
│ TCP │ checksum, seq, ack │ │
├──────────────┤ │ AF_UNIX │
│ IP │ headers, routing │ buffer │ copy_to_buffer()
├──────────────┤ │ │
│ loopback dev │ qdisc, netfilter │ │
└──────────────┘ └──────────────┘
~3-5 µs RTT ~1-2 µs RTT
SOCK_STREAM vs SOCK_DGRAM¶
| Свойство | SOCK_STREAM | SOCK_DGRAM |
|---|---|---|
| Семантика | Поток байт, без границ | Сообщения с границами |
| Соединение | connect/accept |
Без соединения, sendto |
| Гарантия доставки | Внутри хоста — да | Внутри хоста — да |
| Порядок | Сохраняется | Сохраняется |
| Сравнимо с | TCP | UDP |
SOCK_SEQPACKET сочетает оба свойства: соединение + границы сообщений (как SCTP).
Адресация: pathname vs abstract namespace¶
Unix sockets бывают трёх видов адресации:
┌─────────────────────┬─────────────────────────────────────────────┐
│ pathname │ sun_path = "/tmp/my.sock" │
│ (классический) │ создаётся inode в ФС, нужно unlink() │
├─────────────────────┼─────────────────────────────────────────────┤
│ abstract namespace │ sun_path[0] = '\0', далее имя │
│ (Linux-specific) │ не появляется в ФС, исчезает с процессом │
├─────────────────────┼─────────────────────────────────────────────┤
│ unnamed │ bind не вызывался; ядро присвоит при первом│
│ │ send (autobind в abstract namespace) │
└─────────────────────┴─────────────────────────────────────────────┘
Abstract socket в ss -x отображается как @name.
#include <sys/socket.h>
#include <sys/un.h>
#include <string.h>
struct sockaddr_un addr = {0};
addr.sun_family = AF_UNIX;
// pathname:
strncpy(addr.sun_path, "/tmp/my.sock", sizeof(addr.sun_path) - 1);
// abstract namespace (Linux):
addr.sun_path[0] = '\0';
strncpy(addr.sun_path + 1, "my.sock", sizeof(addr.sun_path) - 2);
socklen_t len = offsetof(struct sockaddr_un, sun_path) + 1 + strlen("my.sock");
socketpair для родственных процессов¶
socketpair(2) создаёт сразу два соединённых socket'а — аналог pipe(2), но двунаправленный и доступен для
SOCK_STREAM/SOCK_DGRAM/SOCK_SEQPACKET. Передаётся в дочерний процесс через fork().
#include <sys/socket.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int sv[2];
socketpair(AF_UNIX, SOCK_STREAM, 0, sv);
if (fork() == 0) {
close(sv[0]);
write(sv[1], "ping", 4);
char buf[16] = {0};
read(sv[1], buf, sizeof(buf));
printf("child got: %s\n", buf);
return 0;
}
close(sv[1]);
char buf[16] = {0};
read(sv[0], buf, sizeof(buf));
printf("parent got: %s\n", buf);
write(sv[0], "pong", 4);
wait(NULL);
}
Передача file descriptors через SCM_RIGHTS¶
Уникальная возможность Unix domain sockets: переслать открытый file descriptor от одного процесса другому.
Получающий процесс получает свой собственный FD, указывающий на тот же struct file в ядре — refcount увеличивается,
как при dup/fork. На этом построены protocol activation (systemd), демоны вроде pulseaudio, X server passing FDs,
container runtimes.
Process A kernel Process B
┌──────────┐ ┌──────────────────┐ ┌──────────┐
│ fd = 5 │────────▶│ struct file * │◀────────│ fd = 7 │
└──────────┘ │ refcount: 2 │ └──────────┘
└──────────────────┘
sendmsg(SCM_RIGHTS, fd=5) ──▶ recvmsg() → fd=7
(FD номера не совпадают — ядро находит свободный slot у B)
Передача через sendmsg/recvmsg с control message типа SCM_RIGHTS:
#include <sys/socket.h>
#include <sys/un.h>
#include <string.h>
void send_fd(int sock, int fd) {
struct msghdr msg = {0};
char buf[CMSG_SPACE(sizeof(int))] = {0};
char dummy = 'x';
struct iovec iov = { .iov_base = &dummy, .iov_len = 1 };
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = buf;
msg.msg_controllen = sizeof(buf);
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
memcpy(CMSG_DATA(cmsg), &fd, sizeof(int));
sendmsg(sock, &msg, 0);
}
int recv_fd(int sock) {
struct msghdr msg = {0};
char buf[CMSG_SPACE(sizeof(int))] = {0};
char dummy;
struct iovec iov = { .iov_base = &dummy, .iov_len = 1 };
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = buf;
msg.msg_controllen = sizeof(buf);
recvmsg(sock, &msg, 0);
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
int fd;
memcpy(&fd, CMSG_DATA(cmsg), sizeof(int));
return fd;
}
Сопутствующее control message SCM_CREDENTIALS (опция сокета SO_PASSCRED) даёт получателю PID/UID/GID отправителя —
kernel сам проверяет credentials, обмануть нельзя.
eventfd: счётчик в ядре для сигнализации¶
eventfd(2) создаёт файловый дескриптор, под которым в ядре живёт 64-битный беззнаковый счётчик. write() атомарно
прибавляет значение, read() возвращает текущее значение и обнуляет его (или вычитает 1 в режиме EFD_SEMAPHORE). Если
счётчик == 0, read() блокируется (или возвращает EAGAIN в non-blocking режиме).
eventfd: 8-байтный счётчик в kernel
thread A / process A kernel thread B / process B
┌──────────────────┐ ┌──────────────────┐
│ write(efd, &v) │──▶ counter += v │ │
│ v = 1 │ ┌────────────────┐ │ │
└──────────────────┘ │ uint64_t = N │ │ read(efd, &out) │
└────────────────┘ ──▶ │ out = N │
│ │ counter = 0 │
▼ └──────────────────┘
блокирует read'еров,
если counter == 0
EFD_SEMAPHORE: read() возвращает 1, counter -= 1 (как семафор)
EFD_NONBLOCK: read/write возвращают EAGAIN вместо блокировки
EFD_CLOEXEC: закрывается при exec
eventfd дешевле pipe для простой сигнализации: один счётчик, один syscall с фиксированными 8 байтами, ядро не выделяет
буфер. Хорошо комбинируется с epoll/poll/select — становится readable, как только counter > 0.
#include <sys/eventfd.h>
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdint.h>
int efd;
void *worker(void *arg) {
sleep(1);
uint64_t v = 1;
write(efd, &v, sizeof(v)); // сигнализируем main
return NULL;
}
int main() {
efd = eventfd(0, EFD_CLOEXEC);
pthread_t tid;
pthread_create(&tid, NULL, worker, NULL);
uint64_t out;
read(efd, &out, sizeof(out)); // блокируется до write от worker
printf("got %lu\n", (unsigned long)out);
pthread_join(tid, NULL);
close(efd);
}
Типичные применения: wakeup для event loop, готовность данных в lock-free очереди, cross-thread cancellation (один поток
write(efd, 1), остальные ловят через epoll).
System V IPC¶
System V IPC — три семейства legacy-механизмов из SysV Unix: message queues (msg*), semaphores (sem*), shared
memory (shm*). API получает ключ через ftok(), возвращает целочисленный id; объекты живут в ядре до *ctl(IPC_RMID)
или перезагрузки.
#include <sys/ipc.h>
#include <sys/shm.h>
key_t key = ftok("/some/file", 'A');
int shmid = shmget(key, 4096, IPC_CREAT | 0666);
void *p = shmat(shmid, NULL, 0);
// ...
shmdt(p);
shmctl(shmid, IPC_RMID, NULL);
Просмотр и удаление из shell: ipcs, ipcrm. Без явного удаления объекты остаются в ядре — типичная утечка ресурсов в
старых сервисах.
POSIX vs System V¶
| Аспект | POSIX (mq_*, sem_*, shm_*) |
System V (msgget, semget, shmget) |
|---|---|---|
| Именование | Строка /name |
key_t через ftok() |
| Тип handle | File descriptor | Целочисленный id |
| Интеграция с poll/epoll | Да (это обычный fd) | Нет |
| Жизненный цикл | Reference counting + *_unlink |
Persistent до IPC_RMID |
| Просмотр в ФС | /dev/shm/, /dev/mqueue/ |
Только ipcs |
| Семафоры | sem_post/sem_wait, один счётчик |
Массив семафоров, atomic многооперационные semop |
| Поддержка | POSIX.1-2001 | SUSv2, в Linux считается legacy |
| Когда выбирать | Новый код | Совместимость со старыми системами |
POSIX API в Linux чище, ложится на event-driven код (fd-based) и не оставляет за собой объекты при падении. System V
встречается в oracle, postgresql (исторически — shm для shared buffers), некоторых HPC-приложениях.
Связанные темы¶
- Сигналы — сигналы как простейший механизм межпроцессного уведомления (
SIGUSR1,SIGPIPE) - mmap и маппинг файлов — основа для POSIX shared memory
- fork и exec — наследование дескрипторов pipe через fork
Источники¶
man 2 pipe,man 7 pipeman 3 mkfifo,man 7 fifoman 3 shm_open,man 2 mmap,man 7 shm_overviewman 7 mq_overview— POSIX message queuesman 7 unix— UNIX domain sockets, abstract namespace, SCM_RIGHTSman 2 socketpair,man 3 cmsgman 2 eventfd,man 7 eventfdman 7 sysvipc,man 2 shmget,man 2 msgget,man 2 semget- Linux kernel: pipe(7)
- Beej's Guide to Unix IPC