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

261

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

jkramer
источник

Ответы:

255

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

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

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

signal(SIGPIPE, SIG_IGN);

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

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

Dvorak
источник
75
Чтобы сделать это явным: сигнал (SIGPIPE, SIG_IGN);
jhclark
5
Это может быть необходимо, если вы получаете код возврата выхода 141 (128 + 13: SIGPIPE). SIG_IGN - обработчик сигнала игнорирования.
липучка
6
Для сокетов действительно проще использовать send () с MSG_NOSIGNAL, как сказал @talash.
Павел Веселов
3
Что именно происходит, если моя программа пишет в сломанный канал (в моем случае это сокет)? SIG_IGN заставляет программу игнорировать SIG_PIPE, но приводит ли это к тому, что send () делает что-то нежелательное?
Судо
2
Стоит упомянуть, поскольку я нашел это однажды, потом забыл об этом и запутался, а потом обнаружил во второй раз. Отладчики (я знаю, что GDB делает) отменяют вашу обработку сигналов, поэтому игнорируемые сигналы не игнорируются! Протестируйте свой код вне отладчика, чтобы убедиться, что SIGPIPE больше не происходит. stackoverflow.com/questions/6821469/…
Jetski S-type
155

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

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

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

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

user55807
источник
45
Это звучит хорошо, но SO_NOSIGPIPE, похоже, не существует в Linux - так что это не широко переносимое решение.
Нобар
1
Привет всем, вы можете сказать мне, где место, где я должен положить этот код? я не понимаю благодарности
Р. Деви
9
В Linux я вижу опцию MSG_NOSIGNAL во флагах отправки
Aadishri
Отлично
116

Я супер опоздал на вечеринку, но 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 );
sklnd
источник
5
Другими словами, используйте send (..., MSG_NOSIGNAL) в качестве замены для write (), и вы не получите SIGPIPE. Это должно хорошо работать для сокетов (на поддерживаемых платформах), но send (), по-видимому, ограничен для использования с сокетами (не конвейерами), так что это не общее решение проблемы SIGPIPE.
Нобар
@ Ray2k Кто на земле все еще разрабатывает приложения для Linux <2.2? Это на самом деле полусерьезный вопрос; большинство дистрибутивов с минимум 2.6.
Парфянский выстрел
1
@Parthian Shot: Вы должны переосмыслить свой ответ. Обслуживание старых встроенных систем является уважительной причиной заботиться о старых версиях Linux.
kirsche40
1
@ kirsche40 Я не ОП - мне было просто любопытно.
Парфянский снимок
@sklnd это отлично сработало для меня. Я использую QTcpSocketобъект Qt, чтобы обернуть использование сокетов, мне пришлось заменить writeвызов метода операционной системой send(используя socketDescriptorметод). Кто-нибудь знает более чистую опцию, чтобы установить эту опцию в QTcpSocketклассе?
Эмилио Гонсалес Монтанья
29

В этом посте я описал возможное решение для случая 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

Кроки
источник
22

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

Сэм
источник
этот вызов должен быть сделан после подключения сокета или до этого? где это лучше всего разместить. Спасибо
Ахмед
@Ahmed лично я положил его в applicationDidFinishLaunching: . Главное, чтобы оно было перед взаимодействием с сокетным соединением.
Сэм
но вы получите ошибку чтения / записи при попытке использовать сокет, так что вам нужно будет проверить это. - Могу я спросить, как это возможно? сигнал (SIGPIPE, SIG_IGN) работает для меня, но в режиме отладки он возвращает ошибку, можно ли также игнорировать эту ошибку?
июня
@ Dreyfus15 Я полагаю, что я просто обернул вызовы, используя сокет в блоке try / catch для локальной обработки. Прошло некоторое время, так как я видел это.
Сэм
15

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

Джонатан Леффлер
источник
6

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

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

Сэм

Сэм Рейнольдс
источник
3

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

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

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

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

Какова лучшая практика, чтобы предотвратить аварию здесь?

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

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

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

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

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

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

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

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

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

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

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

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

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

Для получения дополнительной информации прочитайте справочную страницу .

Алексис Уилке
источник
Нет необходимости читать-модифицировать-записывать набор заблокированных сигналов, подобных этому. sigprocmaskдобавляет в набор заблокированных сигналов, так что вы можете сделать все это одним вызовом.
Луч
Я не читаю-изменяю-пишу , я читаю, чтобы сохранить текущее состояние, в котором я храню, old_stateчтобы я мог восстановить его позже, если захочу. Если вы знаете, что вам не нужно когда-либо восстанавливать состояние, нет необходимости читать и хранить его таким образом.
Алексис Вилке
1
Но вы делаете: первый вызов для sigprocmask()чтения старого состояния, вызов для sigaddsetего изменения, второй вызов для sigprocmask()его записи. Вы можете удалить первый вызов, выполнить инициализацию sigemptyset(&set)и изменить второй вызов на sigprocmask(SIG_BLOCK, &set, &old_state).
Луч
Ах! Ха ха! Ты прав. Я делаю. Ну ... в моем программном обеспечении я не знаю, какие сигналы я уже заблокировал или не заблокировал. Так что я делаю это так. Тем не менее, я согласен, что если у вас есть только один «набор сигналов таким образом», тогда достаточно простого clear + set.
Алексис Уилке