У меня очень большое многопоточное приложение на языке C (по этой и некоторым другим причинам я не могу предоставить весь код). Короче говоря, среди прочих есть два потока: поток 1 отправляет SIGINT
потоку 2, а поток 2 ожидает ppoll()
. Код выглядит примерно так:
Тема 1:
...
if (thread && pthread_kill(&thread, SIGINT) != 0)
{
fprintf(log1, "Got error: %s", strerror((int)errno));
}
...
thread
вот переменная pthread_t
, содержащая дескриптор потока 2.
Тема 2:
...
sigset_t mask;
int rc;
sigemptyset(mask);
...
while(true)
{
...
rc = ppoll(fds, nfds, NULL, mask);
if (rc < 0)
{
if (errno == EINTR) fprintf(log2, "Got EINTR");
else fprintf(log2, "Got error: %s", strerror((int)errno));
continue;
}
fprintf(log, "%d descriptor(s) ready", rc);
}
...
Когда поток 1 вызывает pthread_kill
, ожидается, что поток 2 упадет из ppoll()
и выведет "Got EINTR"
в журнал, но по какой-то причине весь процесс завершается.
Другие вещи, которые я сейчас знаю:
Раньше код работал так, как и ожидалось. С тех пор были внесены некоторые, казалось бы, несвязанные изменения, позволяющие приложению загружать данные из базы данных Postgres, а не из Oracle (старая реализация использовала Pro*C, новая использует libpq). В исходном коде функций, отвечающих за работу с потоками, ничего не менялось.
Я проверил, какие потоки какие создают, какие сигналы отправляются (от/куда) и в каком порядке все это происходит. Но все это выглядело одинаково в старой и новой версиях (кроме того, новая версия перестает работать в тот момент, когда поток 1 отправляет SIGINT
в поток 2, а старая версия продолжает работать с потоком 2, успешно печатая "Got EINTR"
в журнал.
2.5 Я абсолютно уверен, что поток 1 отправляет сигнал SIGINT
после того, как поток 2 отправляет сигнал ppoll()
.
Я попробовал использовать gdb
и с огромным трудом смог еще раз подтвердить, что правильная ветка получает SIGINT
, но, к сожалению, не более того.
Я попытался создать минимально воспроизводимый пример, но пока не смог его придумать (все работает так, как ожидалось).
У меня очень мало опыта работы с сигналами (и еще меньше опыта их использования в многопоточных приложениях, но это то, что есть), поэтому я, вероятно, не предоставил достаточно информации, но я не ожидаю точного ответа - просто некоторые идеи о том, как это узнать. что может быть причиной этой проблемы. Я обновлю вопрос, добавив более конкретную информацию, если это указано в комментариях.
@some-programmer-чувак Я знаю, но, к сожалению, код уже написан так. Также не объясняется, почему старая версия работает без проблем.
Есть ли какой-нибудь код для настройки расположения сигнала и/или маски сигнала?
Единственное место, где маски сигналов каким-либо образом манипулируются, — это поток 2: создается переменная маски сигнала mask
, затем инициализируется вызовом sigemptyset()
, а затем передается в ppoll()
.
Распечатайте расположение SIGINT прямо перед ppoll
в обеих версиях. Подозреваю, вы увидите, что Pro*C грубо установил собственный обработчик SIGINT тогда как libpq оставляет вам SIG_DFL. В последнем случае ваш процесс обрабатывает SIGINT по умолчанию, что является ненормальным завершением.
Спасибо за предложение, попробую. Тем временем мне удалось сравнить маски сигналов потоков, созданных в старой и новой версиях, и они различаются по SigIgn и SigCgt, так что это тоже может быть частью картины.
@pilcrow, ты был прав, проблема была именно в этом. Для моей настройки было достаточно установить фиктивный обработчик сигнала SIGINT в потоке 2. Пожалуйста, опубликуйте ответ, чтобы я мог его принять.
@Kekers_Dev, опубликовал. Спасибо!
(@Kekers_Dev, возможно, в этом нет необходимости, но просто для пояснения: расположение сигнала является общепроцессным, а не конкретным потоком. Поэтому не имеет значения, устанавливаете ли вы свой фиктивный обработчик в потоке 2, потоке 1 или main
перед любым другим тред создается.)
Я думаю, вам просто не хватает обработчика сигнала.
Распределение сигналов работает для каждого процесса отдельно. По умолчанию сигнал SIGINT завершает текущий процесс. Это означает, что если вы не зарегистрировали обработчик сигнала (или вообще игнорируете сигнал), то, что произойдет, будет определяться расположением сигнала по умолчанию. Для SIGINT значением по умолчанию является завершение процесса (и, следовательно, всех его потоков).
Если у вас есть хотя бы один поток, который не заблокировал сигнал, вы не проигнорировали сигнал и не зарегистрировали ни одного обработчика сигнала, ваш процесс будет завершен. Обратите внимание, что «игнорируемые» сигналы и «заблокированные» сигналы — это две разные вещи.
Если вы вообще хотите иметь возможность перехватывать сигнал во время ppoll
, вам придется зарегистрировать для него обработчик, а также убедиться, что его нет в передаваемом вами mask
.
Когда поток 1 вызывает pthread_kill, ожидается, что поток 2 выйдет из ppoll() и напечатает в журнале «Got EINTR», но по какой-то причине весь процесс завершается.
Это ожидается только в том случае, если у вас зарегистрирован обработчик сигналов. В противном случае весь ваш процесс будет завершен, как и должно быть, из-за расположения по умолчанию для SIGINT.
Мой совет: внимательно прочитайте сигнал man 7, это развеет некоторые ваши сомнения и поможет вам лучше понять, как работают сигналы.
Вместо использования сигналов (которые несколько хрупки в многопоточной среде, поскольку они включают в себя большое количество состояний всего процесса), вы можете найти более надежную альтернативу прерыванию этого вызова ppoll()
— создать канал с использованием pipe(2)
, добавить файл стороны чтения. дескриптор в набор опроса и запишите один байт на сторону записи, если вы хотите прервать опрос.
Спасибо, я рассмотрю это как вариант будущего рефакторинга.
Старая библиотека Pro*C грубо устанавливает обработчики сигналов, в том числе, предположительно, для SIGINT. libpq нет.
Таким образом, когда SIGINT доставляется во время ppoll
, версия кода, поддерживающая Oracle, запускает действие SIGINT и продолжает работу. Версия с поддержкой Postgres этого не делает, поэтому вам придется завершить процесс SIG_DFL.
Как вы обнаружили, установка фиктивного или нуп-обработчика для SIGINT решает проблему.
Потоки и сигналы на самом деле не совпадают. Некоторые сигналы могут быть специфичными для потока, некоторые — нет. А для тех, кто не привязан к конкретному потоку, система IIRC может доставить сигнал в любой поток, который его не маскирует.