Как предотвратить SIGPIPE (или обращаться с ними правильно) - PullRequest
232 голосов
/ 20 сентября 2008

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

Ответы [ 10 ]

229 голосов
/ 20 сентября 2008

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

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

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

signal(SIGPIPE, SIG_IGN);

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

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

148 голосов
/ 16 января 2009

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

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

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

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

111 голосов
/ 10 ноября 2009

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

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

Пример замены write(...) на send(...,MSG_NOSIGNAL) (см. Комментарий nobar )

char buf[888];
//write( sockfd, buf, sizeof(buf) );
send(    sockfd, buf, sizeof(buf), MSG_NOSIGNAL );
29 голосов
/ 27 февраля 2010

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

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

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

22 голосов
/ 27 января 2012

Ручка SIGPIPE Локально

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

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

15 голосов
/ 20 сентября 2008

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

6 голосов
/ 20 сентября 2008

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

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

Sam

3 голосов
/ 16 сентября 2015

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

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

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

Да, используйте select ().

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

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

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

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

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

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

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

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

3 голосов
/ 26 марта 2014

Руководство Linux говорит:

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

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

2 голосов
/ 12 февраля 2016

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

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

...