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

86

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

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

Ю Хао
источник
12
Простой и не очень полезный ответ на вопрос в вашем заголовке: видите этот printfвызов в этом устройстве обработки сигналов? Удалите это.
Кейт Томпсон
6
Привет, Ю Хао! Думаю, вам будет очень интересно прочитать ссылку. «Используйте реентерабельные функции для более безопасной обработки сигналов». Я прочитал это спустя столько времени, я хотел бы поделиться с вами искусственным. Надеемся, вам понравится.
Grijesh Chauhan

Ответы:

58

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

Гриджеш Чаухан
источник
18
Считается лучшей практикой объявлятьvolatile sigatomic_t alarm_fired;
Василий Старынкевич
1
@GrijeshChauhan: если мы работаем в коде продукта, то мы не можем вызвать функцию паузы, поток может быть где угодно, когда появляется сигнал, поэтому в этом случае мы действительно не знаем, где хранить "if (alarm_fired) printf (" Дин! \ n ");" в коде.
панкадж кушваха
@pankajkushwaha да, вы правы, он страдает от состояния гонки
Grijesh Chauhan
@GrijeshChauhan, Есть две вещи, которых я не мог понять. 1. Как узнать, когда нужно проверять флаг? Так что будет несколько контрольных точек в коде почти в каждой точке печати. 2. Определенно будут условия гонки, когда сигнал может быть вызван до регистрации сигнала или сигнал может появиться после контрольной точки. Я думаю, что это поможет печати только в некоторых условиях, но не решит проблему полностью.
Даршан b
52

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

Стандарт C придерживается очень консервативного взгляда на то, что вы можете делать в обработчике сигналов:

ISO / IEC 9899: 2011 §7.14.1.1 signalФункция

¶5 Если сигнал возникает не в результате вызова функции abortили raise, поведение не определено, если обработчик сигнала ссылается на любой объект со статической продолжительностью хранения или хранения потока, который не является атомарным объектом без блокировки, кроме как путем присвоения значения объекту, объявленному как volatile sig_atomic_t, или обработчик сигнала вызывает любую функцию в стандартной библиотеке, кроме abortфункции, _Exitфункции, quick_exitфункции или signalфункции с первым аргументом, равным номеру сигнала, соответствующему сигналу, который вызвал вызов обработчик. Более того, если такой вызов signalфункции приводит к SIG_ERRвозврату, значение errnoнеопределенно. 252)

252) Если какой-либо сигнал генерируется асинхронным обработчиком сигнала, поведение не определено.

POSIX гораздо более щедр в отношении того, что вы можете делать в обработчике сигналов.

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

2016 POSIX обновления расширяет список безопасных функций включает, в частности, большое количество функций из <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 Чаухана .


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

Джонатан Леффлер
источник
Почему большинство строковых функций <string.h>или функций классов символов <ctype.h>и многих других функций стандартной библиотеки C нет в списке выше? Реализация должна быть преднамеренно злой, чтобы сделать strlen()вызов из обработчика сигнала небезопасным.
chqrlie
@chqrlie: интересный вопрос - посмотрите обновление (не было возможности разумно уместить столько в комментариях).
Джонатан Леффлер,
Спасибо за подробный анализ. Что касается <ctype.h>материала, это зависит от языкового стандарта и может вызвать проблемы, если сигнал прерывает функцию установки языкового стандарта, но после загрузки языкового стандарта их использование должно быть безопасным. Я предполагаю, что в некоторых сложных ситуациях загрузка данных локали может выполняться постепенно, что делает функции <ctype.h>небезопасными. Вывод остается: если есть сомнения, воздержитесь.
chqrlie
@chqrlie: Я согласен с тем, что в этой истории должна быть мораль. Если есть сомнения, воздержитесь . Хорошее резюме.
Джонатан Леффлер,
13

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

  1. Всегда избегайте этого, скажет: просто не используйте printf()в обработчиках сигналов.

  2. По крайней мере, в системах, совместимых с POSIX, вы можете использовать write(STDOUT_FILENO, ...)вместо printf(). Однако форматирование может быть непростым: напечатайте int из обработчика сигнала, используя функции записи или асинхронные функции.

алк
источник
1
Алк Always avoid it.значит? Избегать printf()?
Grijesh Chauhan 03
2
@GrijeshChauhan: Да, поскольку OP спрашивал, когда следует избегать использования printf()в обработчиках сигналов.
alk
Alk +1 для 2точки, проверьте OP с вопросом Как избежать использования printf()в обработчиках сигналов?
Grijesh Chauhan 03
7

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

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

dwks
источник
Запрос функции GCC: gcc.gnu.org/ml/gcc-help/2012-03/msg00210.html
Ciro Santilli 郝海东 冠状 病 六四 事件
1

Внедрите свой собственный безопасный асинхронный сигнал 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:
 * /programming/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: /programming/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 вверх по течению .

Чиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
источник
0

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

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

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

Джон Хэсколл
источник
-1

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

Drlolly
источник
3
Обработчики сигналов не выполняются в каком-то отдельном потоке, они выполняются в контексте потока, который выполнялся, когда произошло прерывание сигнала. Это совершенно неверный ответ.
itaych