Сигналы, очереди сообщений и семафоры это три классических механизма межпроцессного взаимодействия (IPC) в Linux. Они пришли из UNIX, живут в ядре и до сих пор нужны в демонах, контейнерах, systemd и скриптах. Ниже коротко история, как это устроено в ядре, чем отличаются System V и POSIX, и рабочие примеры на C.
Откуда это всё взялось
- Сигналы есть в UNIX с ранних версий 1970-х: способ сообщить процессу о событии (SIGINT, SIGTERM, SIGCHLD и другие).
- Семафоры придумал Эдсгер Дейкстра в 1960-х: счётчик и две операции, P (уменьшить или ждать) и V (увеличить и разбудить).
- Очереди сообщений, семафоры и разделяемая память вошли в UNIX System V в 1983 году. Это и есть System V IPC с ключами key_t.
- Расширения реального времени POSIX.1b (1993) добавили сигналы реального времени, POSIX-очереди и POSIX-семафоры с более простым API.
В Linux живут оба мира: System V IPC для совместимости и старого софта, POSIX для нового кода.
Сигналы
Жизненный цикл сигнала: его генерируют (kill, sigqueue, ядро при ошибке), он ждёт в очереди ожидающих сигналов задачи, затем доставляется, и выполняется действие: по умолчанию (SIG_DFL), игнорирование (SIG_IGN) или свой обработчик. Код ядра лежит в kernel/signal.c.
Стандартных сигналов 31, а с Linux 2.2 есть ещё сигналы реального времени. По signal(7) ядро поддерживает 33 таких сигнала с номерами от 32 до 64, но glibc забирает два-три из них под свои нужды, поэтому в программах нужно использовать SIGRTMIN+n, а не жёсткие номера. В отличие от обычных, сигналы реального времени встают в очередь: если прилетело пять одинаковых, обработчик вызовется пять раз.
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
static void handler(int sig) {
/* printf в обработчике небезопасен, для примера сойдёт */
printf("Сигнал %d\n", sig);
}
int main(void) {
struct sigaction sa = {0};
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sigaction(SIGUSR1, &sa, NULL);
printf("PID: %d\n", getpid());
for (;;)
pause();
}
В другом терминале: kill -USR1 <pid>. В реальном коде используйте sigaction, а не signal: поведение предсказуемее, а маски задаются явно.
Очереди сообщений: System V и POSIX
Очереди System V создаются через msgget по ключу, сообщения отправляются msgsnd и забираются msgrcv с выбором по полю mtype. Код в ipc/msg.c. POSIX-очереди появились в Linux 2.6.6 и устроены проще: mq_open по имени вида /name, mq_send с приоритетом, mq_receive забирает самое приоритетное сообщение, mq_notify присылает уведомление. Код в ipc/mqueue.c.
По mq_overview(7) приоритеты идут от 0 до sysconf(_SC_MQ_PRIO_MAX) минус 1, и в Linux это 32768 значений, хотя POSIX требует минимум 32 (от 0 до 31). Очереди можно увидеть как файлы, если смонтировать файловую систему mqueue в /dev/mqueue, а лимиты лежат в /proc/sys/fs/mqueue/ (msg_max, msgsize_max, queues_max).
Отправитель:
#include <mqueue.h>
#include <fcntl.h>
#include <sys/stat.h>
int main(void) {
struct mq_attr attr = { .mq_maxmsg = 10, .mq_msgsize = 128 };
mqd_t mq = mq_open("/myqueue", O_CREAT | O_WRONLY, 0644, &attr);
mq_send(mq, "High priority", 14, 10);
mq_close(mq);
return 0;
}
Получатель:
#include <mqueue.h>
#include <stdio.h>
int main(void) {
mqd_t mq = mq_open("/myqueue", O_RDONLY);
char buf[128];
unsigned int prio;
mq_receive(mq, buf, sizeof buf, &prio);
printf("Сообщение: %s, приоритет: %u\n", buf, prio);
mq_close(mq);
mq_unlink("/myqueue");
return 0;
}
Собирать с -lrt. Размер буфера в mq_receive должен быть не меньше mq_msgsize очереди, иначе вызов вернёт ошибку EMSGSIZE.
Семафоры: System V и POSIX
Семафоры System V это наборы: semget создаёт массив, semop выполняет сразу несколько операций атомарно, semctl управляет. Есть флаг SEM_UNDO, который откатывает изменения, если процесс упал. Код в ipc/sem.c.
POSIX-семафоры проще: один sem_t и операции sem_wait, sem_post, sem_timedwait. Именованные создаются через sem_open(“/name”, …), и, как пишет sem_overview(7), в Linux они лежат в /dev/shm с префиксом sem. Неименованные создаются sem_init в памяти, общей для потоков или процессов. Под капотом у POSIX-семафоров glibc и futex, код которого в ядре теперь лежит в отдельном каталоге kernel/futex/. Собирать с -pthread.
#include <semaphore.h>
#include <fcntl.h>
#include <stdio.h>
int main(void) {
sem_t *s = sem_open("/mysem", O_CREAT, 0644, 1);
sem_wait(s);
/* критическая секция */
printf("critical\n");
sem_post(s);
sem_close(s);
return 0;
}
Где это встречается в DevOps
- Сигналы: перечитать конфиг по SIGHUP (nginx, многие демоны), остановка контейнера (docker stop шлёт SIGTERM, а через 10 секунд SIGKILL), KillSignal в unit-файлах systemd.
- Процесс с PID 1 в контейнере не получает обработчики по умолчанию, поэтому без своего обработчика или init вроде tini SIGTERM может просто игнорироваться.
- Очереди: локальный обмен между воркерами без брокера и сети.
- Семафоры: ограничение параллелизма и координация процессов на одной машине.
В Kubernetes и systemd вы чаще видите сигналы и лимиты cgroup, чем сырые System V IPC. Но старые приложения, СУБД и часть сервисов на C и C++ до сих пор держат очереди, семафоры и разделяемую память. Посмотреть их помогают ipcs, удалить зависшие объекты ipcrm, а в контейнерах помнить, что IPC изолируется своим namespace.
В итоге сигналы управляют жизнью процесса, очереди передают данные с приоритетами, семафоры синхронизируют доступ. System V это ключи, наборы и SEM_UNDO, POSIX это имена, простой API и futex под капотом. Для нового кода чаще берут POSIX, а System V приходится знать, чтобы разбираться со старыми системами.