Как и любой другой инструмент, POSIX-сигналы имеют свои правила, как их использовать грамотно, надежно и безопасно. Они испокон веков описаны в самом стандарте POSIX, в стандартах языков программирования, в manpages, однако и по сей день я нередко встречаю связанные с этим грубые ошибки даже в коде опытных разработчиков, что в коммерческих проектах, что в открытых. Поэтому давайте поговорим о важном еще раз (кстати, для новичков в мире разработки ПО: коммитить в открытые проекты исправления явных косяков в обработчиках POSIX-сигналов — прекрасный способ набить руку в опенсорсе и пополнить себе портфолио, благо, проектов с подобными ошибками немало).

1. Набор доступных вызовов из обработчика сигнала строго ограничен

Начнем с самого важного. Что происходит, когда процесс получает сигнал? Обработчик сигнала может быть вызван в любом из потоков процесса, для которого этот конктретный сигнал (например, SIGINT) не отмечен как заблокированный (blocked). Если таких потоков несколько, то ядро выбирает один из них — чаще всего, это будет основной поток программы, но это не гарантировано, и не стоит на это рассчитывать. Ядро создает на специальный фрейм на стеке, который, во-первых, нужен для непосредственно работы функции-обработчика сигнала, а во-вторых, в него сохраняются данные, необходимые для продолжения работы, такие как значения регистра счетчика команд (program counter register, адрес, с которого будет продолжено выполнение кода), специфичные для архитектуры регистры, которые необходимы для продолжения выполнения выполнявшегося кода, текущую маску сигналов потока, и т.д. После этого непосредственно в этом потоке вызывается функция-обработчик сигнала.

О чем это говорит? О том, что выполнение любого потока (который не заблокирован для обработки нашего сигнала) может быть прервано в любой момент. Абсолютно любой. Даже посреди выполнения любой функции, любого системного вызова. А теперь представим, если этот вызов у нас имеет какое-то статическое, глобальное или thread-local внутреннее состояние, например, буфер, какие-то флаги, мьютекс, или что-либо еще, то вызов функции еще раз, когда она еще не закончила работу, может привести к совершенно непредсказуемым результатам. В компьютерных науках про такую функцию говорят, что она non-reentrant (нереентерабельна).

Пусть мы используем какую-нибудь функцию из stdio.h, например, всем известную printf(). Она использует внутри статически выделенный буфер данных вместе со счетчиками и индексами, которые хранят объем данных и текущую позицию в буфере. Обновляется все это не атомарно, и если вдруг в момент выполнения printf() в каком-нибудь потоке мы поймаем сигнал и запустим его обработчик, который тоже вызовет printf(), то эта функция будет работать с некорректным внутренним состоянием, что в лучшем случае приведет просто к неправильному результату, а в худшем случае уронит всю программу в segmentation fault.

Другой пример: на большинстве платформ malloc() и free() не реентерабельны, потому что они используют внутри статическую структуру данных, в которой хранится, какие блоки памяти свободны. Проблема усугубляется тем, что malloc()/free() могут неявно использоваться в глубине других библиотечных функций, и об этом вы можете даже не подозревать.

Поэтому существует такое понятие, как async-signal-safety. А именно, стандарт POSIX явно предприсывает в обработчиках сигналов функции из строго ограниченного набора и ничего больше.

Список разрешенных функций
       Function              Notes
       abort()               Added in POSIX.1-001 TC1
       accept()
       access()
       aio_error()
       aio_return()
       aio_suspend()        
       alarm()

       bind()
       cfgetispeed()
       cfgetospeed()
       cfsetispeed()
       cfsetospeed()
       chdir()
       chmod()
       chown()
       clock_gettime()
       close()
       connect()
       creat()
       dup()
       dup()
       execl()               Added in POSIX.1-008;
                              
       execle()              
       execv()               Added in POSIX.1-008
       execve()
       _exit()
       _Exit()
       faccessat()           Added in POSIX.1-008
       fchdir()              Added in POSIX.1-008 TC1
       fchmod()
       fchmodat()            Added in POSIX.1-008
       fchown()
       fchownat()            Added in POSIX.1-008
       fcntl()
       fdatasync()
       fexecve()             Added in POSIX.1-008
       ffs()                 Added in POSIX.1-008 TC
       fork()                
       fstat()
       fstatat()             Added in POSIX.1-008
       fsync()
       ftruncate()
       futimens()            Added in POSIX.1-008
       getegid()
       geteuid()
       getgid()
       getgroups()
       getpeername()
       getpgrp()
       getpid()
       getppid()
       getsockname()
       getsockopt()
       getuid()
       htonl()               Added in POSIX.1-008 TC
       htons()               Added in POSIX.1-008 TC
       kill()
       link()
       linkat()              Added in POSIX.1-008
       listen()
       longjmp()             Added in POSIX.1-008 TC;
       lseek()
       lstat()
       memccpy()             Added in POSIX.1-008 TC
       memchr()              Added in POSIX.1-008 TC
       memcmp()              Added in POSIX.1-008 TC
       memcpy()              Added in POSIX.1-008 TC
       memmove()             Added in POSIX.1-008 TC
       memset()              Added in POSIX.1-008 TC
       mkdir()

       mkdirat()             Added in POSIX.1-008
       mkfifo()
       mkfifoat()            Added in POSIX.1-008
       mknod()               Added in POSIX.1-008
       mknodat()             Added in POSIX.1-008
       ntohl()               Added in POSIX.1-008 TC
       ntohs()               Added in POSIX.1-008 TC
       open()
       openat()              Added in POSIX.1-008
       pause()
       pipe()
       poll()
       posix_trace_event()
       pselect()
       pthread_kill()        Added in POSIX.1-008 TC1
       pthread_self()        Added in POSIX.1-008 TC1
       pthread_sigmask()     Added in POSIX.1-008 TC1
       raise()
       read()
       readlink()
       readlinkat()          Added in POSIX.1-008
       recv()
       recvfrom()
       recvmsg()
       rename()
       renameat()            Added in POSIX.1-008
       rmdir()
       select()
       sem_post()
       send()
       sendmsg()
       sendto()
       setgid()
       setpgid()
       setsid()
       setsockopt()
       setuid()
       shutdown()
       sigaction()
       sigaddset()
       sigdelset()
       sigemptyset()
       sigfillset()
       sigismember()
       siglongjmp()          Added in POSIX.1-008 TC; 
       signal()
       sigpause()
       sigpending()
       sigprocmask()
       sigqueue()
       sigset()
       sigsuspend()
       sleep()
       sockatmark()          Added in POSIX.1-001 TC
       socket()
       socketpair()
       stat()
       stpcpy()              Added in POSIX.1-008 TC
       stpncpy()             Added in POSIX.1-008 TC
       strcat()              Added in POSIX.1-008 TC
       strchr()              Added in POSIX.1-008 TC
       strcmp()              Added in POSIX.1-008 TC
       strcpy()              Added in POSIX.1-008 TC
       strcspn()             Added in POSIX.1-008 TC

       strlen()              Added in POSIX.1-008 TC
       strncat()             Added in POSIX.1-008 TC
       strncmp()             Added in POSIX.1-008 TC
       strncpy()             Added in POSIX.1-008 TC
       strnlen()             Added in POSIX.1-008 TC
       strpbrk()             Added in POSIX.1-008 TC
       strrchr()             Added in POSIX.1-008 TC
       strspn()              Added in POSIX.1-008 TC
       strstr()              Added in POSIX.1-008 TC
       strtok_r()            Added in POSIX.1-008 TC
       symlink()
       symlinkat()           Added in POSIX.1-008
       tcdrain()
       tcflow()
       tcflush()
       tcgetattr()
       tcgetpgrp()
       tcsendbreak()
       tcsetattr()
       tcsetpgrp()
       time()
       timer_getoverrun()
       timer_gettime()
       timer_settime()
       times()
       umask()
       uname()
       unlink()
       unlinkat()            Added in POSIX.1-008
       utime()
       utimensat()           Added in POSIX.1-008
       utimes()              Added in POSIX.1-008
       wait()
       waitpid()
       wcpcpy()              Added in POSIX.1-008 TC
       wcpncpy()             Added in POSIX.1-008 TC
       wcscat()              Added in POSIX.1-008 TC
       wcschr()              Added in POSIX.1-008 TC
       wcscmp()              Added in POSIX.1-008 TC
       wcscpy()              Added in POSIX.1-008 TC
       wcscspn()             Added in POSIX.1-008 TC
       wcslen()              Added in POSIX.1-008 TC
       wcsncat()             Added in POSIX.1-008 TC
       wcsncmp()             Added in POSIX.1-008 TC
       wcsncpy()             Added in POSIX.1-008 TC
       wcsnlen()             Added in POSIX.1-008 TC
       wcspbrk()             Added in POSIX.1-008 TC
       wcsrchr()             Added in POSIX.1-008 TC
       wcsspn()              Added in POSIX.1-008 TC
       wcsstr()              Added in POSIX.1-008 TC
       wcstok()              Added in POSIX.1-008 TC
       wmemchr()             Added in POSIX.1-008 TC
       wmemcmp()             Added in POSIX.1-008 TC
       wmemcpy()             Added in POSIX.1-008 TC
       wmemmove()            Added in POSIX.1-008 TC
       wmemset()             Added in POSIX.1-008 TC
       write()

Обратите внимание, что список функций отличается между разными версиями стандарта POSIX, причем изменения могут происходить в обе стороны. Например, fpathconf(), pathconf() и sysconf() в стандарте 2001 года считались безопасными, а в стандарте 2008 года уже перестали. fork() пока что относится к безопасным функциям, но есть планы удалить его из списка в следущих версиях стандарта по ряду причин.

А теперь самое главное. Внимательный глаз заметит, что в этом списке функций нет ни printf(), ни syslog(), ни malloc(). Вообще нет. Соответственно, использовать их и всё, что в теории может использовать их внутри себя, в обработчике сигналов нельзя. В std::cout и std::cerr в C++ писать тоже нельзя, эти операции тоже нереентерабельны.

Среди функций стандартной библиотеки языка C очень многие функции тоже нереентерабельны, например, почти все функции из <stdio.h>, многие функции из <string.h>, ряд функций из <stdlib.h> (некоторые, правда, напротив явно есть в списке разрешенных). Впрочем, стандарт языка C явно запрещает вызывать в обработчиках сигналов практически всё из стандартной библиотеки, кроме abort(), _Exit(), quick_exit() и самого signal():

ISO/IEC 9899:2011 §7.14.1.1 The signal function

5. If the signal occurs other than as the result of calling the abort or raise function, the behavior is undefined if ... the signal handler calls any function in the standard library other than the abort function, the _Exit function, the quick_exit function, or the signal function with the first argument equal to the signal number corresponding to the signal that caused the invocation of the handler.

Так что если вам уж очень сильно хочется что-то вывести в консольку из обработчика сигналов, можно сделать это старым дедовским методом:

#include <unistd.h> 
 ...
write(1,"Hello World!", 12); 

Но вообще, хороший практикой (кстати, явно рекомендуемой в документации libc) будет делать обработчики сигналов как можно более простыми и короткими: например, делать write() в pipe, а в другом потоке (или в основном event loop'е вашей программы) вы будете делать select() для этого pipe'а. Можно вообще ожидать и обрабатывать сигналы в специально выделенном для этого потоке (через sigwait(), заранее позаботившись о правильной маске). Или самый простой вариант: обработчик сигнала вообще сведется к установке переменной-флага, которая будет обрабатываться в основном цикле программы. Правда, с переменными-флагами тоже не все так просто, об этом в следущем пункте.

2. Используйте только volatile sig_atomic_t или atomic-типы в качестве флагов

Смотрим тот же пункт из стандарта языка C:

ISO/IEC 9899:2011 §7.14.1.1 The signal function

5. If the signal occurs other than as the result of calling the abort or raise function, the behavior is undefined if the signal handler refers to any object with static or thread storage duration that is not a lock-free atomic object other than by assigning a value to an object declared as volatile sig_atomic_t

В современных стандартах C++ упомянуто примерно то же самое. Логика тут точно такая же, как в предыдущем пункте: поскольку обработчик сигнала может быть вызван в абсолютно любой момент, важно, чтобы не-локальные переменные, с которыми вы в нем имеете дело, во-первых обновлялись атомарно (в противном случае при прерывании в неудачный момент есть риск получить в них некорректное содержимое), а во-вторых, поскольку с точки зрения выполняемой функции они изменяются "чем-то другим", важно чтобы обращения к ним не оптимизировались компилятором (иначе компилятор может решить, что между итерациями цикла изменение значения переменной невозможно и вообще выкинет эту проверку, либо поместит переменную в регистр процессора для оптимизации). Поэтому в качестве статических/глобальных флагов, изменяемых из обработчика сигнала, можно использовать или atomic-типы (при условии, что на вашей платформе они точно lock-free), либо специально созданный для этого тип sig_atomic_t со спецификатором volatile.

И боже упаси вас блокировать в обработчике сигналов какой-нибудь мьютекс, также используемый в остальной части программы или в хендлерах других сигналов — это самый прямой путь к дедлоку. Поэтому о conditional variables в качестве флагов можно тоже забыть.

3. Сохраняйте errno

Тут все просто: Если в обработчике сигнала вы вызываете какую-либо функцию, которая теоретически может изменить глобальную переменную errno, сохраняйте текущее значение errno в начале обработчика сигнала куда-нибудь, и восстанавливайте обратно в конце. Иначе вы можете сломать какой-либо внешний код, проверяющий этот самый errno.

4. Помните, что поведение signal() может сильно отличаться в разных ОС и даже в разных версиях одной ОС

Начнем с того, что у signal() есть весомый плюс: он входит в стандарт языка Си, а вот sigaction() — это уже чисто POSIX-штука. С другой стороны, поведение signal() может довольно сильно отличаться в разных ОС, и более того, в интернете встречаются упоминания, что поведение signal() может отличаться даже при разных версиях ядра Linux.

Для начала, немного истории.

В оригинальных системах UNIX, когда вызывался обработчик сигнала, ранее установленный с помощью signal(), обработчик сбрасывался на SIG_DFL, и система не блокировала доставку последующих экземпляров сигнала (в наше время это эквивалентно вызову sigaction() с флагами SA_RESETHAND | SA_NODEFER). Иными словами, получили сигнал, обработали -> обработчик сбросился на стандартный, и поэтому закончив обработку полученного сигнала мы должны были не забыть вызвать signal() еще раз и снова установить вместо стандартного обработчика нашу функцию. В System V было то же самое. Это было плохо, потому что следущий сигнал мог быть послан и доставлен процессу еще раз до того, как обработчик успел восстановить себя. Более того, быстрая доставка одного и того же сигнала могла привести к рекурсивным вызовам обработчика.

В BSD улучшили эту ситуацию, там когда сигнал получен, обработчики сигнала не сбрасываются на стандартные. Но это было не единственное изменение в поведении: там еще обработка всех последующих экземпляров этого сигнала блокируется на время обработки первого из них. Кроме того, некоторые блокирующие системные вызовы (типа read() или wait()) автоматически перезапускаются, если их прерывает обработчик сигнала. Семантика BSD эквивалентна вызову sigaction() с флагом SA_RESTART.

В Linux же ситуация следующая:

  • Системный вызов ядра signal() обеспечивает семантику System V.

  • По умолчанию в glibc 2 и новее функция-оболочка signal() не вызывает системный вызов ядра. Вместо этого он вызывает sigaction(), используя флаги, обеспечивающие семантику BSD. Это поведение по умолчанию обеспечивается до тех пор, пока определен макрос _BSD_SOURCE в glibc 2.19 и ранее или _DEFAULT_SOURCE в glibc 2.19 и новее. Если такой макрос не определен, то signal() предоставляет семантику System V. По умолчанию он определен :)

Итак, основные различия между signal() и sigaction() следущие:

  1. Функция signal() во многих реализациях не блокирует поступление других сигналов во время выполнения текущего обработчика; sigaction() в зависимости от флагов может блокировать другие сигналы, пока не вернется текущий обработчик.

  2. Системный вызов signal() (без учета оберток типа libc) по умолчанию на многих платформах сбрасывает обработчик сигнала обратно на SIG_DFL почти для всех сигналов. К чему это может привести, описано выше.

  3. Итого, поведение signal() варьируется в зависимости от платформы, системы и даже сборки libc — и стандарты допускают такие вариации. Короче говоря, при использовании signal() никто вам ничего не гарантирует. sigaction() гораздо более предсказуем.

Поэтому во избежании нежданчиков и проблем с переносимостью, рекомендация не использовать signal(), а предпочитать вместо него sigaction() в новом коде дана прямым текстом в The Open Group Base Specification.

5. Аккуратнее с fork() и execve()

Дочерний процесс, созданный с помощью fork(), наследует установленные обработчики сигналов своего родителя. Во время execve() обработчики сигналов сбрасывается на дефолтные, а вот настройки заблокированных (blocked) сигналов остаются неизменными для свежезапущенного процесса. Поэтому если вы, например, в родителе заигнорили SIGINT, SIGUSR1, или еще что-нибудь, а запущенный процесс рассчитывает на них, то это может привести к интересным эффектам.

6. Еще пара мелочей

Если процессу отправлено несколько стандартных (не realtime) сигналов, порядок, в котором они доставятся вашему процессу, может быть любым.

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

7. Читайте документацию

Всё, что я написал выше, там есть. Да и вообще, там есть очень много интересного, полезного и неожиданного, особенно в секциях Portability, Bugs и Known issues.

Например, мне очень нравится описание функции getlogin()/cuserid():

Sometimes it does not work at all, because some program messed up the utmp file. Often, it gives only the first 8 characters of the login name.

и дальше еще прекрасное:

Nobody knows precisely what cuserid() does; avoid it in portable programs.

На этом все. Безбажного вам кода!

Комментарии (18)


  1. dlinyj
    13.10.2021 11:24

    Не смотря на то, что статья важная и нужная, но это всё описано и разжовано в каждом учебнике разработки *nix. И мне казалось, что каждый, хоть как-то мало-мальски программирует под линукс знает об этом.


    1. F0iL Автор
      13.10.2021 11:40
      +5

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

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


      1. dlinyj
        13.10.2021 14:08

        Расскажите, что за проекты, где идёт разработка в юзерспейсе?


        1. F0iL Автор
          13.10.2021 14:15
          +3

          Не совсем понял вопрос. Что вы имели в виду под "где идет разработка в юзерспейсе"? Так-то у нас обычно практически все работает в юзерспейсе, не считая специфичных вещей типа кода ядерных модулей :)


          1. dlinyj
            13.10.2021 17:26

            Я просто не там ворочаюсь. Обычно меня зовут либо дрова разрабатывать, либо портировать линукс, либо с загрузчиками возится. А так, чтобы спокойно писать приложения в юзерспейсе не сталкивался.


            1. F0iL Автор
              13.10.2021 17:42
              +1

              Ну вот например, чем я занимался совсем давно: система видеонаблюдения для авиадиспетчеров на аэродромах. Обработка видео с камер и отображение в реальном времени, запись видео в архив, просмотр архива, и мониторинг всех частей комплекса (и софта, и железа, и самих камер, и стека коммутаторов). Всё под Linux, все в юзерспейсе.

              Чем занимался на прошлом месте: прошивки для Smart-телевизоров. Там тоже система на базе Linux, конкретно наша часть - доработка браузера на базе Chromium для работы в этой системе, исполнения веб-приложений и показа интерактивного ТВ-контента.

              Чем занимаюсь сейчас: есть большой класс разных устройств (скажем так, от маленьких домашне-офисных до огромных промышленных размером со здоровенный шкаф). В них вставляется специальная коммуникационная карточка, которая с одной стороны общается с этим устройством, собирая с него разные диагностические параметры (больше сотни значений) и отправляя ему команды, а с другой стороны - предоставляет данные во внешние системы по разным протоколам (SNMP, Modbus, BACnet, MQTT, SMTP, и всякое другое), может отправлять данные в облачные сервисы, предоставляет юзеру/сервис-инженеру Web-интерфейс для диагностики и конфигурирования, все это с IPv6 и серьезными требованиями к cybersecurity. На карточке точно так же крутится ОС на Linux-ядре, а сервисы, которые взаимодействуют с железкой, логгируют и анализируют данные, коммуницируют по разным протоколам, и т.д. - это всё именно бинарники, работающие в user-space.


              1. dlinyj
                13.10.2021 19:57

                Спасибо!


  1. checkpoint
    13.10.2021 17:47
    +2

    Функция signal() во многих реализациях не блокирует поступление других сигналов во время выполнения текущего обработчика; sigaction() в зависимости от флагов может блокировать другие сигналы, пока не вернется текущий обработчик.

    Часто использую только signal(), по этому тут же полез консультироваться у FreeBSD-шного man-а, где сказано:

    This signal() facility is a simplified interface to the more general sigaction(2) facility.

    Т.е., как миниму во FreeBSD библиотечная функция signal() это обертка к системному вызову sigaction().

    Далее читаем мануал по sigaction():

    Once a signal handler is installed, it normally remains installed until another sigaction() system call is made, or an execve(2) is performed. A signal-specific default action may be reset by setting sa_handler to SIG_DFL. The defaults are process termination, possibly with core dump; no action; stopping the process; or continuing the process.

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

    Отлегло. Пошел спать дальше.


    1. F0iL Автор
      13.10.2021 17:53
      +1

      Да, всё так. Собственно, я в статье сказал, что судя по "историческим записям", разработчики первых BSD, перенимая signal() из UNIX, поменяли поведение и избавились от этой засады. FreeBSD, как видно, тоже унаследовала это поведение.


      1. Oxyd
        14.10.2021 01:15

        Так зачем ломать то что хорошо работает? Другой вопрос почему в линукс не пошли по этому пути.


  1. kibb
    13.10.2021 23:39
    +4

    То, что вы описали, формально правильно с точки зрения стандартов C и POSIX. Разве что, вы не упомянули atomic_signal_fence(), которая становится крайне необходима при нетривиальной работе с сигналами и современными компиляторами.

    Содержательно ваша статья правильна для асинхронных сигналов. Но для них более предпочтительным подходом является sigwait(2) или в крайнем случае pipe trick. При использовании блока сигнала и sig*wait(2), обработчики не нужны, как они по сути не нужны и при использовании pipe trick (точнее, тут будет тривиальный заведомо async signal safe обработчик).

    А вот для синхронных сигналов, которые доставляются синхронно как результат исключения, и именно в нитку, которая исключение сгенерировала, все существенно по другому. Во-первых, обработчику известно состояние программы, потому что известно место, где произошло исключение. Во-вторых, в обработчике обычно можно пользоваться практически всеми сервисами runtime (если только мы не говорим об исключениях вследствие разрушения состояния).

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

    Современные типичные пользователи синхронных исключений - это всевозможные managed runtimes, которые реализуют на них барьеры для сборщиков мусора, парковку ниток, отслеживание выполнения (редких) блоков кода и тд.

    Кстати, забавно, что работа с сигналами не обеспечена в Rust stdlib. Лучшее из того, что я находил, signal-hook, выглядит неубедительно.


    1. F0iL Автор
      13.10.2021 23:55

      Спасибо, ценный комментарий.

      Я в статье упомянул про вариант, когда для обработки сигналов выделяют отдельный поток, имея в виду как раз sigwait(). Наверное, стоило раскрыть этот вариант поподробнее :) Допишу как время будет.

      Pipe trick - это, я так понял, когда вместо sigwait() отдельный поток делает блокирующий read() на пайп, а из обработчика сигнала мы шлем туда какой-нибудь символ через write() чтобы его разблочить. Об этом в черновике статьи тоже было, но решил не переусложнять.


      1. kibb
        14.10.2021 00:19
        +3

        Под pipe trick понимают запись из обработчика сигнала в канал, при этом или основной цикл делает select(2) на чтение из другого конца канала, или (реже, как вы сказали) выделенная нитка читает из канала. Выделенная нитка неудобна тем, что ее нужно все равно интегрировать в основной событийный цикл с select/poll/epoll/kqueue.

        Наскольно я помню, в линуксах есть signalfd(2), возвращающий файловый дескриптор, который можно использовать с select/epoll для синхронного оповещения о поступлении асинхронного сигнала напрямую, без создания канала.


    1. netch80
      19.10.2021 23:39

      Хотел написать примерно то же самое.
      Ещё можно добавить, что
      1) Есть техника, когда обработка какого-то сигнала разрешается «окнами»; даже если он генерируется асинхронно, обрабатываться он может синхронно. Особенно это типично для event-driven обработок. Есть системные вызовы pselect(), ppoll(), epoll_pwait() для поддержки такого стиля. В этом случае тоже снижаются ограничения на async-signal-safe сигналы.
      2) Можно через sigwait допускать обработку назначенным обработчиком, а можно просто получать номер сигнала и далее шедулить самому.
      3) Теперь есть ещё и signalfd для Linux и EV_SIGNAL для BSD kqueue.


  1. F0iL Автор
    13.10.2021 23:54

    (del, не та ветка)


  1. Ivanhoe
    14.10.2021 19:57
    +4

    в лучшем случае приведет просто к неправильному результату, а в худшем случае уронит всю программу в segmentation fault.

    Наоборот, первое - это худший случай, а второе -- лучший :)


  1. Nick0las
    14.10.2021 22:50

    Сигналы иногда еще приводят к неожиданным эффектам в неочевидных местах. Например мне попадалась такая ситуация:

    1. Есть драйвер который обрабатывает прерывания от железа и шлет сигналы процессам подписчикам когда что-то происходит в железе.

    2. Процесс-подписчик работает в том числе и с i2c, в моем случае это был USB-I2C на чипе mcp2221.

    В итоге иногда сигнал будит драйвер посреди транзакции и из за особенности драйвера транзакция фейлится. Но в драйвере есть retry поэтому снаружи (в userspace который инициирует i2c транзакции) ошибки не видно. Нашли когда мониторили ошибки i2c через Kernel trace. Исправили изменениями в драйвере.


    1. edo1h
      18.10.2021 02:30

      я бы отнёс этот случай к


      в идеале они должны вообще сводиться к установке переменной-флага, которая будет обрабатываться в основном цикле программы или в специально выделенном для этого потоке