- •Основная часть Механизм сигналов
- •Сигналы общего назначения и сигналы реального времени
- •Структура sigaction и функция sigaction()
- •Маскирование сигналов (signal masking)
- •Передача сигнала между потоками и возможности передачи данных
- •Синхронный и асинхронный приём сигналов
- •Вопросы асинхронной безопасности
- •Заключение
- •Список источников
Маскирование сигналов (signal masking)
Механизм маскирования (signal mask, sigset_t) позволяет временно блокировать доставку некоторых сигналов. Операции над наборами сигналов выполняются функциями sigemptyset, sigaddset, sigdelset, sigfillset. Для изменения текущей маски используется sigprocmask() (однопоточный контекст) или pthread_sigmask() (для текущего потока в многопоточной программе). При блокировке сигналов их доставка откладывается: стандарт различает отложенные (pending) сигналы и блокированные.
Маскирование — основной инструмент для предотвращения гонок при установке ожиданий сигнала: типичная схема — заблокировать нужные сигналы, настроить обработчик/создать поток приёма, затем вызвать атомарную смену маски с ожиданием (sigsuspend, sigwait, sigwaitinfo) — это устраняет классические race condition, возникающие при использовании pause().
Передача сигнала между потоками и возможности передачи данных
Как уже отмечалось, в многопоточных приложениях сигнал, посланный на уровень процесса, может быть доставлен любому потоку, который не блокирует этот сигнал. Для точечной адресации пользуются pthread_kill()/pthread_sigqueue(); при этом sigqueue()/pthread_sigqueue() обеспечивают передачу union sigval с данными.
siginfo_t — ключевая структура, которую ядро заполняет при доставке сигнала (если обработчик объявлен с SA_SIGINFO). В siginfo_t содержатся:
si_signo — номер сигнала; si_code — код причины (например, SI_USER, SI_QUEUE, SI_TIMER);
si_pid, si_uid — PID/UID отправителя; si_addr — адрес, вызвавший ошибку (для SIGSEGV);
si_value — union sigval, переданные через sigqueue(); и другие поля в зависимости от сигнала.
Благодаря si_value сигналы реального времени используются для передачи небольшой порции данных вместе с уведомлением (например, идентификатор задачи, код события, указатель), что удобно в системах реального времени.
Синхронный и асинхронный приём сигналов
Асинхронный приём — обработчик (signal handler) срабатывает в произвольный момент, когда поток занят другими действиями. Обработчик прерывает текущую работу и выполняется в контексте того же потока. Этот режим требует особенно осторожного обращения с разделяемыми ресурсами и соблюдения правил асинхронной безопасности.
Синхронный приём — поток явно ожидает сигнала, блокируя себя (через sigwait, sigwaitinfo, sigtimedwait, sigsuspend и т. п.). Такой подход позволяет обработке выполняться в обычном (не-асинхронном) контексте и безопасно вызывать большинство библиотечных функций, потому что приём происходит синхронно в управляющем коде. Для многопоточных приложений распространён шаблон: блокировать интересующие сигналы во всех потоках, затем выделить один «приёмный» поток, который делает sigwait() и синхронно обрабатывает события.
Вопросы асинхронной безопасности
Одной из ключевых проблем, возникающих при использовании механизма сигналов в операционных системах семейства POSIX, является обеспечение асинхронной безопасности программ. Асинхронная безопасность связана с особенностями обработки сигналов, которые могут возникать в любой момент времени и прерывать выполнение текущего потока управления. В отличие от обычных вызовов функций, инициируемых программой последовательно, сигнал доставляется процессу асинхронно и может привести к немедленному выполнению обработчика сигнала независимо от того, в каком состоянии находится программа в данный момент.
Суть проблемы заключается в том, что обработчик сигнала выполняется в контексте того же процесса или потока, который был прерван. При этом выполнение обработчика может происходить в момент, когда основная программа находится внутри системного вызова, выполняет библиотечную функцию или изменяет разделяемые структуры данных. В такой ситуации состояние памяти и внутренних структур библиотек может быть неполным или временно несогласованным. Если обработчик сигнала обращается к тем же данным или вызывает те же функции, что и прерванный код, возникает риск нарушения целостности данных, возникновения гонок или неопределённого поведения программы.
Особую опасность представляют функции стандартных библиотек, внутреннее состояние которых может изменяться в несколько этапов. Многие такие функции используют глобальные или статические структуры данных, буферы ввода-вывода или механизмы синхронизации. Если сигнал возникает во время выполнения подобной функции, обработчик сигнала может получить доступ к этим структурам до завершения их обновления. Повторный вызов той же функции или обращение к связанным данным может привести к повреждению внутреннего состояния библиотеки. В результате возможны различные негативные последствия: от некорректного вывода данных до аварийного завершения программы.
Для предотвращения подобных проблем стандарт POSIX вводит понятие асинхронно безопасных функций (async-signal-safe functions). К этой категории относятся функции, выполнение которых гарантированно не приводит к повреждению внутренних структур данных и не зависит от промежуточных состояний выполнения других функций. Такие функции либо являются атомарными по своей природе, либо реализованы таким образом, что их повторный вызов не нарушает корректность работы программы. Однако количество подобных функций ограничено, и большинство высокоуровневых библиотечных функций не относится к этой категории. Поэтому использование сигналов требует осторожного проектирования архитектуры программы.
Серьёзной проблемой при обработке сигналов являются состояния гонки (race conditions). Гонка возникает в ситуации, когда корректность программы зависит от взаимного расположения во времени двух или более событий, например поступления сигнала и выполнения определённой операции в основной программе. Поскольку сигнал может быть доставлен в любой момент, существует вероятность, что обработчик сигнала изменит состояние программы между двумя логически связанными операциями. Это может привести к нарушению предположений, на которых основан алгоритм программы.
Классическим примером такой ситуации является ожидание сигнала при помощи системных вызовов, блокирующих выполнение процесса. Если программа сначала проверяет некоторое условие, а затем переходит в состояние ожидания сигнала, существует промежуток времени между этими операциями, в течение которого сигнал может быть получен и обработан. После этого программа всё равно перейдёт в состояние ожидания, хотя соответствующее событие уже произошло. В результате процесс может заблокироваться на неопределённое время. Подобные ситуации особенно характерны для программ, использующих примитивные механизмы ожидания сигналов.
Для устранения подобных гонок используются механизмы маскирования сигналов и атомарные операции управления состоянием сигналов. Маскирование позволяет временно блокировать доставку определённых сигналов на период выполнения критически важного участка кода. В течение этого времени сигнал может быть помечен как ожидающий, но его обработка откладывается до снятия маски. Благодаря этому программа может безопасно подготовить необходимые структуры данных или перейти в режим ожидания сигнала без риска пропуска события. Правильное использование масок сигналов позволяет обеспечить атомарность операций, которые иначе были бы уязвимы для асинхронного прерывания.
В многопоточных программах проблема асинхронной безопасности становится ещё более сложной. В соответствии со стандартом POSIX сигнал, отправленный процессу, может быть доставлен любому потоку, который не блокирует данный сигнал. Это означает, что обработчик сигнала может быть выполнен в контексте произвольного потока, что значительно усложняет анализ поведения программы. Поток, получивший сигнал, может в этот момент выполнять критическую секцию, владеть блокировкой или находиться внутри библиотечной функции. Если обработчик сигнала попытается использовать те же механизмы синхронизации или обратиться к тем же данным, существует риск возникновения взаимных блокировок или повреждения данных.
Одним из распространённых подходов к решению этой проблемы является использование синхронной обработки сигналов. Вместо выполнения сложной логики непосредственно в обработчике сигнала программа может организовать специальный поток, отвечающий за получение сигналов в синхронном режиме. В этом случае сигналы блокируются во всех рабочих потоках, а выделенный поток ожидает их поступления при помощи специальных функций ожидания. Когда сигнал поступает, он обрабатывается в обычном контексте выполнения программы, без асинхронного прерывания других потоков. Такой подход значительно упрощает обеспечение корректности и повышает надёжность программ.
Дополнительным аспектом асинхронной безопасности является необходимость минимизации действий, выполняемых непосредственно в обработчике сигнала. Поскольку обработчик может прерывать выполнение программы в произвольный момент, он должен выполнять только наиболее простые и предсказуемые операции. В большинстве случаев обработчик используется лишь для фиксации факта наступления события, например установки специального флага или записи уведомления в заранее подготовленный механизм передачи сообщений. Основная обработка события выполняется позже, в обычном потоке управления программы.
Таким образом, вопросы асинхронной безопасности являются центральным элементом проектирования программ, использующих механизм сигналов. Асинхронная природа сигналов создаёт потенциальные угрозы для корректности выполнения программы, включая гонки, повреждение данных и взаимные блокировки. Для их предотвращения используются специальные методы: ограничение набора функций, вызываемых в обработчиках сигналов, маскирование сигналов при выполнении критических операций, синхронные механизмы ожидания сигналов и архитектурные решения, позволяющие минимизировать асинхронное взаимодействие. Соблюдение этих принципов позволяет существенно повысить надёжность программных систем, работающих в условиях параллелизма и асинхронных событий.
