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

199

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

Теги:
io
signals
broken-pipe
sigpipe

10 ответов

207
Лучший ответ

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

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

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

signal(SIGPIPE, SIG_IGN);

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

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

  • 72
    Чтобы сделать это явным: сигнал (SIGPIPE, SIG_IGN);
  • 5
    Это может быть необходимо, если вы получаете код возврата выхода 141 (128 + 13: SIGPIPE). SIG_IGN - обработчик сигнала игнорирования.
Показать ещё 7 комментариев
139

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

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

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

При этом вместо генерируемого сигнала SIGPIPE возвращается EPIPE.

  • 40
    Это звучит хорошо, но SO_NOSIGPIPE, похоже, не существует в Linux - так что это не широко переносимое решение.
  • 1
    Привет всем, вы можете сказать мне, где место, где я должен положить этот код? я не понимаю, спасибо
Показать ещё 3 комментария
106

Я очень опаздываю на вечеринку, но 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 );
  • 2
    +1, Linux 2.2+ хотя по словам человека
  • 4
    Другими словами, используйте send (..., MSG_NOSIGNAL) в качестве замены для write (), и вы не получите SIGPIPE. Это должно хорошо работать для сокетов (на поддерживаемых платформах), но send (), по-видимому, ограничен для использования с сокетами (не конвейерами), поэтому это не общее решение проблемы SIGPIPE.
Показать ещё 4 комментария
27

В этой 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

19

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

  • 0
    этот вызов должен быть сделан после подключения сокета или до этого? где это лучше всего разместить. Спасибо
  • 0
    @ Ахмед, я лично положил его в приложение DidFinishLaunching :. Главное, чтобы оно было перед взаимодействием с сокетным соединением.
Показать ещё 2 комментария
13

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

  • 9
    @Math: игнорируйте SIGPIPE и обратите внимание на ошибки записи.
4

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

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

Сэм

2

В руководстве Linux сказано:

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

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

  • 1
    Я определенно получаю SIGPIPE, не увидев EPIPE на сокете в Linux. Это звучит как ошибка ядра.
1

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

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

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

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

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

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

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

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

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

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

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

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

  • 0
    Сигпайп говорит вам, что что-то пошло не так, он не говорит вам, что или что вы должны с этим делать. Именно так. Практически единственная цель SIGPIPE - это разорвать служебные программы командной строки, когда следующий этап больше не нуждается во вводе. Если ваша программа работает в сети, вы должны просто блокировать или игнорировать SIGPIPE для всей программы.
  • 0
    Точно. SIGPIPE предназначен для таких ситуаций, как «найти XXX | голову». Как только голова соответствует 10 строкам, бессмысленно продолжать поиск. Итак, голова выходит, и в следующий раз, когда find пытается с ней поговорить, он получает трубку и знает, что тоже может выйти.
Показать ещё 1 комментарий
0

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

Подробнее читайте справочная страница.

  • 0
    Нет необходимости читать-модифицировать-записывать набор заблокированных сигналов, подобных этому. sigprocmask добавляет к набору заблокированных сигналов, так что вы можете сделать все это одним вызовом.
  • 0
    Я не читаю-изменяю-пишу , я читаю, чтобы сохранить текущее состояние, которое я храню в old_state чтобы я мог восстановить его позже, если old_state . Если вы знаете, что вам не нужно когда-либо восстанавливать состояние, нет необходимости читать и хранить его таким образом.
Показать ещё 2 комментария

Ещё вопросы

Сообщество Overcoder
Наверх
Меню