Всегда ли небезопасно, когда я вызываю неасинхронную функцию из обработчика сигнала?

Я просто выясняю, могу ли я вызывать неасинхронную функцию в обработчике сигналов.
Цитаты из справочного руководства по Linux (7):

Если сигнал прерывает выполнение небезопасной функции, а обработчик вызывает небезопасную функцию, то поведение программы не определено.

и TLPI:

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

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

Например, я устанавливаю обработчик для SIGINT, который вызывает небезопасную функцию, предположим, что crypt(3) который не возвращается, а именно небезопасен.

sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sa.sa_handler = handler;
sigaction(SIGINT, &sa, NULL);

И я также называю printf() в бесконечном цикле в main() и у меня только основной поток работает.

Вопрос в этом простом примере: я не вижу, чтобы что-то плохое происходило, когда обработчик прерывал выполнение printf() и это вызывает небезопасную функцию. Афак, printf() получит консольную блокировку и будет иметь внутренний буфер для выполнения буферизованного ввода / вывода, но его состояние в этом примере непротиворечиво. И хотя crypt() возвращает статически размещенную строку, но она не используется другими функциями или потоками.

Я что-то неправильно понимаю? Я хочу, чтобы кто-то объяснил мне, что всегда опасно, чтобы обработчик сигнала прерывал выполнение небезопасной функции в основной программе и сам также вызывал небезопасную функцию, или это безопасно в некоторых ситуациях (например, простой пример выше)?

2 ответа

Решение

Да, действительно, во всех случаях небезопасно вызывать не-асинхронно-безопасную функцию изнутри обработчика сигнала (если вы не погрузитесь в код реализации -eg libc и, возможно, ваш компилятор генерирует код для него); тогда вы могли бы доказать, что вызов такой функции на самом деле безопасен; но такое доказательство может оказаться невозможным или занять месяцы или годы вашего времени, даже с помощью статических анализаторов, например, Frama-C,... и потребовать изучения всех деталей реализации.

Конкретно, вероятно, что crypt внутренне звонит malloc (для некоторых внутренних данных и т.д...). И стандарт malloc Функция, очевидно, имеет некоторое глобальное состояние (например, вектор связанных списков сегментов, связанных с ранее free -d зона памяти, для повторного использования в будущих вызовах malloc).

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

Детали сильно зависят от реализации, в зависимости от вашего компилятора и флагов оптимизации, libc, ядро, архитектура процессора и т.д...

Вы можете не заботиться об асинхронно-безопасных функциях, делая ставку на то, что катастрофы не произойдет. Это может быть приемлемо для целей отладки (например, очень часто, но не всегда, printf внутри обработчик сигнала практически работал бы большую часть времени; и компилятор GCC внутренне использует свою "async-unsafe" библиотеку libbacktrace в обработчиках сигналов) для кода, заключенного в #ifndef NDEBUG, но это не будет хорошо для производственного кода; если вам действительно нужно добавить такой код в обработчик, упомяните в комментарии, что вы знаете, что делаете неправильно вызов функции, не связанной с асинхронным сигналом, и будьте готовы к тому, что будущие коллеги будут работать над тем же кодовая база.

Типичный трюк для решения таких ситуаций - просто установить volatile sig_atomic_t Отметьте флаг в обработчике сигналов (см. документацию POSIX signal.h) и проверьте этот флаг в каком-то безопасном месте в некотором цикле - вне обработчика - или запишите (2) один - или несколько байтов в канал (7) предварительно настроенный при инициализации приложения, и имеющий конец чтения этого канала периодически опрашиваемый (2), а затем читаемый вашим циклом событий - или каким-то другим потоком -).

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

Небезопасно вызывать любую асинхронно-небезопасную функцию в обработчике сигнала, если сигнал прерывает любую асинхронно-небезопасную функцию в основной программе. Асинхронно-небезопасные функции не должны иметь ничего общего друг с другом - результаты не определены.

Таким образом, практически единственный способ безопасного вызова асинхронно-небезопасных функций в обработчике сигнала состоит в том, чтобы гарантировать, что сигнал никогда не будет возникать при вызове небезопасной функции aysnc. Один из способов сделать это - обернуть каждый вызов любой асинхронно-небезопасной функции соответствующими вызовами sigblock/sigsetmask, чтобы гарантировать, что сигналы не будут доставлены во время выполнения небезопасной функции. Другое - установить / очистить основную программу. sigatomic флаг, когда он вызывает асинхронно-небезопасные функции, и обработчик сигнала должен проверить этот флаг, прежде чем пытаться вызвать асинхронно-небезопасные функции.

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

Другие вопросы по тегам