Как избежать использования printf в обработчике сигналов?
Поскольку printf
не реентерабелен, его использование в обработчике сигналов не может быть безопасным. Но я видел много примеров кодов, которые используют printf
сюда.
Поэтому мой вопрос: когда нам нужно избегать использования printf
в обработчике сигналов, и есть ли рекомендуемая замена?
9 ответов
Вы можете использовать некоторую переменную флага, установить этот флаг в обработчике сигнала и на основе этого вызова флага printf()
функция в main() или другой части программы во время нормальной работы.
Не безопасно вызывать все функции, такие как
printf
из обработчика сигнала. Полезный прием - использовать обработчик сигнала для установкиflag
а потом проверь чтоflag
из основной программы и при необходимости распечатайте сообщение.
Обратите внимание, что в приведенном ниже примере обработчик сигнала ding() установил флаг alarm_fired
1 как SIGALRM пойман и в основной функции alarm_fired
значение проверяется, чтобы правильно вызвать printf.
static int alarm_fired = 0;
void ding(int sig) // can be called asynchronously
{
alarm_fired = 1; // set flag
}
int main()
{
pid_t pid;
printf("alarm application starting\n");
pid = fork();
switch(pid) {
case -1:
/* Failure */
perror("fork failed");
exit(1);
case 0:
/* child */
sleep(5);
kill(getppid(), SIGALRM);
exit(0);
}
/* if we get here we are the parent process */
printf("waiting for alarm to go off\n");
(void) signal(SIGALRM, ding);
pause();
if (alarm_fired) // check flag to call printf
printf("Ding!\n");
printf("done\n");
exit(0);
}
Ссылка: Начало программирования на Linux, 4-е издание. В этой книге объясняется именно ваш код (что вы хотите), Глава 11: Процессы и сигналы, стр. 484.
Кроме того, вы должны быть особенно внимательны при написании функций-обработчиков, потому что они могут вызываться асинхронно. То есть обработчик может быть вызван в любой точке программы непредсказуемо. Если два сигнала поступают в течение очень короткого интервала, один обработчик может работать в другом. И считается лучшей практикой объявлять volatile sigatomic_t
к этому типу всегда обращаются атомарно, избегая неопределенности по поводу прерывания доступа к переменной. (читайте: Доступ к атомарным данным и обработка сигналов для детализации).
Прочитайте Определение обработчиков сигналов: чтобы узнать, как написать функцию обработчика сигналов, которая может быть установлена с помощью signal()
или же sigaction()
функции.
Список разрешенных функций на странице руководства, вызов этой функции внутри обработчика сигнала безопасен.
Основная проблема заключается в том, что если сигнал прерывается malloc()
или какой-либо аналогичной функции, внутреннее состояние может быть временно несовместимым, пока оно перемещает блоки памяти между свободным и использованным списком или другими подобными операциями. Если код в обработчике сигнала вызывает функцию, которая затем вызывает malloc()
, это может полностью разрушить управление памятью.
Стандарт C использует очень консервативный взгляд на то, что вы можете сделать в обработчике сигналов:
ISO / IEC 9899: 2011 §7.14.1.1
signal
функцияIf5 Если сигнал возникает не в результате вызова
abort
или жеraise
функция, поведение не определено, если обработчик сигнала ссылается на любой объект со статической или продолжительностью хранения потока, который не является атомарным объектом без блокировки, кроме как путем присвоения значения объекту, объявленному какvolatile sig_atomic_t
или обработчик сигнала вызывает любую функцию в стандартной библиотеке, кромеabort
функция,_Exit
функция,quick_exit
функция илиsignal
функция с первым аргументом, равным номеру сигнала, соответствующему сигналу, вызвавшему вызов обработчика. Кроме того, если такой вызовsignal
Функция приводит кSIG_ERR
возврат, значениеerrno
является неопределенным. 252)252) Если какой-либо сигнал генерируется асинхронным обработчиком сигнала, поведение не определено.
POSIX намного более щедр в отношении того, что вы можете сделать в обработчике сигналов.
Концепции сигналов в выпуске POSIX 2008 гласят:
Если процесс многопоточный, или если процесс однопоточный и обработчик сигнала выполняется иначе, чем в результате:
Вызов процесса
abort()
,raise()
,kill()
,pthread_kill()
, или жеsigqueue()
генерировать сигнал, который не заблокированОжидающий сигнал разблокируется и доставляется до того, как вызов, который разблокирован, возвращается
поведение не определено, если обработчик сигнала ссылается на любой объект, кроме
errno
со статической продолжительностью хранения, отличной от присвоения значения объекту, объявленному какvolatile sig_atomic_t
или если обработчик сигнала вызывает любую функцию, определенную в этом стандарте, кроме одной из функций, перечисленных в следующей таблице.В следующей таблице определен набор функций, которые должны быть безопасны для асинхронных сигналов. Поэтому приложения могут вызывать их без ограничений из функций сбора сигналов:
_Exit() fexecve() posix_trace_event() sigprocmask() _exit() fork() pselect() sigqueue() … fcntl() pipe() sigpause() write() fdatasync() poll() sigpending()
Все функции, не указанные в приведенной выше таблице, считаются небезопасными в отношении сигналов. При наличии сигналов все функции, определенные этим объемом в POSIX.1-2008, должны вести себя так, как определено, когда вызывается или прерывается функцией перехвата сигнала, с единственным исключением: когда сигнал прерывает небезопасную функцию, а сигнал - Функция перехвата вызывает небезопасную функцию, поведение не определено.
Операции, которые получают значение
errno
и операции, которые присваивают значениеerrno
должен быть безопасным для асинхронного сигнала.Когда сигнал доставляется потоку, если действие этого сигнала указывает завершение, остановку или продолжение, весь процесс должен быть завершен, остановлен или продолжен, соответственно.
Тем не менее printf()
Семейство функций, в частности, отсутствует в этом списке и не может быть безопасно вызвано из обработчика сигнала.
Обновление POSIX 2016 расширяет список безопасных функций и включает, в частности, большое количество функций из <string.h>
, что является особенно ценным дополнением (или было особенно расстраивающим упущением). Список сейчас:
_Exit() getppid() sendmsg() tcgetpgrp()
_exit() getsockname() sendto() tcsendbreak()
abort() getsockopt() setgid() tcsetattr()
accept() getuid() setpgid() tcsetpgrp()
access() htonl() setsid() time()
aio_error() htons() setsockopt() timer_getoverrun()
aio_return() kill() setuid() timer_gettime()
aio_suspend() link() shutdown() timer_settime()
alarm() linkat() sigaction() times()
bind() listen() sigaddset() umask()
cfgetispeed() longjmp() sigdelset() uname()
cfgetospeed() lseek() sigemptyset() unlink()
cfsetispeed() lstat() sigfillset() unlinkat()
cfsetospeed() memccpy() sigismember() utime()
chdir() memchr() siglongjmp() utimensat()
chmod() memcmp() signal() utimes()
chown() memcpy() sigpause() wait()
clock_gettime() memmove() sigpending() waitpid()
close() memset() sigprocmask() wcpcpy()
connect() mkdir() sigqueue() wcpncpy()
creat() mkdirat() sigset() wcscat()
dup() mkfifo() sigsuspend() wcschr()
dup2() mkfifoat() sleep() wcscmp()
execl() mknod() sockatmark() wcscpy()
execle() mknodat() socket() wcscspn()
execv() ntohl() socketpair() wcslen()
execve() ntohs() stat() wcsncat()
faccessat() open() stpcpy() wcsncmp()
fchdir() openat() stpncpy() wcsncpy()
fchmod() pause() strcat() wcsnlen()
fchmodat() pipe() strchr() wcspbrk()
fchown() poll() strcmp() wcsrchr()
fchownat() posix_trace_event() strcpy() wcsspn()
fcntl() pselect() strcspn() wcsstr()
fdatasync() pthread_kill() strlen() wcstok()
fexecve() pthread_self() strncat() wmemchr()
ffs() pthread_sigmask() strncmp() wmemcmp()
fork() raise() strncpy() wmemcpy()
fstat() read() strnlen() wmemmove()
fstatat() readlink() strpbrk() wmemset()
fsync() readlinkat() strrchr() write()
ftruncate() recv() strspn()
futimens() recvfrom() strstr()
getegid() recvmsg() strtok_r()
geteuid() rename() symlink()
getgid() renameat() symlinkat()
getgroups() rmdir() tcdrain()
getpeername() select() tcflow()
getpgrp() sem_post() tcflush()
getpid() send() tcgetattr()
В результате вы либо в конечном итоге использовать write()
без поддержки форматирования, предоставляемой printf()
и т. д., или вы в конечном итоге устанавливаете флаг, который вы тестируете (периодически) в соответствующих местах вашего кода. Эта техника умело продемонстрирована в ответе Grijesh Chauhan.
Стандартные функции C и безопасность сигнала
chqrlie задает интересный вопрос, на который у меня есть не более чем частичный ответ:
Как получается, что большинство строковых функций
<string.h>
или функции класса символов из<ctype.h>
и еще много функций стандартной библиотеки C нет в списке выше? Реализация должна быть намеренно злой, чтобы сделатьstrlen()
небезопасно звонить из обработчика сигнала.
Для многих функций в <string.h>
, трудно понять, почему они не были объявлены безопасными асинхронными сигналами, и я бы согласился strlen()
яркий пример, наряду с strchr()
, strstr()
и т. д. С другой стороны, другие функции, такие как strtok()
, strcoll()
а также strxfrm()
являются довольно сложными и вряд ли будут безопасны для асинхронного сигнала. Так как strtok()
сохраняет состояние между вызовами, и обработчик сигнала не может легко определить, используется ли какая-то часть кода strtok()
будет испорчен. strcoll()
а также strxfrm()
функции работают с данными, чувствительными к локали, а загрузка локали включает в себя всевозможные настройки состояния.
Функции (макросы) из <ctype.h>
все чувствительны к локали, и поэтому могут столкнуться с теми же проблемами, что и strcoll()
а также strxfrm()
,
Мне трудно понять, почему математические функции из <math.h>
не безопасны для асинхронных сигналов, за исключением случаев, когда на них может влиять SIGFPE (исключение с плавающей запятой), хотя в эти дни я вижу единственное время для целочисленного деления на ноль. Подобная неопределенность возникает из <complex.h>
, <fenv.h>
а также <tgmath.h>
,
Некоторые из функций в <stdlib.h>
может быть освобожден - abs()
например. Другие конкретно проблематичны: malloc()
и семья являются яркими примерами.
Аналогичная оценка может быть сделана для других заголовков в стандарте C (2011), используемых в среде POSIX. (Стандарт C настолько ограничен, что его не интересует анализ в чисто стандартной среде C.) Эти помеченные как "зависящие от локали" небезопасны, поскольку манипулирование локалями может потребовать выделения памяти и т. Д.
<assert.h>
- Вероятно, не безопасно<complex.h>
- Возможно безопасно<ctype.h>
- Не безопасно<errno.h>
- Безопасный<fenv.h>
- Вероятно, не безопасно<float.h>
- Нет функций<inttypes.h>
- Чувствительные к локали функции (небезопасные)<iso646.h>
- Нет функций<limits.h>
- Нет функций<locale.h>
- Чувствительные к локали функции (небезопасные)<math.h>
- Возможно безопасно<setjmp.h>
- Не безопасно<signal.h>
- Позволил<stdalign.h>
- Нет функций<stdarg.h>
- Нет функций<stdatomic.h>
- Возможно, безопасно, возможно, не безопасно<stdbool.h>
- Нет функций<stddef.h>
- Нет функций<stdint.h>
- Нет функций<stdio.h>
- Не безопасно<stdlib.h>
- Не все в безопасности (некоторые разрешены, другие нет)<stdnoreturn.h>
- Нет функций<string.h>
- не все в безопасности<tgmath.h>
- Возможно безопасно<threads.h>
- Вероятно, не безопасно<time.h>
- зависит от локали (ноtime()
явно разрешено)<uchar.h>
- Зависимый от локали<wchar.h>
- Зависимый от локали<wctype.h>
- Зависимый от локали
Анализировать заголовки POSIX было бы... сложнее, потому что их много, и некоторые функции могли бы быть безопасными, но многие не будут... но также более простыми, потому что POSIX сообщает, какие функции безопасны по асинхронному сигналу (не многие из них). Обратите внимание, что заголовок, как <pthread.h>
имеет три безопасные функции и много небезопасных функций.
NB. Практически вся оценка функций и заголовков C в среде POSIX является полуобученной догадкой. Нет смысла делать однозначное заявление от органа по стандартизации.
Как избежать использования
printf
в обработчике сигнала?
Всегда избегай этого, скажет: просто не пользуйтесь
printf()
в обработчиках сигналов.По крайней мере, в системах, соответствующих POSIX, вы можете использовать
write(STDOUT_FILENO, ...)
вместоprintf()
, Однако форматирование может быть непростым: выведите int из обработчика сигнала, используя функции записи или асинхронной защиты.
Для целей отладки я написал инструмент, который проверяет, что вы на самом деле вызываете только функции на async-signal-safe
список и печатает предупреждающее сообщение для каждой небезопасной функции, вызываемой в контексте сигнала. Хотя это и не решает проблему вызова не асинхронно-безопасных функций из контекста сигнала, это, по крайней мере, помогает вам найти случаи, когда вы сделали это случайно.
Исходный код находится на GitHub. Работает от перегрузки signal/sigaction
затем временно угоня PLT
записи небезопасных функций; это приводит к тому, что вызовы небезопасных функций перенаправляются в оболочку.
Реализуйте свой собственный async-signal-safe snprintf("%d
и использовать write
Это не так плохо, как я думал, как преобразовать int в строку в C? имеет несколько реализаций.
Поскольку есть только два интересных типа данных, к которым могут обращаться обработчики сигналов:
sig_atomic_t
глобалыint
сигнальный аргумент
это в основном охватывает все интересные варианты использования.
Дело в том, что strcpy
также безопасный сигнал делает вещи еще лучше.
Программа POSIX, приведенная ниже, печатает для вывода количества получений SIGINT, которое вы можете запустить с помощью Ctrl + C
, а также и идентификатор сигнала.
Вы можете выйти из программы с Ctrl + \
(SIGQUIT).
main.c:
#define _XOPEN_SOURCE 700
#include <assert.h>
#include <limits.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
/* Calculate the minimal buffer size for a given type.
*
* Here we overestimate and reserve 8 chars per byte.
*
* With this size we could even print a binary string.
*
* - +1 for NULL terminator
* - +1 for '-' sign
*
* A tight limit for base 10 can be found at:
* https://stackru.com/questions/8257714/how-to-convert-an-int-to-string-in-c/32871108#32871108
*
* TODO: get tight limits for all bases, possibly by looking into
* glibc's atoi: https://stackru.com/questions/190229/where-is-the-itoa-function-in-linux/52127877#52127877
*/
#define ITOA_SAFE_STRLEN(type) sizeof(type) * CHAR_BIT + 2
/* async-signal-safe implementation of integer to string conversion.
*
* Null terminates the output string.
*
* The input buffer size must be large enough to contain the output,
* the caller must calculate it properly.
*
* @param[out] value Input integer value to convert.
* @param[out] result Buffer to output to.
* @param[in] base Base to convert to.
* @return Pointer to the end of the written string.
*/
char *itoa_safe(intmax_t value, char *result, int base) {
intmax_t tmp_value;
char *ptr, *ptr2, tmp_char;
if (base < 2 || base > 36) {
return NULL;
}
ptr = result;
do {
tmp_value = value;
value /= base;
*ptr++ = "ZYXWVUTSRQPONMLKJIHGFEDCBA9876543210123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"[35 + (tmp_value - value * base)];
} while (value);
if (tmp_value < 0)
*ptr++ = '-';
ptr2 = result;
result = ptr;
*ptr-- = '\0';
while (ptr2 < ptr) {
tmp_char = *ptr;
*ptr--= *ptr2;
*ptr2++ = tmp_char;
}
return result;
}
volatile sig_atomic_t global = 0;
void signal_handler(int sig) {
char key_str[] = "count, sigid: ";
/* This is exact:
* - the null after the first int will contain the space
* - the null after the second int will contain the newline
*/
char buf[2 * ITOA_SAFE_STRLEN(sig_atomic_t) + sizeof(key_str)];
enum { base = 10 };
char *end;
end = buf;
strcpy(end, key_str);
end += sizeof(key_str);
end = itoa_safe(global, end, base);
*end++ = ' ';
end = itoa_safe(sig, end, base);
*end++ = '\n';
write(STDOUT_FILENO, buf, end - buf);
global += 1;
signal(sig, signal_handler);
}
int main(int argc, char **argv) {
/* Unit test itoa_safe. */
{
typedef struct {
intmax_t n;
int base;
char out[1024];
} InOut;
char result[1024];
size_t i;
InOut io;
InOut ios[] = {
/* Base 10. */
{0, 10, "0"},
{1, 10, "1"},
{9, 10, "9"},
{10, 10, "10"},
{100, 10, "100"},
{-1, 10, "-1"},
{-9, 10, "-9"},
{-10, 10, "-10"},
{-100, 10, "-100"},
/* Base 2. */
{0, 2, "0"},
{1, 2, "1"},
{10, 2, "1010"},
{100, 2, "1100100"},
{-1, 2, "-1"},
{-100, 2, "-1100100"},
/* Base 35. */
{0, 35, "0"},
{1, 35, "1"},
{34, 35, "Y"},
{35, 35, "10"},
{100, 35, "2U"},
{-1, 35, "-1"},
{-34, 35, "-Y"},
{-35, 35, "-10"},
{-100, 35, "-2U"},
};
for (i = 0; i < sizeof(ios)/sizeof(ios[0]); ++i) {
io = ios[i];
itoa_safe(io.n, result, io.base);
if (strcmp(result, io.out)) {
printf("%ju %d %s\n", io.n, io.base, io.out);
assert(0);
}
}
}
/* Handle the signals. */
if (argc > 1 && !strcmp(argv[1], "1")) {
signal(SIGINT, signal_handler);
while(1);
}
return EXIT_SUCCESS;
}
Скомпилируйте и запустите:
gcc -std=c99 -Wall -Wextra -o main main.c
./main 1
После нажатия Ctrl + C пятнадцать раз, терминал показывает:
^Ccount, sigid: 0 2
^Ccount, sigid: 1 2
^Ccount, sigid: 2 2
^Ccount, sigid: 3 2
^Ccount, sigid: 4 2
^Ccount, sigid: 5 2
^Ccount, sigid: 6 2
^Ccount, sigid: 7 2
^Ccount, sigid: 8 2
^Ccount, sigid: 9 2
^Ccount, sigid: 10 2
^Ccount, sigid: 11 2
^Ccount, sigid: 12 2
^Ccount, sigid: 13 2
^Ccount, sigid: 14 2
где 2
номер сигнала для SIGINT
,
Проверено на Ubuntu 18.04. GitHub вверх по течению.
Один из методов, который особенно полезен в программах, в которых есть цикл выбора, - это записать один байт вниз по каналу при получении сигнала, а затем обработать сигнал в цикле выбора. Что-то вроде этого (обработка ошибок и другие детали опущены для краткости):
static int sigPipe[2];
static void gotSig ( int num ) { write(sigPipe[1], "!", 1); }
int main ( void ) {
pipe(sigPipe);
/* use sigaction to point signal(s) at gotSig() */
FD_SET(sigPipe[0], &readFDs);
for (;;) {
n = select(nFDs, &readFDs, ...);
if (FD_ISSET(sigPipe[0], &readFDs)) {
read(sigPipe[0], ch, 1);
/* do something about the signal here */
}
/* ... the rest of your select loop */
}
}
Если вам важно, какой это был сигнал, тогда байт вниз по каналу может быть номером сигнала.
Вы также можете использовать
write()
непосредственно, которая является функцией, безопасной для асинхронного сигнала.
#include <unistd.h>
int main(void) {
write(1,"Hello World!", 12);
return 0;
}
AFAI видит в очень надежных кодах, кодовый блок (тело) обработчика сигнала должен быть как можно короче. Я думаю, именно поэтому они разрабатывают тип, известный как sig_atomic_t
, Таким образом, метод, которому следует следовать, является принятым ответом выше.
Вы можете использовать printf в обработчиках сигналов, если вы используете библиотеку pthread. unix/posix указывает, что printf является атомарным для потоков, см. Дейв Бутенхоф ответ здесь: https://groups.google.com/forum/ Обратите внимание, что для получения более четкой картинки из вывода printfвы должны запустить ваше приложение в консоли (в linux используйте ctl+alt+f1 для запуска консоли 1), а не псевдо-tty, созданный в графическом интерфейсе.