Как предотвратить SIGPIPEs (или обрабатывать их должным образом)

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

10 ответов


вы обычно хотите игнорировать 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 флаг на вашем вызове send (2).

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

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 создали один или несколько Сигпайпов), то мы ждем его в этом потоке, очищая его состояние ожидания (для этого мы используем 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 без SIGPIPE. Сигнал sigpipe, это genereated если я стараюсь писать на те же сломанные гнезда второй раз. Поэтому вам не нужно игнорировать SIGPIPE, если этот сигнал происходит, это означает логическую ошибку в вашей программе.


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

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

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

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

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

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

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

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

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

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

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

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


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