Статьи

Основы обработки сигналов

Невеликорусский Гэтсби-Green-Light

Сигналы являются одним из самых основных способов, которыми программы могут получать сообщения из внешнего мира. Я нашел ограниченную документацию по типу учебника, поэтому в этом посте рассказывается, как их настроить, и некоторые методы отладки.

Самый простой способ почувствовать обработку сигнала — поиграть с простой программой на Си, например:

#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: