Отправка SIGINT в конкретный поток с помощью pthread_kill() приводит к завершению всего процесса

У меня очень большое многопоточное приложение на языке 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" в журнал, но по какой-то причине весь процесс завершается.

Другие вещи, которые я сейчас знаю:

  1. Раньше код работал так, как и ожидалось. С тех пор были внесены некоторые, казалось бы, несвязанные изменения, позволяющие приложению загружать данные из базы данных Postgres, а не из Oracle (старая реализация использовала Pro*C, новая использует libpq). В исходном коде функций, отвечающих за работу с потоками, ничего не менялось.

  2. Я проверил, какие потоки какие создают, какие сигналы отправляются (от/куда) и в каком порядке все это происходит. Но все это выглядело одинаково в старой и новой версиях (кроме того, новая версия перестает работать в тот момент, когда поток 1 отправляет SIGINT в поток 2, а старая версия продолжает работать с потоком 2, успешно печатая "Got EINTR" в журнал.

2.5 Я абсолютно уверен, что поток 1 отправляет сигнал SIGINT после того, как поток 2 отправляет сигнал ppoll().

  1. Я попробовал использовать gdb и с огромным трудом смог еще раз подтвердить, что правильная ветка получает SIGINT, но, к сожалению, не более того.

  2. Я попытался создать минимально воспроизводимый пример, но пока не смог его придумать (все работает так, как ожидалось).

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

Потоки и сигналы на самом деле не совпадают. Некоторые сигналы могут быть специфичными для потока, некоторые — нет. А для тех, кто не привязан к конкретному потоку, система IIRC может доставить сигнал в любой поток, который его не маскирует.

Some programmer dude 24.06.2024 17:59

@some-programmer-чувак Я знаю, но, к сожалению, код уже написан так. Также не объясняется, почему старая версия работает без проблем.

Kekers_Dev 24.06.2024 18:07

Есть ли какой-нибудь код для настройки расположения сигнала и/или маски сигнала?

Ian Abbott 24.06.2024 18:35

Единственное место, где маски сигналов каким-либо образом манипулируются, — это поток 2: создается переменная маски сигнала mask, затем инициализируется вызовом sigemptyset(), а затем передается в ppoll().

Kekers_Dev 24.06.2024 18:46

Распечатайте расположение SIGINT прямо перед ppoll в обеих версиях. Подозреваю, вы увидите, что Pro*C грубо установил собственный обработчик SIGINT тогда как libpq оставляет вам SIG_DFL. В последнем случае ваш процесс обрабатывает SIGINT по умолчанию, что является ненормальным завершением.

pilcrow 24.06.2024 19:08

Спасибо за предложение, попробую. Тем временем мне удалось сравнить маски сигналов потоков, созданных в старой и новой версиях, и они различаются по SigIgn и SigCgt, так что это тоже может быть частью картины.

Kekers_Dev 24.06.2024 19:20

@pilcrow, ты был прав, проблема была именно в этом. Для моей настройки было достаточно установить фиктивный обработчик сигнала SIGINT в потоке 2. Пожалуйста, опубликуйте ответ, чтобы я мог его принять.

Kekers_Dev 24.06.2024 23:07

@Kekers_Dev, опубликовал. Спасибо!

pilcrow 25.06.2024 05:23

(@Kekers_Dev, возможно, в этом нет необходимости, но просто для пояснения: расположение сигнала является общепроцессным, а не конкретным потоком. Поэтому не имеет значения, устанавливаете ли вы свой фиктивный обработчик в потоке 2, потоке 1 или main перед любым другим тред создается.)

pilcrow 25.06.2024 20:17
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
9
75
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Я думаю, вам просто не хватает обработчика сигнала.

Распределение сигналов работает для каждого процесса отдельно. По умолчанию сигнал SIGINT завершает текущий процесс. Это означает, что если вы не зарегистрировали обработчик сигнала (или вообще игнорируете сигнал), то, что произойдет, будет определяться расположением сигнала по умолчанию. Для SIGINT значением по умолчанию является завершение процесса (и, следовательно, всех его потоков).

Если у вас есть хотя бы один поток, который не заблокировал сигнал, вы не проигнорировали сигнал и не зарегистрировали ни одного обработчика сигнала, ваш процесс будет завершен. Обратите внимание, что «игнорируемые» сигналы и «заблокированные» сигналы — это две разные вещи.

Если вы вообще хотите иметь возможность перехватывать сигнал во время ppoll, вам придется зарегистрировать для него обработчик, а также убедиться, что его нет в передаваемом вами mask.

Когда поток 1 вызывает pthread_kill, ожидается, что поток 2 выйдет из ppoll() и напечатает в журнале «Got EINTR», но по какой-то причине весь процесс завершается.

Это ожидается только в том случае, если у вас зарегистрирован обработчик сигналов. В противном случае весь ваш процесс будет завершен, как и должно быть, из-за расположения по умолчанию для SIGINT.


Мой совет: внимательно прочитайте сигнал man 7, это развеет некоторые ваши сомнения и поможет вам лучше понять, как работают сигналы.

Вместо использования сигналов (которые несколько хрупки в многопоточной среде, поскольку они включают в себя большое количество состояний всего процесса), вы можете найти более надежную альтернативу прерыванию этого вызова ppoll() — создать канал с использованием pipe(2), добавить файл стороны чтения. дескриптор в набор опроса и запишите один байт на сторону записи, если вы хотите прервать опрос.

Спасибо, я рассмотрю это как вариант будущего рефакторинга.

Kekers_Dev 25.06.2024 12:30
Ответ принят как подходящий

Старая библиотека Pro*C грубо устанавливает обработчики сигналов, в том числе, предположительно, для SIGINT. libpq нет.

Таким образом, когда SIGINT доставляется во время ppoll, версия кода, поддерживающая Oracle, запускает действие SIGINT и продолжает работу. Версия с поддержкой Postgres этого не делает, поэтому вам придется завершить процесс SIG_DFL.

Как вы обнаружили, установка фиктивного или нуп-обработчика для SIGINT решает проблему.

Другие вопросы по теме