Як запобігти SIGPIPE (або правильно поводитися з ними)


259

У мене невелика серверна програма, яка приймає з'єднання в TCP або локальному сокеті UNIX, читає просту команду і, залежно від команди, надсилає відповідь. Проблема полягає в тому, що клієнт може не мати інтересу до відповіді іноді і виходить зранку, тому запис у цей сокет спричинить SIGPIPE і призведе до збою мого сервера. Яка найкраща практика для запобігання аварій тут? Чи є спосіб перевірити, чи читається інша сторона рядка? (select (), здається, не працює тут, оскільки завжди говорить, що сокет можна записати). Або я повинен просто зловити SIGPIPE за допомогою обробника і проігнорувати його?

Відповіді:


253

Як правило, ви хочете ігнорувати SIGPIPEта керувати помилкою безпосередньо у своєму коді. Це тому, що обробники сигналів в C мають багато обмежень щодо того, що вони можуть робити.

Найбільш портативний спосіб зробити це - встановити SIGPIPEобробник SIG_IGN. Це запобіжить появі SIGPIPEсигналу будь-якої розетки або труби .

Щоб ігнорувати SIGPIPEсигнал, використовуйте наступний код:

signal(SIGPIPE, SIG_IGN);

Якщо ви використовуєте send()дзвінок, ще одним варіантом є використання MSG_NOSIGNALопції, яка SIGPIPEвимикає поведінку на основі виклику. Зауважте, що не всі операційні системи підтримують MSG_NOSIGNALпрапор.

Нарешті, ви також можете розглянути SO_SIGNOPIPEпрапор сокета, який можна встановити setsockopt()в деяких операційних системах. Це не призведе SIGPIPEдо того, що він буде записаний просто у розетки, на які він встановлений.


75
Щоб зробити це явним: сигнал (SIGPIPE, SIG_IGN);
jhclark

5
Це може знадобитися, якщо ви отримуєте вихідний код повернення 141 (128 + 13: SIGPIPE). SIG_IGN - обробник сигналу ігнорування.
velcrow

6
Для розеток реально простіше використовувати send () з MSG_NOSIGNAL, як сказав @talash.
Павло Веселов

3
Що саме відбувається, якщо моя програма записує на розбиту трубу (в моєму випадку розетка)? SIG_IGN змушує програму ігнорувати SIG_PIPE, але тоді це призводить до того, що send () робить щось небажане?
судо

2
Варто згадати, оскільки я знайшов його один раз, потім забув його згодом і розгубився, а потім відкрив його вдруге. Налагоджувачі (я знаю, що gdb) переважають обробку сигналів, тому ігноровані сигнали не ігноруються! Перевірте свій код поза налагоджувачем, щоб переконатися, що SIGPIPE більше не виникає. stackoverflow.com/questions/6821469/…
Jetski S-type

155

Інший метод полягає в зміні сокета, щоб він ніколи не створював SIGPIPE на write (). Це зручніше в бібліотеках, де, можливо, не потрібно глобального обробника сигналу для SIGPIPE.

У більшості систем на базі BSD (MacOS, FreeBSD ...) (якщо ви користуєтесь C / C ++), ви можете це зробити за допомогою:

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

При цьому фактично замість генерування сигналу SIGPIPE повернеться EPIPE.


45
Це звучить добре, але, схоже, SO_NOSIGPIPE не існує в Linux - тому це не є широко портативним рішенням.
nobar

1
привіт усім, чи можете ви сказати мені, де мені належить поставити цей код? я не розумію подяки
R. Dewi

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 );

5
Іншими словами, використовуйте send (..., MSG_NOSIGNAL) як заміну для write (), і ви не отримаєте SIGPIPE. Це повинно добре працювати з сокетами (на підтримуваних платформах), але send (), здається, обмежено для використання з розетками (а не з трубами), тому це не є загальним вирішенням проблеми SIGPIPE.
nobar

@ Ray2k Хто на землі ще розробляє додатки для Linux <2.2? Це насправді напівсерйозне питання; більшість дистрибуторів постачають щонайменше 2,6.
Парфянський розстріл

1
@Parthian Shot: Вам слід переосмислити свою відповідь. Обслуговування старих вбудованих систем є вагомою причиною догляду за старими версіями Linux.
kirsche40

1
@ kirsche40 Я не ОП - мені просто цікаво.
Парфянський розстріл

@sklnd це працювало для мене чудово. Я використовую об'єкт Qt, QTcpSocketщоб обернути використання сокетів, мені довелося замінити 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 локально

Зазвичай найкраще обробляти помилку локально, а не глобальним обробником сигнальних подій, оскільки на місцевому рівні у вас буде більше контексту щодо того, що відбувається і який спосіб звернутися.

У мене в одному з моїх програм є комунікаційний рівень, який дозволяє моїй програмі спілкуватися із зовнішнім аксесуаром. Коли виникає помилка запису, я кидаю і виняток у комунікаційному шарі і даю йому міхур до блоку спробувати вловлювати, щоб обробити його там.

Код:

Код для ігнорування сигналу 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) працює для мене, але в режимі налагодження він повертає помилку, чи можна також ігнорувати цю помилку?
jongbanaag

@ 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. Це звучить як помилка ядра.
davmac

3

Яка найкраща практика для запобігання аварій тут?

Або відключіть вивіски, як усі, або вловлюйте та ігноруйте помилку.

Чи є спосіб перевірити, чи читається інша сторона рядка?

Так, використовуйте select ().

select (), здається, не працює тут, оскільки це завжди говорить, що сокет можна записати.

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

Коли дальній кінець закриє свою обробку файлів, функція Select скаже вам, що є дані, готові прочитати. Перейшовши і прочитавши це, ви отримаєте 0 байтів, і ось як ОС повідомляє вам, що обробка файлу закрита.

Єдиний раз, коли ви не можете ігнорувати біти запису, це якщо ви надсилаєте великі обсяги, і є ризик того, що інший кінець буде відкладений, що може призвести до заповнення ваших буферів. Якщо це трапиться, то спроба запису в ручку файлу може призвести до блокування вашої програми / потоку або відмови. Вибір тестування перед написанням захистить вас від цього, але це не гарантує, що інший кінець є здоровим або що ваші дані надійдуть.

Зауважте, що ви можете отримати підписи з наближення (), а також під час написання.

Закривання видаляє будь-які завантажені дані. Якщо інший кінець уже закритий, закрити не вдасться, і ви отримаєте вивіску.

Якщо ви використовуєте захищений TCPIP, то успішне записування означає, що ваші дані надійдуть у чергу для надсилання, це не означає, що вони надіслані. Поки ви успішно не зателефонували, ви не знаєте, що ваші дані надіслані.

Sigpipe повідомляє вам, що щось пішло не так, не говорить вам, що і що ви з цим робити.


Sigpipe повідомляє вам, що щось пішло не так, не говорить вам, що і що ви з цим робити. Саме так. Практично єдиною метою SIGPIPE є руйнування утиліти командного рядка, коли наступний етап більше не потребує введення. Якщо ваша програма працює в мережі, зазвичай просто блокуйте або ігноруйте програму SIGPIPE.
ДепресіяДаніель

Точно. SIGPIPE призначений для таких ситуацій, як "знайти XXX | head". Після того, як голова зібрала 10 ліній, немає сенсу продовжувати пошук. Тож голова виходить, і наступного разу, коли пошук намагається поговорити з нею, вона отримує сигнальний лист і знає, що теж може вийти.
Бен Авелінг

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

Для отримання додаткової інформації читайте сторінку man .


Не потрібно читати-змінювати-записувати набір заблокованих сигналів на зразок цього. sigprocmaskдодає до набору заблокованих сигналів, тому ви можете зробити все це за допомогою одного дзвінка.
Лучс

Я не читаю-не змінюю-пишу , я читаю, щоб зберегти поточний стан, який я зберігаю, old_stateщоб таким чином я міг його відновити пізніше, якщо захочу. Якщо ви знаєте, що вам більше не потрібно буде відновлювати стан, дійсно не потрібно читати та зберігати його таким чином.
Алексіс Вілке

1
Але ви робите: перший дзвінок sigprocmask()зчитує старий стан, дзвінок його sigaddsetмодифікує, другий дзвінок, щоб записати sigprocmask()його. Ви можете зняти перший виклик, ініціалізувати sigemptyset(&set)і змінити другий виклик на sigprocmask(SIG_BLOCK, &set, &old_state).
Лукс

Ах! Ха-ха! Ти маєш рацію. Я згоден. Ну ... у своєму програмному забезпеченні я не знаю, які сигнали я вже заблокував чи не заблокував. Тому я роблю це так. Однак я погоджуюся, що якщо у вас є лише один "встановити сигнали таким чином", то достатньо простого прозорого + набору.
Алексіс Вілке
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.