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


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

10 216

10 ответов:

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

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

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

signal(SIGPIPE, SIG_IGN);

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

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

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

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

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

в связи с этим эффектом, а не генерируется сигнал sigpipe, ЭПИПЭ будут возвращены.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

в современной системе POSIX (т. е. Linux), вы можете использовать