Skip to content

Latest commit

 

History

History
 
 

signal-1

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

Сигналы. Часть 1

Введение

Сигнал - это механизм передачи коротких сообщений (номер сигнала), как правило, прерывающий работу процесса, которому он был отправлен.

Сигналы могут быть посланы процессу:

  • ядром, как правило, в случае критической ошибки выполнения;
  • другим процессом;
  • самому себе.

Номера сигналов начинаются с 1. Значение 0 имеет специальное назначение (см. ниже про kill). Некоторым номерам сигналов соответствуют стандартные для POSIX названия и назначения, которые подробно описаны man 7 signal.

При получении сигнала процесс может:

  1. Игнорировать его. Это возможно для всех сигналов, кроме SIGSTOP и SIGKILL.
  2. Обработать отдельной функцией. Кроме SIGSTOP и SIGKILL.
  3. Выполнить действие по умолчанию, предусмотренное назначением стандартного сигнала POSIX. Как правило, это завершение работы процесса.

По умолчанию, все сигналы, кроме SIGCHILD (информирование о завершении дочернего процесса) и SIGURG (информировании о поступлении TCP-сегмента с приоритетными данными), приводят к завершению работы процесса.

Если процесс был завершён с помощью сигнала, а не с ипользованием системного вызова exit, то для него считается не определенным код возврата. Родительский процесс может отследить эту ситуацию, используюя макросы WIFSIGNALED и WTERMSIG:

pid_t child = ...
...
int status;
waitpid(child, &status, 0);
if (WIFEXITED(status)) {
    // дочерний процесс был завершён через exit
    int code = WEXITSTATUS(status); // код возврата
}
if (WIFSIGNALED(status)) {
    // дочерний процесс был завершёл сигналом
    int signum = WTERMSIG(status); // номер сигнала
}

Отправить сигнал любому процессу можно с помощью команды kill. По умолчанию отправляется сигнал SIGTERM, но можно указать в качестве опции, какой именно сигнал нужно отправить. Кроме того, некоторые сигналы отправляются терминалом, например Ctrl+C посылает сигнал SIGINT, а Ctrl+\ - сигнал SIGQUIT.

Пользовательские сигналы

Изначально в POSIX было зарезервировано два номера сигнала, которые можно было использовать на умотрение пользователя: SIGUSR1 и SIGUSR2.

Кроме того, в Linux предусмотрен диапазон сигналов с номерами от SIGRTMIN до SIGRTMAX, которые можно использовать на усмотрение пользователя.

Действием по умолчанию для всех "пользовательских" сигналов является завершение работы процесса.

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

Системный вызов kill

По аналогии с одноимённой командой, kill предназначен для отправки сигнала любому процессу.

int kill(pid_t pid, int signum); // возврашает 0 или -1, если ошибка

Отправлять сигналы можно только тем процессам, которые принадлежат тому пользователю, что и пользователь, по которым выполняется системный вызов kill. Исключение составляет пользователь root, который может всё. При попытке отправить сигнал процессу другого пользователя, kill вернёт значение -1.

Номер процесса может быть меньше 1 в случаях:

  • 0 - отправить сигнал всем процессам текущей группы процессов;
  • -1 - отправить сигнал всем процессам пользователя (использовать с осторожностью!);
  • отрицательное значение -PID - отправить сигнал всем процессам группы PID.

Номер сигнала может принимать значение 0, - в этом случае никакой сигнал не будет отправлен, а kill вернёт значение 0 в том случае, если процесс (группа) с указанным pid существует, и есть права на отправку сигналов.

Функции raise и abort

Функция raise предназначен для отправки сигнала процессом самому себе. Функция стандартной библиотеки abort посылает самому себе сигнал SIGABRT, и часто используется для генерации исключительных ситуаций, которые получилось диагностировать во время выполнения, например, функцией assert.

Системный вызов alarm

Системный вызов alarm запускает таймер, по истечении которого процесс сам себе отправит сигнал SIGALRM.

unsigned int alarm(unsigned int seconds);

Отменить ранее установленный таймер можно, вызвав alarm с параметром 0. Возвращаемым значением является количество секунд предыдущего установленного таймера.

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

Сигналы, которые можно перехватить, то есть все, кроме SIGSTOP и SIGKILL, можно обработать программным способом. Для этого необходимо зарегистрировать функцию-обработчик сигнала.

Системный вызов signal

#include <signal.h>

// Этот тип определен только в Linux!
typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler); // для Linux
void (*signal(int signum, void (*func)(int))) (int); // по стандарту POSIX

Системный вызов signal предназначен для того, чтобы зарегистрировать функцию в качестве обработчика определенного сигнала. Первым аргументом является номер сигнала, вторым - указатель на функцию, которая принимает единственный аргумент - номер пришедшего сигнала (т.е. одну функцию можно использовать сразу для нескольких сигналов), и ничего не возвращает.

Два специальных значения функции-обработчика SIG_DFL и SIG_IGN предназанчены для указания обработчика по умолчанию (т.е. отмены ранее зарегистрированного обработчика) и установки игнорирования сигнала.

Системный вызов signal возвращает указатель на ранее установленный обработчик.

System-V v.s. BSD

В стандартах, родоначальниками которых были UNIX System-V и BSD UNIX, используется различное поведение обработчика сигнала, зарегистрированного с помощью signal. При определении одного из макросов препроцессора: _BSD_SOURCE, _GNU_SOURCE или _DEFAULT_SOURCE (что подразумевается опцией компиляции -std=gnu99 или -std=gnu11), используется семантика BSD; в противном случае (-std=c99 или -std=c11) - семантика System-V.

Отличия BSD от System-V:

  • В System-V обработчик сигнала выполяется один раз, после чего сбрасывается на обработчик по умолчанию, а в BSD - остается неизменным.
  • В BSD обработчик сигнала не будет вызван, если в это время уже выполняется обработчик того же самого сигнала, а в System-V это возможно.
  • В System-V блокирующие системные вызовы (например, read) завершают свою работу при поступлении сигнала, а в BSD большинство блокирующих системных вызовов возобновляют свою работу после того, как обработчик сигнала заверщает свою работу.

По этой причине, системный вызов signal считается устаревшим, и в новом коде использовать его запрещено, за исключением двух ситуаций:

signal(signum, SIG_DFL); // сброс на обработчик по умолчанию
signal(signum, SIG_IGN); // игнорирование сигнала

Системный вызов sigaction

Системный вызов sigaction, в отличии от signal, в качестве второго аргумента принимает не указатель на функцию, а указатель на структуру struct sigaction, с которой, помимо указателя на функцию, хранится дополнительная информация, описывающая семантику обработки сигнала. Поведение обработчиков, зарегистрированных с помощью sigaction, не зависит от операционной системы.

int sigaction(int signum,
              const struct sigaction *restrict act,
              struct sigaction *oldact);

Третьим аргументов является указатель на структуру, описывающую обработчик, который был зарегистрирован для этого. Если эта информация не нужна, то можно передать значение NULL.

Основные поля структуры struct sigaction:

  • sa_handler - указатель на функцию-обработчик с одним аргументом типа int, могут быть использованы значения SIG_DFL и SIG_IGN;
  • sa_flags - набор флагов, опиывающих поведение обработчика;
  • sa_sigaction - указатель на функцию-обработчик с тремя параметрами, а не одним (используется, если в флагах присутствует SA_SIGINFO).

Некоторые флаги, которые можно передавать в sa_flags:

  • SA_RESTART - продолжать выполнение прерванных системных вызовов (семантика BSD) после завершения обработки сигнала. По умолчанию (если флаг отсутствует) используется семантика System-V.
  • SA_SIGINFO - вместо функции из sa_handler нужно использовать функцию с тремя параметрами int signum, siginfo_t *info, void *context, которой помимо номера сигнала, передается дополнительная информация (например PID отправителя) и пользовательский контекст.
  • SA_RESETHAND - после выполнения обработчика сбросить на обработчик по умолчанию (семантика System-V). По умолчанию (если флаг отсутствует) используется семантика BSD.
  • SA_NODEFER - при повторном приходе сигнала во время выполени обработчика он будет обработан немедленно (семантика System-V). По умолчанию (если флаг отсутствует) используется семантика BSD.

Асинхронность обработки сигналов

Сигнал может прийти процессу в любой момент времени. При этом, выполнение текущего кода будет прервано, и будет запущен обработчик сигнала.

Таким образом, возникает проблема "гонки данных", которая часто встречается в многопоточном программировании.

Существует безопасный целочисленный (32-разрядный) тип данных, для которого гарантируется атомарность чтения/записи при переключении между выполнением основной программы и выполнением обработчика сигнала: sig_atomic_t, объявленный в <signal.h>.

Кроме того, во время выполнения обработчика сигналов запрещено использовать не потоко-безопасные функции (большинство функций стандартной библиотеки). В то же время, использование системных вызовов - безопасно.