Всем здравствуйте. Я расскажу о различных, подчас интересных, эффектах, которые возникают при работе с 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
Комментарии (6)
fareloz
06.12.2021 11:44Предлагаю обратить внимание на
std::this_thread::get_id()
для получения id потока вместо самопальной функции.
tmk826
100% в точку. Исследовать POSIX сигналы под виндоус...
staticmain
Не говоря уже о том, что printf — не реентерабельная функция, и использовать её внутри обработчика асинхронного сигнала нельзя.
NTDLL Автор
А если использовать мьютекс для блокировки общего ресурса, кем является поток вывода.
INSTE
Тогда при следующем сигнале получаем deadlock и все.
Не знаю как в винде, возможно своя специфика, но в POSIX подробно описано что такое async-signal-safe и что гарантировано им является.
mayorovp
Ну вот вы захватили мьютекс, вызвали printf. Тут пришёл сигнал, и вы снова захватываете мьютекс чтобы вызвать printf. Результат — либо ошибка, либо взаимоблокировка. И даже если вы используете реентерабельную критическую секцию, вызов printf пока выполняется printf всё равно будет ошибкой.