Как избежать использования printf в обработчике сигналов?

Так как printf не является реентерабельным, он не должен быть безопасным для его использования в обработчике сигналов. Но я видел множество примеров кодов, которые используют printf таким образом.

Итак, мой вопрос: когда нам нужно избегать использования printf в обработчике сигнала и есть ли рекомендуемая замена?

Ответ 1

Вы можете использовать некоторую переменную-флаг, установить этот флаг в обработчике сигнала и на основе этого флага вызвать функцию 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().
Список разрешенных функций на странице руководства, вызов этой функции в обработчике сигналов безопасен.

Ответ 2

Основная проблема заключается в том, что если сигнал прерывает 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, должны вести себя так, как это определено, когда вызывается или прерывается функцией перехвата сигнала, с единственным исключением: когда сигнал прерывает небезопасную функцию, а сигнал - Функция catch вызывает небезопасную функцию, поведение не определено.

Операции, которые получают значение 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() и др., Либо заканчиваете тем, что устанавливаете флаг, который вы проверяете (периодически) в соответствующих местах вашего кода. Эта техника умело продемонстрирована в ответе Грижеша Чаухана.


Стандартные функции C и безопасность сигнала

chqrlie задает интересный вопрос, на который у меня есть не более чем частичный ответ:

Почему большинство строковых функций из <string.h> или функций класса символов из <ctype.h> и многих других функций стандартной библиотеки C отсутствуют в списке выше? Реализация должна быть намеренно злой, чтобы сделать strlen() небезопасным для вызова из обработчика сигнала.

Для многих функций в <string.h> трудно понять, почему они не были объявлены безопасными как async-signal, и я согласился бы, что 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() и family являются яркими примерами.

Аналогичная оценка может быть сделана для других заголовков в стандарте C (2011), используемых в среде POSIX. (Стандарт C настолько ограничен, что не заинтересован в их анализе в чисто стандартной среде C.) Эти помеченные как "зависимые от локали" небезопасны, поскольку манипулирование локалями может потребовать выделения памяти и т.д.

  • <assert.h> - вероятно, не безопасно
  • <complex.h> - возможно безопасно
  • <ctype.h> - небезопасно
  • <errno.h> - Сейф
  • <fenv.h> - вероятно, не безопасно
  • <float.h> - Нет функций
  • <inttypes.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> - зависит от <uchar.h>
  • <wchar.h> - зависит от локали
  • <wctype.h> - зависит от локали

Анализировать заголовки POSIX будет... сложнее, поскольку их много, и некоторые функции могут быть безопасными, но многие не будут... но также проще, потому что POSIX сообщает, какие функции безопасны по асинхронному сигналу (не многие из них). Обратите внимание, что заголовок типа <pthread.h> имеет три безопасные функции и множество небезопасных функций.

NB. Практически вся оценка функций и заголовков C в среде POSIX является полуобученной догадкой. Это не имеет смысла окончательное утверждение от органа по стандартам.

Ответ 3

Как избежать использования printf в обработчике сигналов?

Ответ 4

В целях отладки я написал инструмент, который проверяет, что вы на самом деле вызываете только функции из списка async-signal-safe, и печатает предупреждающее сообщение для каждой небезопасной функции, вызываемой в контексте сигнала. Хотя это не решает проблему вызова не асинхронно-безопасных функций из контекста сигнала, по крайней мере, это помогает вам найти случаи, когда вы сделали это случайно.

Исходный код находится на GitHub. Он работает, перегружая signal/sigaction, а затем временно signal/sigaction записи PLT небезопасных функций; это приводит к тому, что вызовы небезопасных функций перенаправляются в оболочку.

Ответ 5

Один метод, который особенно полезен в программах, которые имеют цикл выбора, состоит в том, чтобы написать один байт по трубе при поступлении сигнала, а затем обработать сигнал в цикле выбора. Что-то в этих строках (обработка ошибок и другие сведения опущены для краткости):

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 */
    }
}

Если вам не нравится, какой сигнал он был, то байтом по трубе может быть номер сигнала.

Ответ 6

Вы можете использовать printf в обработчиках сигналов, если вы используете библиотеку pthread. unix/posix указывает, что printf является атомарным для потоков cf Dave Butenhof здесь: https://groups.google.com/forum/#!topic/comp.programming.threads/1-bU71nYgqw Обратите внимание, что для получения более четкой картины вывода printf, вы должны запустить свое приложение в консоли (при использовании linux ctl + alt + f1 для запуска консоли 1), а не псевдо-tty, созданный графическим интерфейсом.

Ответ 7

Реализуйте свой собственный async-signal-safe snprintf("%d и используйте write

Это не так плохо, как я думал, как преобразовать int в строку в C? имеет несколько реализаций.

Поскольку есть только два интересных типа данных, к которым могут обращаться обработчики сигналов:

  • sig_atomic_t globals
  • аргумент типа 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:
 * /questions/40375/how-to-convert-an-int-to-string-in-c32871108#32871108
 *
 * TODO: get tight limits for all bases, possibly by looking into
 * glibc atoi: https://stackoverflow.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 вверх по течению.

Ответ 8

AFAI видит в очень надежных кодах, кодовый блок (тело) обработчика сигнала должен быть как можно короче. Я думаю, что именно поэтому они разработали тип, известный как sig_atomic_t. Таким образом, метод, которому следует следовать, является принятым ответом выше.