Разработчик любого сколько-нибудь серьезного приложения рано или поздно вынужден озаботиться вопросами поведения программы в различных краевых и внештатных ситуациях: запрос досрочного завершения, внезапное закрытие терминала, обработка маловероятных ошибочных состояний. Во многих этих случаях приходится иметь дело с довольно примитивным механизмом межпроцессного взаимодействия — с обработкой сигналов.
Программист регистрирует обработчики нужных ему сигналов и забот не знает, очень часть допуская серьезную ошибку — выполняет в обработчике сигналов код, который там выполнять небезопасно: выделяет память, делает I/O, захватывает блокировки...
Сигналы прерывают нормальный ход исполнения программы и могут быть обработаны в произвольном потоке. Поток мог начать выделять память, захватить блокировку в аллокаторе и в этот момент быть прерванным сигналом. Если обработчик сигнала в свою очередь запросит выделение памяти... Будет повторный захват блокировки в одном и том же потоке. Неопределенное поведение.
И результат может быть самым неожиданным. Например, в OpenSSH в 2006 году была обнаружена критическая уязвимость, с возможностью удаленно получить root доступ к системам с запущенным sshd сервером. Баг непосредственно связан с кодом, вызывавшим malloc и free при обработке сигналов. Уязвимость исправили, но в 2020, спустя 14 лет, ee случайно занесли обратно. Ошибку снова обнаружили и исправили лишь в 2024 году, и кто знает сколько раз и кто воспользовался этой RegreSSHion за 4 года!
Очень легко можно продемонстрировать проблему на следующем примере
std::mutex global_lock;
int main() {
std::signal(SIGINT, [](int){
std::scoped_lock lock {global_lock};
printf("SIGINT!\n");
});
{
std::scoped_lock lock {global_lock};
printf("start long job\n");
sleep(10);
printf("end long job\n");
}
sleep(10);
}
Если мы скомпилируем эту программу под Linux (не забыв указать -pthread
), запустим и нажмем Ctrl+C
, то она зависнет навсегда из-за повторного захвата мьютекса одним и тем же потоком. Если же забудем -pthread
, то не зависнет и отработает «ожидаемым» образом.
Под Windows эта программа также работает «ожидаемо» из-за специфики обработки сигналов — там для обработки SIGINT
/SIGTERM
всегда неявно порождается новый поток.
В любом случае этот код некорректен из-за использования сигналонебезопасной функции внутри обработчика сигналов.
Обработка сигналов — вопрос крайне платформоспецифичный и зависит от конкретной прикладной задачи и архитектуры вашего приложения. Также это довольно сложный вопрос, если учитывать, что во время обработки одного сигнала нас могут прервать для обработки другого.
Наиболее часто встречаемое использование обработки сигналов — корректное завершение приложения, с очисткой ресурсов, закрытием соединений — graceful shutdown. В таком случае обычно обработка сигналов сводится к выставлению и проверке некоторого глобального флага.
Стандарты C и C++ описывают специальный целочисленный тип — sig_atomic_t
. При доступе к переменным этого типа гарантируется сигналобезопасность. На практике этот тип может оказаться просто алиасом для int
или long
. volatile sig_atomic_t
можно использовать в качестве глобального флага, выставляемого в обработчике сигналов. Но только в однопоточной среде. Тут volatile
необходим только для предотвращения нежелательной оптимизаций — компилятор не делает предположений о возможной обработке сигналов и прерывании нормального потока выполнения программы.
Нужно помнить, что volatile
не дает гарантий потокобезопасности. И в многопоточной среде необходимо использовать настоящие атомарные типы, поддерживаемые на вашей платформе. Например, std::atomic<int>
. Если, конечно, std::atomic<T>::is_lock_free
истинно.
- Делать обработчики сигналов как можно более простыми
- Отключать автоматический прием сигналов и выполнять их обработку в рамках обычного исполнения программы (см., например,
sigprocmask
иsigwait
) - Сверяться с документацией, безопасно ли использование той или иной функции в контексте обработчика сигналов
- Для флагов обработки сигналов использовать атомарные переменные, lock-free структуры или, если приложение однопоточное,
volatile sig_atomic_t
.
- https://man7.org/linux/man-pages/man7/signal-safety.7.html
- https://www.gnu.org/software/libc/manual/html_node/Blocking-Signals.html
- https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/signal
- https://ftp.gnu.org/old-gnu/Manuals/glibc-2.2.3/html_chapter/libc_24.html
- https://en.cppreference.com/w/cpp/utility/program/sig_atomic_t