Сигналы являются одним из самых основных способов, которыми программы могут получать сообщения из внешнего мира. Я нашел ограниченную документацию по типу учебника, поэтому в этом посте рассказывается, как их настроить, и некоторые методы отладки.
Самый простой способ почувствовать обработку сигнала — поиграть с простой программой на Си, например:
#include <signal.h> #include <stdio.h> #include <unistd.h> void my_handler(int signum) { const char msg[] = "Signal handler got signal\n"; write(STDOUT_FILENO, msg, sizeof msg); } int main(int argc, char *argv[]) { printf("PID: %d\n", getpid()); // Set up signal handler struct sigaction action = {}; action.sa_handler = &my_handler; sigaction(SIGINT, &action, NULL); while (1) { pause(); } return 0; }
Скомпилируйте и запустите и попробуйте несколько раз нажать Ctrl-C:
$ gcc signals.c -o signals $ ./signals PID: 11152 ^CSignal handler got signal 2 ^CSignal handler got signal 2 ^CSignal handler got signal 2
Каждый сигнал вызывает обработчик сигнала, который мы настроили.
Если вы подключите strace
(системный вызов трассировщик), а затем снова нажмете Ctrl-C в работающем терминале ./signals , вы увидите каждый поступающий сигнал:
$ $ strace -p 11152 -e trace=none -e signal=all Process 11152 attached - interrupt to quit --- SIGINT (Interrupt) @ 0 (0) --- --- SIGINT (Interrupt) @ 0 (0) --- --- SIGINT (Interrupt) @ 0 (0) ---
Поскольку мы не можем убить его с помощью Ctrl-C, мы можем использовать kill для выключения ./signals :
$ kill 11152
kill
по умолчанию отправляет SIGTERM, который мы не обрабатываем (пока). Вы могли бы добавить обработчик для него, добавив строку, sigaction(SIGTERM, &action, NULL);
но тогда нам пришлось бы kill -9
в процессе убить его (это два дополнительных символа ввода), поэтому я оставляю SIGTERM необработанным.
Игнорирование сигналов
Есть также способы заставить вашу программу даже не получать сигналы: игнорировать и блокировать их (которые немного отличаются). Чтобы игнорировать сигнал, измените sa_action
на SIG_IGN
:
#include <signal.h> #include <stdio.h> #include <unistd.h> void my_handler(int signum) { const char msg[] = "Signal handler got signal\n"; write(STDOUT_FILENO, msg, sizeof msg); } int main(int argc, char *argv[]) { printf("PID: %d\n", getpid()); // Set up signal handler struct sigaction action = {}; action.sa_handler = SIG_IGN; sigaction(SIGINT, &action, NULL); while (1) { pause(); } return 0; }
Теперь перекомпилируйте, запустите и нажмите Ctrl-C. Вы получите что-то вроде этого:
$ ./signals PID: 86579 ^C^C^C^C^C
Если вы присоедините strace , вы увидите, что ./signals даже не получает SIGINT.
Вы можете увидеть сигналы, которые программа игнорирует, посмотрев на / proc / PID / status :
$ cat /proc/86579/status Name: signals State: S (sleeping) Tgid: 86579 Pid: 86579 PPid: 30493 TracerPid: 0 Uid: 197420 197420 197420 197420 Gid: 5000 5000 5000 5000 FDSize: 256 Groups: 4 20 24 25 44 46 104 128 499 5000 5001 5762 74990 75209 77056 78700 79910 79982 VmPeak: 4280 kB VmSize: 4160 kB VmLck: 0 kB VmPin: 0 kB VmHWM: 352 kB VmRSS: 352 kB VmData: 48 kB VmStk: 136 kB VmExe: 4 kB VmLib: 1884 kB VmPTE: 28 kB VmSwap: 0 kB Threads: 1 SigQ: 0/192723 SigPnd: 0000000000000000 ShdPnd: 0000000000000000 SigBlk: 0000000000000000 SigIgn: 0000000000000002SigCgt: 0000000000000000 CapInh: 0000000000000000 CapPrm: 0000000000000000 CapEff: 0000000000000000 CapBnd: ffffffffffffffff Cpus_allowed: ffffffff Cpus_allowed_list: 0-31 Mems_allowed: 00000000,00000001 Mems_allowed_list: 0 voluntary_ctxt_switches: 2 nonvoluntary_ctxt_switches: 3
SigIgn — это шестнадцатеричное число, и оно имеет странный формат: бит проигнорированного номера сигнала установлен. Итак, для 2 установлен второй бит. Для SIGTERM (сигнал 15) установлен 15-й бит: 0100_0000_0000_0000 в двоичном формате или 0 × 4000 в шестнадцатеричном формате. Итак, если вы игнорируете SIGINT и SIGTERM, SigIgn будет выглядеть следующим образом: 0000000000004002.
SigCgt для сигналов, которые улавливаются программой, а SigBlk для сигналов, которые блокируются.
Блокирующие сигналы
Что если вы хотите, чтобы ваша программа обрабатывала любые поступающие сигналы, просто сделайте это позже? У вас может быть критическая секция, где вы не хотите, чтобы вас прерывали, но после этого вы хотите знать, что произошло. Вот где блокирующие сигналы пригодятся.
Вы можете заблокировать сигналы, используя sigprocmask
:
#include <signal.h> #include <stdio.h> #include <unistd.h> void my_handler(int signum) { const char msg[] = "Signal handler got signal\n"; write(STDOUT_FILENO, msg, sizeof msg); } int main(int argc, char *argv[]) { printf("PID: %d\n", getpid()); // Set up signal handler struct sigaction action = {}; action.sa_handler = &my_handler; sigaction(SIGINT, &action, NULL); printf("Blocking signals...\n"); sigset_t sigset; sigemptyset(&sigset); sigaddset(&sigset, SIGINT); sigprocmask(SIG_BLOCK, &sigset, NULL); // Critical section sleep(5); printf("Unblocking signals...\n"); sigprocmask(SIG_UNBLOCK, &sigset, NULL); while (1) { pause(); } return 0; }
Сначала мы создаем, sigset_t
который может содержать набор сигналов. Мы очищаем набор вызовом sigemptyset
и добавляем элемент сигнала: SIGINT. (Существует множество других настроек, которые вы можете использовать для изменения sigset_t
, если это необходимо.)
Если вы скомпилируете и запустите это и попробуете Ctrl-C-ing, когда сигналы заблокированы, один сигнал будет «пропущен», когда сигналы разблокированы:
$ ./signals PID: 86791 Blocking signals... ^C^C^C^C^C^CUnblocking signals... Signal handler got signal 2
Одним из распространенных применений для этого является блокировка сигналов во время работы обработчика сигналов. Таким образом, вы можете заставить свой обработчик сигналов безопасно изменять неатомарное состояние (скажем, счетчик количества поступивших сигналов).
Однако предположим, что мы вызвали sigprocmask
обработчик сигнала. Всегда будет условие гонки: еще один сигнал может прийти, прежде чем мы позвоним sigprocmask
! Таким образом, sigaction
принимает маску сигналов, которые он должен блокировать во время выполнения обработчика:
#include <signal.h> #include <stdio.h> #include <unistd.h> void my_handler(int signum) { const char msg[] = "Signal handler got signal\n"; write(STDOUT_FILENO, msg, sizeof msg); } int main(int argc, char *argv[]) { printf("PID: %d\n", getpid()); // Set up signal handler struct sigaction action = {}; action.sa_handler = &my_handler; sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGINT); sigaddset(&mask, SIGTERM); sigaction(SIGINT, &action, NULL); while (1) { pause(); } return 0; }
Здесь мы маскируем как SIGINT, так и SIGTERM: если какой-либо из этих сигналов my_handler
поступит во время работы, они будут заблокированы до его завершения.
наследование
Игнорируемые и заблокированные сигналы наследуются при форке программы. Таким образом, если программа не реагирует на сигналы так, как вы ожидаете, возможно, виноват тот, кто ее раздвоил. Кроме того, если вы хотите, чтобы ваша программа обрабатывала сигналы определенным образом, вы должны явно установить это, а не в зависимости от значения по умолчанию.
If you want to use a signal’s default behavior (which is usually “terminate the program”), you can use SIG_DFL
as the sa_handler
.
What you can do in a signal handler
You might notice that I’m using write
in the signal handlers above, instead of the somewhat more friendly printf
. This is because there are only a small set of “async safe” functions you can call in a signal handler and printf isn’t one of them. There is a list of functions you can call on the signal(7) man page. A few examples that often come up: you cannot heap-allocate memory, buffer output, or mess with locks.
If you call any unsafe functions in a signal handler, the behavior is undefined (meaning it might work fine, or it might make your car blow up).
Edit: thanks to Vincent Bernat, who mentioned this in the comments.
References: