Як уникнути використання 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 (" Ding! \ n ");" в коді.
pankaj kushwaha

@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 набагато щедріший щодо того, що ви можете зробити в обробнику сигналів.

Концепції сигналів у виданні 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()et al, або встановлюєте прапор, який ви тестуєте (періодично) у відповідних місцях у коді. Цей метод вміло продемонстрував в відповідь по Grijesh Чаухан .


Стандартні функції С та безпека сигналу

chqrlie задає цікаве запитання, на яке я маю лише часткову відповідь:

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

Для багатьох з функцій <string.h>, то важко зрозуміти , чому вони не були визнані безпечним асинхронним сигнал, і я погоджуюся strlen()є яскравим прикладом, поряд з strchr(), strstr()і так далі З іншого боку, іншими функціями , такими як strtok(), strcoll()і strxfrm()досить складні і навряд чи будуть безпечними для асинхронного сигналу. Оскільки strtok()зберігається стан між дзвінками, і обробник сигналу не міг легко визначити, чи буде якась частина коду, який використовується strtok(), зіпсована. Функції strcoll()and 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>має три безпечні функції та багато небезпечних функцій.

Примітка: Майже вся оцінка функцій і заголовків 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

2
@GrijeshChauhan: Так, оскільки OP запитував, коли уникати використання printf()в обробниках сигналів.
alk

Alk +1 для 2точки, перевірте ОП з запитом Як уникнути використання printf()в обробниках сигналів?
Grijesh Chauhan

7

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

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



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

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


3
Обробники сигналів не працюють у якомусь окремому потоці, вони працюють у контексті потоку, який працював, коли відбулося переривання сигналу. Ця відповідь абсолютно помилкова.
ітайч
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.