Посланный сигнал обрабатывается получившим его потоком или процессом (как уже обсуждалось ранее, собственно процесс ничего не может обрабатывать: это пассивная субстанция; понятно, что речь в таком контексте идет об обработке сигнала главным потоком процесса). Поток может отреагировать на полученный сигнал следующими способами (иногда действие, установленное для конкретного сигнала, называют диспозицией сигнала):
• Стандартное действие - выполнение действия, предписанного для обработки этого сигнала по умолчанию. Для многих сигналов действием по умолчанию является завершение, но это необязательно; есть сигналы, которые по умолчанию игнорируются. Для большей части сигналов, чьим действием по умолчанию является принудительное завершение процесса, предписано действие создания дампа памяти при завершении (core dump), но для некоторых, например SYSINT (завершение по [Ctrl+C]), определено такой дамп при завершении не создавать.
• Игнорирование сигнала - сигнал не оказывает никакого воздействия на ход выполнения потока получателя.
• Вызов обработчика - по поступлению сигнала вызывается функция реакции, определенная пользователем. Если для сигнала устанавливается функция-обработчик, то говорят, что сигнал перехватывается (относительно стандартного действия).
Для различных сигналов программа может установить различные механизмы обработки. Более того, в ходе выполнения программа может динамически переопределять реакции, установленные для того или иного сигнала.
Для большинства сигналов их воздействие можно перехватить из программного кода или игнорировать, но не для всех. Например, сигналы SIGKILL и SIGSTOP не могут быть ни перехвачены, ни проигнорированы. Другой пример - описываемые далее специальные сигналы QNX, начиная с SIGSELECT и далее.
В QNX определены 64 сигнала в трех диапазонах:
• 1…40 - 40 POSIX-сигналов общего назначения;
• 41…56 - 16 POSIX-сигналов реального времени, введенных в стандарт позже (от SIGRTMIN до SIGRTMAX);
• 57…64 - 8 сигналов, используемых в QNX Neutrino для специальных целей.
Начнем со специальных сигналов. Эти сигналы не могут быть проигнорированы или перехвачены: попытка вызвать signal() или sigaction() (или вызов ядра SignalAction() native API) завершится для них с ошибкой EINVAL. Более того, эти сигналы всегда блокированы в пользовательском приложении, и для них установлено разрешение очереди обслуживания (очереди сигналов будут подробно рассмотрены далее). Попытка разблокировать эти сигналы, используя sigprocmask() (SignalProcmask() - native API), будет проигнорирована. Эти сигнальные механизмы используются, в частности, графической системой Photon для ожидания событий GUI и функцией select() для ожидания ввода/вывода на множестве дескрипторов (один из фундаментальных механизмов UNIX). Вот определения некоторых из этих сигналов:
#define SIGSELECT (SIGRTMAX + 1)
#define SIGPHOTON (SIGRTMAX + 2)
В силу своей недоступности эта группа сигналов представляет небольшой интерес для разработчика приложений.
Далее перейдем к POSIX-сигналам общего назначения (1…40), из которых назовем только самые употребляемые (пока мы описываем все сигналы в классической UNIX-нотации, не углубляясь в нюансы, например такие, как особенности реакции на них в многопоточном окружении):
• SIGABRT (+) [6] - аварийное завершение процесса (process abort signal). Посылается процессу при вызове им функции abort(). В результате обработки сигнала SIGABRT произойдет то, что спецификация XSI описывает как аварийное завершение.
• SIGALRM [14] - наступление тайм-аута таймера сигналов (real-time alarm clock). Посылается при срабатывании ранее установленного пользовательского таймера (таймер устанавливается заданием первого параметра setitimer(), равным ITIMER_REAL), например при помощи системного вызова alarm().
• SIGBUS (+) [10] - сигнал ошибки на шине (bus error). Этот сигнал посылается при возникновении некоторых аппаратных ошибок (зависит от платформы); обычно он генерируется при попытке обращения к допустимому виртуальному адресу, для которого нет физической страницы.
• SIGCHLD [18] - сигнал завершения или остановка дочернего процесса (child process terminated or stopped). По умолчанию родительский процесс игнорирует этот сигнал, поэтому если нужно получать уведомление о завершении порожденного процесса, этот сигнал нужно перехватывать. Для этого сигнала определен синоним:
#define SIGCLD SIGCHLD
• SIGCONT [25] - продолжение работы остановленного процесса (continue executing if stopped). Это сигнал управления выполнением процесса, который возобновляет его выполнение, если ранее он был остановлен сигналом SIGSTOP; если процесс не остановлен, он игнорирует этот сигнал.
• SIGDEADLK [7] - сигнал, говорящий о возникновении "мертвой" блокировки потока на мьютексе (mutex deadlock occurred). Эта ситуация будет подробно рассмотрена далее, когда речь пойдет о примитивах синхронизации (глава 4).
• SIGEMT [7] - ЕМТ-инструкция (emulator trap). Как видно из сравнения с предыдущим сигналом, у них один код, то есть в QNX SIGDEADLK и SIGEMT - это один сигнал.
• SIGFPE (+) [8] - недопустимая арифметическая операция с плавающей точкой (floating point exception).
• SIGHUP [1] - сигнал освобождения линии, разрыв связи с управляющим терминалом (hangup signal). Посылается всем процессам, подключенным к управляющему терминалу при отключении терминала (обычно управляющий терминал группы процесса является терминалом пользователя, но это не всегда так). Этот сигнал также посылается всем членам сеанса, если завершает работу лидер сеанса (обычно процесс командного интерпретатора), связанного с управляющим терминалом (это гарантирует, что, если не были приняты специальные меры, при выходе пользователя из системы будут завершены все запущенные им фоновые процессы).
• SIGILL [4] - попытка выполнения недопустимой инструкции процессора (illegal instruction); обработка сигнала не сбрасывается, когда он перехватывается.
• SIGINT (-) [2] - принудительное прерывание процесса (interrupt); это тот сигнал, который генерируется при нажатии [Ctrl+C]. Это обычный способ завершения выполняющегося процесса.
• SIGKILL (+) [9] - уничтожение процесса (kill); этот сигнал может активизироваться командой kill -9 <PID>. Это сигнал безусловного завершения, посылаемый процессу другим процессом или системой (при завершении работы системы). Этот сигнал не может быть проигнорирован или перехвачен.
• SIGPIPE [13] - попытка выполнить недопустимую запись в канал или сокет, для которых принимающий процесс уже завершил работу (write on pipe or socket when recipient is terminated, write on pipe with no reader).
• SIGPOLL [22] - сигнал уведомления об одном из опрашиваемых событий (pollable event). Этот сигнал генерируется системой, когда некоторый открытый дескриптор файла готов для ввода или вывода. Этот сигнал имеет синоним (наименование SIGPOLL более известно из UNIX System V):
#define SIGIO SIGPOLL
• SIGPROF [29] - сигнал профилирующего таймера (profiling timer expired). Как и сигнал SIGALRM, этот сигнал возбуждается по истечении времени таймера (но это другой таймер), который используется для измерения времени выполнения процесса в пользовательском и системном режимах (таймер устанавливается заданием первого параметра setitimer(), равным ITIMER_PROF).
• SIGQUIT (-) [3] - выход из процесса (quit).
• SIGSEGV (+) [11] - обращение к некорректному адресу памяти, ошибка защиты памяти, нарушение границ сегмента памяти (invalid memory reference, segmentation violation).
• SIGSTOP [23] - временная остановка процесса (sendable stop signal not from tty). Если пользователь вводит с терминала [Ctrl+Z], то активному процессу посылается этот сигнал и процесс приостанавливается. Позже процесс может быть возобновлен с точки остановки при получении сигнала SIGCONT. Этот сигнал нельзя проигнорировать или перехватить.
• SIGSYS (+) [12] - некорректный системный вызов (invalid system call, bad argument to system call).
• SIGTERM [15] - программный сигнал завершения (software termination signal from kill). Программист может использовать этот сигнал для того, чтобы дать процессу время для "наведения порядка", прежде чем посылать ему сигнал SIGKILL. Именно этот сигнал посылается по умолчанию командой kill без параметра, указывающего сигнал.
• SIGTRAP [5] - сигнал трассировочного прерывания (trace trap). Это особый сигнал, который в сочетании с системным вызовом ptrace() используется отладчиками: sdb, adb, gdb. По умолчанию сигнал приводит к аварийному завершению. Обработка сигнала не сбрасывается, когда он перехватывается.
• SIGTSTP [24] - терминальный сигнал остановки (terminal stop signal). Генерируется при нажатии специальной комбинации остановки [Ctrl+Z]. Аналогичен сигналу SIGSTOP, но, в отличие от последнего, может быть перехвачен или проигнорирован.
• SIGTTIN [26] - остановка фонового процесса, если он пытается прочитать данные со своего управляющего терминала (background process attempting read).
• SIGTTOU [27] - остановка фонового процесса, если он пытается писать данные на свой управляющий терминал (background process attempting write).
• SIGURG [21] - сигнал о поступлении в буфер сокета срочных (приоритетных) данных (high bandwidth data is available at a socket, urgent condition on I/O channel) уведомляет процесс, что по открытому им сетевому соединению получены внеочередные данные.
• SIGUSR1 [16], SIGUSR2 [17] - зарезервированные сигналы пользователя. Для этих сигналов предопределенной реакцией в QNX является завершение процесса (хотя естественнее ожидать, и так это предлагает POSIX, реакцию "игнорировать сигнал"), и реакцию на них должен определять пользователь. Так же как и сигнал SIGTERM, эти сигналы никогда не посылаются системой.
• SIGVTALRM [28] - сигнал виртуального таймера (virtual timer expired). Подобно SIGPROF и SIGALRM, этот сигнал возбуждается по истечении времени таймера (это третий из доступных таймеров), который измеряет время процессора только в пользовательском режиме (таймер устанавливается заданием первого параметра setitimer(), равным ITIMER_VIRTUAL).
• SIGXCPU [30] - сигнал о превышении лимита процессорного времени (CPU time limit exceeded). Посылается процессу при исчерпании им ранее установленного лимита процессорного времени. Действие по умолчанию - аварийное завершение.
• SIGXFSZ [31] - сигнал о превышении предела, установленного на размер файла (file size limit exceeded). Действие по умолчанию - аварийное завершение.
• SIGWINCH [20] - сигнал, который генерируется (в консольном режиме pterm и xterm эмулируют его вручную при изменении их размеров) при изменении размера окна (window size change) для запущенного в окне приложения (mc, mqc…), чтобы оно перерисовало свой экран вывода.
Примечание
В QNX определено еще два специфических сигнала, которые вряд ли должны представлять для нас интерес:
• SIGIOT [6] - IOT-инструкция; никогда не генерируется для платформы x86.
• SIGPWR [19] - сигнал power-fail restart о котором в технической документации QNX ничего не говорится, но в преамбуле, описывающей нововведения версии 6.2.1, сказано: "corrected SIGPWR to SIGTERM", то есть этот сигнал, очевидно, - рудимент прежних версий системы.
Примечание
POSIX допускает, что не все сигналы могут быть реализованы. Более того, допускается ситуация, когда некоторое символическое имя сигнала определено, но сам сигнал отсутствует в системе (изменения такого рода вполне могут наблюдаться при переходе от одной версии QNX к другой). Для диагностики реального наличия сигнала можно воспользоваться рекомендацией, приведенной в информативной части стандарта POSIX 1003.1: наличие поддержки сигнала сообщает вызов функции sigaction() с аргументами act и oact, установленными в NULL. Приведем простейший тест (файл s1.cc), реализующий рекомендацию POSIX в QNX 6.2.1:
#include <stdlib.h>
#include <stream.h>
#include <errno.h>
#include <signal.h>
int main(int argc, char *argv[]) {
cout << "SIGNO";
for (int i = _SIGMIN; i <= _SIGMAX; i++) {
if (i % 8 == 1) cout << endl << i << ':';
int res = sigaction(i, NULL, NULL);
cout << '\t' << ((res != 0 && errno == EINVAL) ? '-' : '+');
}
cout << endl;
return EXIT_SUCCESS;
}
И результат его выполнения:
SIGNO
1: + + + + + + + +
9: + + + + + + + +
17: + + + + + + + +
25: + + + + + + + +
33: + + + + + + + +
41: + + + + + + + +
49: + + + + + + + +
57: - - - - - - - -
Система "считает" все сигналы 1…56 реализуемыми, а последние 8 специфических сигналов QNX, как упоминалось выше, не допускают применения к ним sigaction(). Здесь с учетом цитировавшейся выше раскладкой сигналов QNX есть небольшая загадка: максимальным номером POSIX-сигнала, определенного в <signal.h>, является 31 (SIGXFSZ – 31); там же в комментарии есть фраза: "допустимый диапазон пользовательских сигналов - от 1 до 56, используемых ядром - от 57 до 64". Непонятно, в каком качестве используются сигналы 32–40, непосредственно предшествующие сигналам реального времени (41–56) и диагностируемые sigaction() как действительные (valid)? Позже мы увидим, что они обслуживаются системой наравне с документированными сигналами.
Традиционная обработка сигнала
В этой части изложения мы рассмотрим традиционные модели перехвата сигналов и установки для них собственных обработчиков (в том числе и игнорирование или восстановление стандартной обработки по умолчанию). Термин "традиционный" здесь означает, что мы бегло рассмотрим обработку сигналов применительно к процессам и стандартным сигналам UNIX (не сигналам реального времени), то есть в том изложении, как она традиционно рассматривается в литературе по UNIX (и здесь сигнал воспринимается, конечно же, единственным потоком приложения, а не процессом, но в этом случае различие не принципиально). Позже мы рассмотрим модель обработки сигналов реального времени и расширим ее на многопоточные приложения.
"Старая" модель обработки сигнала
В ранних версиях UNIX была принята единственная модель обработки сигналов, основанная на функции signal(), которая подразумевает семантику так называемых "ненадежных сигналов", принятую в этих ОС. Позже эта модель была подвержена радикальной критике, вскрывшей ее "ненадежность". Данная модель сохранена для совместимости с ранее разработанным программным обеспечением. Она обладает существенными недостатками, основными из которых являются:
• процесс не может заблокировать сигнал, то есть отложить получение сигнала на период выполнения критических участков кода;
• каждый раз при получении сигнала его диспозиция устанавливается на действие по умолчанию, и при необходимости продолжить обработку поступающих сигналов требуется повторно восстанавливать требуемый обработчик.
Вот пример (файл s2.cc) использования этой модели в коде, который уже стал иллюстративным образцом и кочует из одного источника в другой: