Всем здравствуйте. Я расскажу о различных, подчас интересных, эффектах, которые возникают при работе с POSIX сигналами в Windows. Затаривайтесь Вискасом и вперёд.

Конечно же не могу не упомянуть гибкость механизма сигналов POSIX. Этот механизм реализует подобие парадигмы событийного программирования в C#, чем он, собственно, и интересен. Обычно, в коде, написанном на C/C++ мы вызываем функции ядра, например внутри getch(). А вот когда ядро вызывает нас, происходят всякие интересные эффекты: такие как асинхронные вызовы и произвольная логика программы, изменение способа обработки исключительных ситуаций. Чем то напоминает callback.
Здесь мы можем отреагировать на действия пользователя, сигналы от ОС или исключительную ситуацию, возникшую в программе. Программирование происходит прозрачно, т.е. мы можем, например, написать обработчик SIGABRT и красиво изменить способ обработки неперехваченных исключений без исследования исходного кода всей программы, т.к. писать обработчики и генерировать в них специально этот сигнал не нужно, он генерируется сам. Но такая гибкость обусловлена тем, что есть некоторые, не совсем очевидные, аспекты.
Я продемонстрирую несколько экспериментов с сигналами, в которых обнаруживаются не совсем очевидные вещи. В качестве примеров я выбрал обработчики сигналов SIGINT и SIGABRT. Обработчик SIGINT вызывается асинхронно, в ответ на нажатие клавиш CTRL+C. Обработчик SIGABRT вызывается в ответ на необработанную исключительную ситуацию, но не всегда. Я объясню особенности, возникающие в зависимости от способа генерации сигналов и наличия отладчика. Обратите внимание на то, что все эксперименты проводились в Windows, а исходный код компилируется как код C++.
Содержание
Обработка исключения, в зависимости от контекста обработчика сигнала.
Обработчик сигнала SIGABRT вызывается, если программа выполняется не под отладчиком.
Исходный код программы
#include <windows.h> #include <stdlib.h> #include <stdio.h> #include <conio.h> #include <signal.h> #include <errno.h> #define SIGINT_RAISE int _get_thread_id() { HANDLE hThread = 0; DWORD dwId = 0; hThread = GetCurrentThread(); dwId = GetThreadId(hThread); return (int)dwId; } void funcabrt(int sig) // обработчик неперехваченного исключения { _set_abort_behavior(0, _CALL_REPORTFAULT); perror("funcabrt"); printf("Thread ID: %d\n", _get_thread_id()); _getch(); }; void funcint(int sig) // обработчик нажатия клавиш CTRL+C { try { printf("funcint\n"); printf("Thread ID: %d\n", _get_thread_id()); _getch(); throw (int)ERANGE; } catch (int err) { _set_errno(err); throw; } }; int main() { #if defined(SIGINT_RAISE) printf("Raise SIGINT mode on\n"); #else printf("Raise SIGINT mode off\n"); #endif printf("main\n"); printf("Thread ID: %d\n", _get_thread_id()); try { if (signal(SIGABRT, funcabrt) == SIG_ERR) exit(EXIT_FAILURE); if (signal(SIGINT, funcint) == SIG_ERR) exit(EXIT_FAILURE); #if defined(SIGINT_RAISE) raise(SIGINT); #endif while (1); // нажмите CTRL+C вылетит птенец } catch (int err) { printf("catch\n"); _getch(); }; };
Обработчики сигналов запускаются в разном контексте в зависимости от того, каким образом сгенерирован сигнал.
Обработчик сигнала SIGINT
void funcint(int sig) // обработчик нажатия клавиш CTRL+C { try { printf("funcint\n"); printf("Thread ID: %d\n", _get_thread_id()); _getch(); throw (int)ERANGE; } catch (int err) { _set_errno(err); throw; } };
Случай, когда сигнал возбужден программой Обработчик запускается в контексте нашей программы.
Вывод
Raise SIGINT mode on main Thread ID: 5380 funcint Thread ID: 5380
Смотрим стек вызовов
ntdll.dll!775270f4() ntdll.dll![Указанные ниже кадры могут быть неверны или отсутствовать, символы для ntdll.dll не загружены] ntdll.dll!775264a4() kernel32.dll!76054b6e() kernel32.dll!760acf97() kernel32.dll!760ad071() ucrtbase.dll!0fb5ed35() ucrtbase.dll!0fb5ec74() experiment.exe!funcint(int sig) ucrtbase.dll!0fb2ca9a() experiment.exe!main() [Внедренный фрейм] experiment.exe!invoke_main() experiment.exe!__scrt_common_main_seh() kernel32.dll!7604ed6c() ntdll.dll!775437eb() ntdll.dll!775437be()
Мы видим, что вызов обработчика происходит в основном потоке, посредством библиотеки выполнения C Runtime
Случай, когда сигнал возбужден ядром. Это стандартная ситуация, когда сигнал SIGINT возбуждается при нажатии на клавиатуре CTRL+C при работе консольных приложений. Обработчик запускается в контексте ядра в отдельном потоке.
Вывод
Raise SIGINT mode off main Thread ID: 976 funcint Thread ID: 2848
Смотрим стек вызовов потока 2848
ntdll.dll!775270f4() ntdll.dll![Указанные ниже кадры могут быть неверны или отсутствовать, символы для ntdll.dll не загружены] ntdll.dll!775264a4() kernel32.dll!76054b6e() kernel32.dll!760acf97() kernel32.dll!760ad071() ucrtbase.dll!0fb5ed35() ucrtbase.dll!0fb5ec74() experiment.exe!funcint(int sig) ucrtbase.dll!0fb2c72d() kernel32.dll!7607e3d8() kernel32.dll!7604ed6c() ntdll.dll!775437eb() ntdll.dll!775437be()
Мы видим, что вызов обработчика происходит в потоке kernel32.dll, посредством библиотеки выполнения C Runtime
Обработка исключения, в зависимости от контекста обработчика сигнала.
int main() { #if defined(SIGINT_RAISE) printf("Raise SIGINT mode on\n"); #else printf("Raise SIGINT mode off\n"); #endif printf("main\n"); printf("Thread ID: %d\n", _get_thread_id()); try { if (signal(SIGABRT, funcabrt) == SIG_ERR) exit(EXIT_FAILURE); if (signal(SIGINT, funcint) == SIG_ERR) exit(EXIT_FAILURE); #if defined(SIGINT_RAISE) raise(SIGINT); #endif while (1); // нажмите CTRL+C вылетит птенец } catch (int err) { printf("catch\n"); _getch(); }; };
Здесь в том случае, если сигнал сгенерирован программой, возбужденное исключение попадёт в блок try/catch. Причина следует из особенностей, которые я описал в п.1. При возбуждении исключения происходит раскрутка стека, т.е. поиск обработчиков в генерирующей сигнал (и фактически вызвавшей её обработчик) функции.
Вывод
Raise SIGINT mode on main Thread ID: 4676 funcint Thread ID: 4676 catch
А вот если SIGINT сгенерирован ядром, исключение не найдёт своего обработчика и отправится в отладчик, либо будет сгенерирован SIGABRT. Точнее найдёт, возможно, в неграх ядра есть SEH обработчик верхнего уровня (системный), до которого исключение в конце концов добирается и уже этот обработчик производит фактически вызов обработчика SIGABRT, который находится в нашей программе. Однако, зафиксировать этот факт при отладке не представляется возможным.
Обработчик сигнала SIGABRT вызывается, если программа выполняется не под отладчиком.
Возбуждаем пустое исключение.
throw;
Если программа выполняется под отладчиком, неперехваченное исключение попадает в отладчик, т.к. никаких обработчиков для такого исключения не предусмотрено.
Вывод
Raise SIGINT mode off main Thread ID: 2940 funcint Thread ID: 5372
Отладчик

Если программа выполняется без присоединённого отладчика, происходит генерация SIGABRT
Обработчик сигнала SIGABRT
void funcabrt(int sig) // обработчик неперехваченного исключения { _set_abort_behavior(0, _CALL_REPORTFAULT); perror("funcabrt"); printf("Thread ID: %d\n", _get_thread_id()); _getch(); };
Вывод
Raise SIGINT mode off main Thread ID: 5748 funcint Thread ID: 2732 funcabrt: Result too large Thread ID: 2732
Итоги:
Если мы генерируем сигнал в программе, то обработчик сигнала вызывается как обычная функция Если сигнал генерирует ядро, то обработчик сигнала вызывается в отдельном потоке
Если программа выполняется под отладчиком, все сгенерированные и необработанные исключения перехватывает отладчик. Если программа выполняется без отладчика, вызывается обработчик сигнала
SIGABRT