Сигналы, очереди и семафоры в Linux: от UNIX до ядра и DevOps

Сигналы, очереди и семафоры в Linux: от UNIX до ядра и DevOps

Сигналы, очереди сообщений и семафоры это три классических механизма межпроцессного взаимодействия (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 приходится знать, чтобы разбираться со старыми системами.

Источники и ссылки


Комментарии загружаются…