Как предотвратить появление SIGPIPEs (или правильно обращаться с ними)

StackOverflow https://stackoverflow.com/questions/108183

Вопрос

У меня есть небольшая серверная программа, которая принимает подключения по TCP или локальному сокету UNIX, считывает простую команду и, в зависимости от команды, отправляет ответ.Проблема в том, что иногда клиент может не интересоваться ответом и завершать работу раньше, поэтому запись в этот сокет вызовет сбой SIGPIPE и приведет к сбою моего сервера.Какова наилучшая практика для предотвращения сбоя здесь?Есть ли способ проверить, продолжает ли другая сторона строки читать?(select(), похоже, здесь не работает, поскольку он всегда говорит, что сокет доступен для записи).Или я должен просто перехватить SIGPIPE с помощью обработчика и игнорировать его?

Это было полезно?

Решение

Обычно вы хотите игнорировать SIGPIPE и обработайте ошибку непосредственно в вашем коде.Это связано с тем, что обработчики сигналов в C имеют много ограничений на то, что они могут делать.

Самый портативный способ сделать это - установить SIGPIPE обработчик для SIG_IGN.Это предотвратит любую запись в сокет или канал, вызывающую SIGPIPE сигнал.

Игнорировать SIGPIPE подайте сигнал, используя следующий код:

signal(SIGPIPE, SIG_IGN);

Если вы используете send() вызовите, другой вариант - использовать MSG_NOSIGNAL вариант, который превратит SIGPIPE поведение отключено для каждого вызова.Обратите внимание, что не все операционные системы поддерживают MSG_NOSIGNAL Отметить.

Наконец, вы также можете захотеть рассмотреть SO_SIGNOPIPE флаг сокета, который может быть установлен с помощью setsockopt() в некоторых операционных системах.Это предотвратит SIGPIPE из-за того, что он вызван записью только в сокеты, на которые он установлен.

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

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

В большинстве систем на базе BSD (macOS, FreeBSD ...) (при условии, что вы используете C / C ++) вы можете сделать это с помощью:

int set = 1;
setsockopt(sd, SOL_SOCKET, SO_NOSIGPIPE, (void *)&set, sizeof(int));

При этом вместо генерируемого сигнала SIGPIPE будет возвращен EPIPE.

Я очень опаздываю на вечеринку, но SO_NOSIGPIPE не переносим и может не работать в вашей системе (похоже, это связано с BSD).

Хорошая альтернатива, если вы используете, скажем, систему Linux без SO_NOSIGPIPE было бы установить MSG_NOSIGNAL отметьте свой вызов для отправки (2).

Пример замены write(...) Автор: send(...,MSG_NOSIGNAL) (см . нобаркомментарий автора)

char buf[888];
//write( sockfd, buf, sizeof(buf) );
send(    sockfd, buf, sizeof(buf), MSG_NOSIGNAL );

В этом Публикация Я описал возможное решение для случая Solaris, когда ни SO_NOSIGPIPE, ни MSG_NOSIGNAL недоступны.

Вместо этого мы должны временно подавить SIGPIPE в текущем потоке, который выполняет библиотечный код.Вот как это сделать:чтобы подавить SIGPIPE, мы сначала проверяем, находится ли он в ожидании.Если это произойдет, это означает, что он заблокирован в этом потоке, и нам ничего не нужно делать.Если библиотека сгенерирует дополнительный SIGPIPE, он будет объединен с ожидающим, и это нецелесообразно.Если SIGPIPE не находится на рассмотрении, то мы блокируем его в этом потоке, а также проверяем, был ли он уже заблокирован.Тогда мы можем свободно выполнять наши записи.Когда мы хотим восстановить SIGPIPE в исходное состояние, мы делаем следующее:если SIGPIPE изначально находился на рассмотрении, мы ничего не делаем.В противном случае мы проверяем, находится ли оно на рассмотрении в данный момент.Если это произойдет (что означает, что действия out сгенерировали один или несколько SIGPIPEs), то мы ждем его в этом потоке, таким образом очищая его статус ожидания (для этого мы используем sigtimedwait() с нулевым таймаутом;это делается для того, чтобы избежать блокировки в сценарии, когда злоумышленник вручную отправлял SIGPIPE всему процессу:в этом случае мы увидим, что оно находится в ожидании, но другой поток может обработать его до того, как мы внесем изменения, чтобы дождаться его).После снятия статуса ожидания мы разблокируем SIGPIPE в этой теме, но только в том случае, если он не был заблокирован изначально.

Пример кода на https://github.com/kroki/XProbes/blob/1447f3d93b6dbf273919af15e59f35cca58fcc23/src/libxprobes.c#L156

Обрабатывать SIGPIPE Локально

Обычно лучше всего обрабатывать ошибку локально, а не в глобальном обработчике событий signal, поскольку локально у вас будет больше контекста относительно того, что происходит и какие меры следует предпринять.

У меня есть уровень связи в одном из моих приложений, который позволяет моему приложению взаимодействовать с внешним аксессуаром.Когда возникает ошибка записи, я создаю исключение and на уровне связи и позволяю ему всплыть в блоке try catch, чтобы обработать его там.

Код:

Код для игнорирования сигнала SIGPIPE, чтобы вы могли обрабатывать его локально, таков:

// We expect write failures to occur but we want to handle them where 
// the error occurs rather than in a SIGPIPE handler.
signal(SIGPIPE, SIG_IGN);

Этот код предотвратит появление сигнала SIGPIPE, но вы получите ошибку чтения / записи при попытке использовать сокет, поэтому вам нужно будет проверить это.

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

Или я должен просто перехватить SIGPIPE с помощью обработчика и игнорировать его?

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

Сэм

В руководстве по Linux говорилось:

EPIPE Локальный конец был отключен на сокете, ориентированном на подключение .В этом случае процесс также получит SIGPIPE если не установлен MSG_NOSIGNAL.

Но для Ubuntu 12.04 это неправильно.Я написал тест для этого случая, и я всегда получаю EPIPE без SIGPIPE.SIGPIPE генерируется, если я пытаюсь записать в тот же сломанный сокет второй раз.Таким образом, вам не нужно игнорировать SIGPIPE, если этот сигнал появляется, это означает логическую ошибку в вашей программе.

Какова наилучшая практика для предотвращения сбоя здесь?

Либо отключите sigpipes, как у всех, либо перехватите и проигнорируйте ошибку.

Есть ли способ проверить, продолжает ли другая сторона строки читать?

Да, используйте функцию select().

select(), похоже, здесь не работает, поскольку он всегда говорит, что сокет доступен для записи.

Вам нужно выбрать на Читать биты.Вероятно, вы можете игнорировать писать биты.

Когда дальний конец закроет свой дескриптор файла, select сообщит вам, что есть данные, готовые к чтению.Когда вы зайдете и прочитаете это, вы получите обратно 0 байт, именно так операционная система сообщает вам, что дескриптор файла был закрыт.

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

Обратите внимание, что вы можете получить sigpipe из close(), а также при записи.

Close удаляет все буферизованные данные.Если другой конец уже был закрыт, то закрыть не удастся, и вы получите sigpipe.

Если вы используете буферизованный TCPIP, то успешная запись просто означает, что ваши данные были поставлены в очередь для отправки, это не значит, что они были отправлены.Пока вы успешно не вызовете close, вы не будете знать, что ваши данные были отправлены.

Sigpipe сообщает вам, что что-то пошло не так, но не сообщает, что именно или что вы должны с этим делать.

В современной системе POSIX (т. е.Linux), вы можете использовать sigprocmask() функция.

#include <signal.h>

void block_signal(int signal_to_block /* i.e. SIGPIPE */ )
{
    sigset_t set;
    sigset_t old_state;

    // get the current state
    //
    sigprocmask(SIG_BLOCK, NULL, &old_state);

    // add signal_to_block to that existing state
    //
    set = old_state;
    sigaddset(&set, signal_to_block);

    // block that signal also
    //
    sigprocmask(SIG_BLOCK, &set, NULL);

    // ... deal with old_state if required ...
}

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

Для получения дополнительной информации прочтите справочная страница.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top