Как предотвратить SIGPIPE (или обращаться с ними правильно)
У меня есть небольшая серверная программа, которая принимает соединения через 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 при записи (). Это более удобно в библиотеках, где вам может не потребоваться глобальный обработчик сигналов для 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(...)
от 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 изначально находился в режиме ожидания, мы ничего не делаем. В противном случае мы проверяем, находится ли он на рассмотрении. Если это произойдет (что означает, что наши действия сгенерировали один или несколько SIGPIPE), то мы ждем его в этом потоке, таким образом очищая его статус ожидания (для этого мы используем sigtimedwait() с нулевым тайм-аутом; это нужно для того, чтобы избежать блокировки в сценарий, когда злонамеренный пользователь отправлял SIGPIPE вручную всему процессу: в этом случае мы увидим, что он ожидает обработки, но другой поток может обработать его до того, как мы получим изменение, чтобы ждать его). После очистки статуса ожидания мы разблокируем SIGPIPE в этой теме, но только если он изначально не был заблокирован.
Пример кода на https://github.com/kroki/XProbes/blob/1447f3d93b6dbf273919af15e59f35cca58fcc23/src/libxprobes.c#L156
Ручка 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, но при попытке использовать сокет вы получите ошибку чтения / записи, поэтому вам нужно будет проверить это.
Вы не можете предотвратить выход процесса на дальнем конце канала, и если он завершится до завершения записи, вы получите сигнал SIGPIPE. Если вы подадите сигнал SIG_IGN, то ваша запись вернется с ошибкой - и вам нужно отметить и отреагировать на эту ошибку. Просто перехватывать и игнорировать сигнал в обработчике - не очень хорошая идея - вы должны заметить, что канал теперь не функционирует, и изменить поведение программы, чтобы он больше не записывал данные в канал (потому что сигнал будет сгенерирован снова и проигнорирован снова, и вы попробуете снова, и весь процесс может продолжаться долго и тратить много энергии процессора).
Или я должен просто поймать SIGPIPE с помощью обработчика и игнорировать его?
Я считаю, что это правильно. Вы хотите знать, когда другой конец закрыл их дескриптор, и это то, что 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
... или может иметь функцию, которая удаляет определенный заблокированный сигнал.
Для получения дополнительной информации прочитайте справочную страницу.
Какова лучшая практика, чтобы предотвратить аварию здесь?
Либо отключите sigpipes для всех, либо поймайте и проигнорируйте ошибку.
Есть ли способ проверить, читает ли другая сторона строки?
Да, используйте select ().
select () здесь не работает, так как всегда говорит, что сокет доступен для записи.
Вы должны выбрать биты для чтения. Вы, вероятно, можете игнорировать биты записи.
Когда дальний конец закроет свой дескриптор файла, select скажет вам, что есть данные, готовые для чтения. Когда вы пойдете и прочитаете это, вы получите обратно 0 байтов, и именно так ОС сообщает вам, что дескриптор файла был закрыт.
Единственный раз, когда вы не можете игнорировать биты записи, это если вы отправляете большие тома, и существует риск того, что другой конец будет заблокирован, что может привести к заполнению ваших буферов. Если это произойдет, то попытка записи в дескриптор файла может привести к блокировке или неудаче вашей программы / потока. Тестирование select перед написанием защитит вас от этого, но не гарантирует, что другой конец исправен или что ваши данные будут доставлены.
Обратите внимание, что вы можете получить sigpipe из close(), а также когда вы пишете.
Закрыть сбрасывает любые буферизованные данные. Если другой конец уже был закрыт, то закрыть не удастся, и вы получите сигпайп.
Если вы используете буферизованный TCPIP, то успешная запись означает, что ваши данные были поставлены в очередь для отправки, но это не значит, что они были отправлены. Пока вы успешно не позвоните, вы не будете знать, что ваши данные были отправлены.
Сигпайп говорит вам, что что-то пошло не так, он не говорит вам, что или что вы должны с этим делать.
В руководстве по Linux сказано:
EPIPE Локальный конец был отключен на ориентированной на соединение розетке. В этом случае процесс также получит SIGPIPE, если не установлен MSG_NOSIGNAL.
Но для Ubuntu 12.04 это не правильно. Я написал тест для этого случая, и я всегда получаю EPIPE без SIGPIPE. SIGPIPE генерируется, если я пытаюсь записать в тот же сломанный сокет второй раз. Поэтому вам не нужно игнорировать SIGPIPE, если этот сигнал происходит, это означает логическую ошибку в вашей программе.