У меня есть небольшая серверная программа, которая принимает соединения в TCP или локальном сокете UNIX, читает простую команду и, в зависимости от команды, отправляет ответ. Проблема в том, что клиент может иногда не интересоваться ответом и уходит рано, поэтому запись в этот сокет приведет к сбою SIGPIPE и сбою моего сервера. Какая самая лучшая практика для предотвращения аварии здесь? Есть ли способ проверить, читает ли другая сторона строки? (select(), похоже, не работает здесь, поскольку он всегда говорит, что сокет доступен для записи). Или я должен просто захватить SIGPIPE обработчиком и игнорировать его?
Как предотвратить SIGPIPE (или обрабатывать их должным образом)
Ответ 1
Обычно вы хотите игнорировать SIGPIPE
и обрабатывать ошибку непосредственно в коде. Это связано с тем, что обработчики сигналов в C имеют множество ограничений на то, что они могут делать.
Самый переносимый способ сделать это - установить обработчик SIGPIPE
на SIG_IGN
. Это предотвратит возникновение сигнала сокета или трубки, вызывающего сигнал SIGPIPE
.
Чтобы игнорировать сигнал SIGPIPE
, используйте следующий код:
signal(SIGPIPE, SIG_IGN);
Если вы используете вызов send()
, другой вариант - использовать параметр MSG_NOSIGNAL
, который отключает поведение SIGPIPE
для каждого вызова. Обратите внимание, что не все операционные системы поддерживают флаг MSG_NOSIGNAL
.
Наконец, вы также можете рассмотреть флаг сокета SO_SIGNOPIPE
, который можно установить с помощью setsockopt()
в некоторых операционных системах. Это предотвратит вызвав SIGPIPE
от записи только к сокетам, на которые он установлен.
Ответ 2
Другой способ - изменить сокет, чтобы он никогда не генерировал SIGPIPE при записи(). Это более удобно в библиотеках, где вам может не понадобиться глобальный обработчик сигналов для SIGPIPE.
В большинстве систем на базе BSD (MacOS, FreeBSD...) (при условии, что вы используете C/С++), вы можете сделать это с помощью
int set = 1;
setsockopt(sd, SOL_SOCKET, SO_NOSIGPIPE, (void *)&set, sizeof(int));
При этом вместо генерируемого сигнала SIGPIPE возвращается EPIPE.
Ответ 3
Я очень опаздываю на вечеринку, но 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 );
Ответ 4
В этой post я описал возможное решение для случая Solaris, когда не доступны ни SO_NOSIGPIPE, ни MSG_NOSIGNAL.
Вместо этого мы должны временно отключить SIGPIPE в текущем потоке, который выполняет библиотечный код. Вот как это сделать: чтобы подавить SIGPIPE, мы сначала проверяем, ожидает ли он. Если это так, это означает, что он заблокирован в этом потоке, и мы ничего не должны делать. Если библиотека генерирует дополнительный SIGPIPE, она будет объединена с ожидающей, и что нет-op. Если SIGPIPE не находится в ожидании, мы блокируем его в этом потоке, а также проверяем, был ли он уже заблокирован. Затем мы можем исполнять наши записи. Когда мы хотим восстановить SIGPIPE в исходное состояние, мы делаем следующее: если SIGPIPE ожидалось первоначально, мы ничего не делаем. В противном случае мы проверяем, ожидает ли он сейчас. Если это так (что означает, что из-за каких-либо действий был сгенерирован один или несколько SIGPIPE), то мы ждем его в этом потоке, тем самым очищаем его ожидающий статус (для этого мы используем sigtimedwait() с нулевым таймаутом, чтобы избежать блокировки в сценарий, в котором злоумышленник отправил SIGPIPE вручную для всего процесса: в этом случае мы увидим его в ожидании, но другой поток может справиться с ним, прежде чем у нас появятся изменения, чтобы дождаться его). После очистки ожидающего статуса мы разблокируем SIGPIPE в этом потоке, но только если он не был заблокирован изначально.
Пример кода в https://github.com/kroki/XProbes/blob/1447f3d93b6dbf273919af15e59f35cca58fcc23/src/libxprobes.c#L156
Ответ 5
Ручка SIGPIPE локально
Обычно лучше всего обрабатывать ошибку локально, а не в глобальном обработчике событий сигнала, поскольку локально у вас будет больше контекста относительно того, что происходит и что нужно делать.
У меня есть уровень связи в одном из моих приложений, который позволяет моему приложению общаться с внешним аксессуаром. Когда возникает ошибка записи, я бросаю и исключаю на уровне связи и позволяю ей пузыриться до блока catch try, чтобы обрабатывать его там.
код:
Код для игнорирования сигнала 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, но при попытке использовать сокет вы получите ошибку чтения/записи, поэтому вам нужно будет это проверить.
Ответ 6
Вы не можете предотвратить выход из процесса на удаленном конце канала, и если он выйдет до того, как вы закончите писать, вы получите сигнал SIGPIPE. Если вы выберете SIG_IGN, тогда ваша запись вернется с ошибкой - и вам нужно будет отметить и отреагировать на эту ошибку. Просто поймать и игнорировать сигнал в обработчике - это не очень хорошая идея - вы должны заметить, что труба теперь не функционирует и модифицирует поведение программы, поэтому она больше не записывается в трубу (потому что сигнал будет сгенерирован снова и проигнорирован снова, и вы попробуете еще раз, и весь процесс может продолжаться долгое время и тратить много энергии процессора).
Ответ 7
Или я должен просто перехватить SIGPIPE обработчиком и игнорировать его?
Я считаю, что это правильно. Вы хотите знать, когда другой конец закрыл свой дескриптор и что говорит SIGPIPE.
Сэм
Ответ 8
В руководстве Linux сказано:
EPIPE Локальный конец был отключен на ориентированном на соединение разъем. В этом случае процесс также получит SIGPIPE если не установлен MSG_NOSIGNAL.
Но для Ubuntu 12.04 это неправильно. Я написал тест для этого случая, и я всегда получаю EPIPE с SIGPIPE. SIGPIPE genereated, если я попытаюсь записать в тот же сломанный сокет второй раз. Поэтому вам не нужно игнорировать SIGPIPE, если этот сигнал происходит, это означает логическую ошибку в вашей программе.
Ответ 9
Какая наилучшая практика для предотвращения аварии здесь?
Либо отключите сигпипы как для всех, либо поймайте и проигнорируйте ошибку.
Есть ли способ проверить, читает ли другая сторона строки?
Да, используйте select().
select(), похоже, не работает здесь, поскольку он всегда говорит, что сокет доступен для записи.
Вам нужно выбрать бит чтения. Вероятно, вы можете игнорировать записи.
Когда дальний конец закрывает свой дескриптор файла, выберите, чтобы вы сказали, что есть данные, готовые для чтения. Когда вы пойдете и прочитаете это, вы вернетесь на 0 байт, так как OS сообщает вам, что дескриптор файла был закрыт.
Единственный раз, когда вы не можете игнорировать биты записи, является то, что вы отправляете большие тома, и существует риск того, что другой конец будет загружен, что может привести к заполнению буферов. Если это произойдет, попытка записи в дескриптор файла может привести к блокировке или сбою вашей программы/потока. Выбор теста перед записью защитит вас от этого, но это не гарантирует, что другой конец будет здоровым или что ваши данные будут поступать.
Обратите внимание, что вы можете получить sigpipe от close(), а также при написании.
Закрыть сбрасывает любые буферные данные. Если другой конец уже закрыт, закрытие завершится неудачно, и вы получите сигпип.
Если вы используете буферизованный TCPIP, успешная запись просто означает, что ваши данные были поставлены в очередь для отправки, это не значит, что он был отправлен. Пока вы не позвоните по телефону, вы не знаете, что ваши данные были отправлены.
Sigpipe говорит вам, что что-то пошло не так, это не говорит вам, что или что вы должны с этим делать.
Ответ 10
В современной системе 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
... или, возможно, иметь функцию, которая удаляет определенный заблокированный сигнал.
Подробнее читайте справочная страница.