Jak zapobiegać SIGPIPE (lub obsługiwać je poprawnie)

259

Mam mały program serwera, który akceptuje połączenia na TCP lub lokalnym gnieździe UNIX, czyta proste polecenie i, w zależności od polecenia, wysyła odpowiedź. Problem polega na tym, że klient czasami nie jest zainteresowany odpowiedzią i wychodzi wcześniej, więc pisanie do tego gniazda spowoduje SIGPIPE i spowoduje awarię serwera. Jaka jest najlepsza praktyka, aby zapobiec awarii tutaj? Czy istnieje sposób sprawdzenia, czy druga strona linii nadal czyta? (select () nie wydaje się tutaj działać, ponieważ zawsze mówi, że gniazdo można zapisać). A może powinienem po prostu złapać SIGPIPE za pomocą programu obsługi i zignorować go?

jkramer
źródło

Odpowiedzi:

253

Na ogół chcesz zignorować SIGPIPEbłąd i obsłużyć go bezpośrednio w kodzie. Wynika to z faktu, że procedury obsługi sygnałów w C mają wiele ograniczeń dotyczących tego, co mogą zrobić.

Najbardziej przenośnym sposobem na to jest ustawienie modułu SIGPIPEobsługi SIG_IGN. Zapobiegnie to powstaniu SIGPIPEsygnału przez jakikolwiek zapis gniazda lub potoku .

Aby zignorować SIGPIPEsygnał, użyj następującego kodu:

signal(SIGPIPE, SIG_IGN);

Jeśli korzystasz z send()połączenia, inną opcją jest skorzystanie z tej MSG_NOSIGNALopcji, która wyłączy SIGPIPEzachowanie dla poszczególnych połączeń. Pamiętaj, że nie wszystkie systemy operacyjne obsługują MSG_NOSIGNALflagę.

Na koniec warto rozważyć SO_SIGNOPIPEflagę gniazda, którą można ustawić setsockopt()w niektórych systemach operacyjnych. Zapobiegnie SIGPIPEto powodowaniu przez zapisy tylko do gniazd, na których jest ustawiony.

dvorak
źródło
75
Aby wyrazić to wyraźnie: sygnał (SIGPIPE, SIG_IGN);
jhclark
5
Może to być konieczne, jeśli otrzymujesz kod powrotu wyjścia 141 (128 + 13: SIGPIPE). SIG_IGN to moduł obsługi sygnału ignorowania.
velcrow
6
W przypadku gniazd naprawdę łatwiej jest używać send () z MSG_NOSIGNAL, jak powiedział @talash.
Paweł Veselov
3
Co dokładnie się stanie, jeśli mój program zapisuje na zepsutej rurze (w moim przypadku gniazdo)? SIG_IGN powoduje, że program ignoruje SIG_PIPE, ale czy to powoduje, że send () robi coś niepożądanego?
sudo
2
Warto wspomnieć, ponieważ raz go znalazłem, potem zapomniałem o nim i pomyliłem się, a potem odkryłem go drugi raz. Debugery (wiem, że gdb to robi) zastępują obsługę sygnałów, więc ignorowane sygnały nie są ignorowane! Przetestuj swój kod poza debugerem, aby upewnić się, że SIGPIPE już nie występuje. stackoverflow.com/questions/6821469/…
Jetski S-type
155

Inną metodą jest zmiana gniazda, aby nigdy nie generowało SIGPIPE przy write (). Jest to wygodniejsze w bibliotekach, w których może nie być potrzebny globalny moduł obsługi sygnału dla SIGPIPE.

W większości systemów opartych na BSD (MacOS, FreeBSD ...) (zakładając, że używasz C / C ++) możesz to zrobić za pomocą:

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

W ten sposób zamiast generowanego sygnału SIGPIPE zostanie zwrócony EPIPE.

użytkownik55807
źródło
45
Brzmi dobrze, ale wydaje się, że SO_NOSIGPIPE nie istnieje w Linuksie - więc nie jest to rozwiązanie przenośne.
nobar
1
cześć wszystkim, czy możesz mi powiedzieć, gdzie mam umieścić ten kod? nie rozumiem podziękowań
R. Dewi
9
W Linuksie widzę opcję MSG_NOSIGNAL w flagach send
Aadishri
Działa idealnie w systemie Mac OS X
Kirugan
116

Jestem spóźniony na imprezę, ale SO_NOSIGPIPEnie jestem przenośny i może nie działać w twoim systemie (wydaje się, że to BSD).

Dobrą alternatywą, jeśli używasz, powiedzmy, systemu Linux bez, SO_NOSIGPIPEbyłoby ustawienie MSG_NOSIGNALflagi w wywołaniu send (2).

Przykład zastąpienia write(...)przez send(...,MSG_NOSIGNAL)(patrz komentarz nobara )

char buf[888];
//write( sockfd, buf, sizeof(buf) );
send(    sockfd, buf, sizeof(buf), MSG_NOSIGNAL );
sklnd
źródło
5
Innymi słowy, użyj send (..., MSG_NOSIGNAL) jako zamiennika write (), a nie dostaniesz SIGPIPE. Powinno to działać dobrze dla gniazd (na obsługiwanych platformach), ale send () wydaje się być ograniczony do używania z gniazdami (nie rurami), więc nie jest to ogólne rozwiązanie problemu SIGPIPE.
nobar
@ Ray2k Kto do diabła wciąż rozwija aplikacje dla systemu Linux <2.2? To właściwie półpoważne pytanie; większość dystrybutorów ma co najmniej 2.6.
Parthian Shot
1
@Parthian Shot: Powinieneś przemyśleć swoją odpowiedź. Utrzymanie starych systemów osadzonych jest ważnym powodem do dbania o starsze wersje Linuksa.
kirsche40,
1
@ kirsche40 Nie jestem OP- Byłem po prostu ciekawy.
Parthian Shot
@sklnd to działało dla mnie idealnie. Używam QTcpSocketobiektu Qt do zawijania użycia gniazd, musiałem zastąpić writewywołanie metody przez system operacyjny send(za pomocą socketDescriptormetody). Czy ktoś zna czystszą opcję ustawienia tej opcji w QTcpSocketklasie?
Emilio González Montaña
29

W tym poście opisałem możliwe rozwiązanie dla przypadku Solaris, gdy nie jest dostępny ani SO_NOSIGPIPE, ani MSG_NOSIGNAL.

Zamiast tego musimy tymczasowo wyłączyć SIGPIPE w bieżącym wątku, który wykonuje kod biblioteki. Oto jak to zrobić: aby ukryć SIGPIPE, najpierw sprawdzamy, czy jest w toku. Jeśli tak, oznacza to, że jest zablokowany w tym wątku i nie musimy nic robić. Jeśli biblioteka wygeneruje dodatkowy SIGPIPE, zostanie scalony z oczekującym, a to nie jest możliwe. Jeśli SIGPIPE nie oczekuje, blokujemy go w tym wątku, a także sprawdzamy, czy został już zablokowany. Następnie możemy wykonać nasze zapisy. Kiedy mamy przywrócić SIGPIPE do pierwotnego stanu, wykonujemy następujące czynności: jeśli SIGPIPE był pierwotnie w toku, nie robimy nic. W przeciwnym razie sprawdzimy, czy jest teraz w toku. Jeśli tak (co oznacza, że ​​nasze działania wygenerowały jeden lub więcej SIGPIPE), czekamy na to w tym wątku, w ten sposób usuwając stan oczekujący (w tym celu używamy sigtimedwait () z zerowym limitem czasu; ma to na celu uniknięcie blokowania w scenariuszu, w którym złośliwy użytkownik wysłał SIGPIPE ręcznie do całego procesu: w tym przypadku zobaczymy, że jest w toku, ale inny wątek może radzimy sobie z tym, zanim będziemy musieli na nią poczekać). Po wyczyszczeniu stanu oczekującego odblokowujemy SIGPIPE w tym wątku, ale tylko jeśli pierwotnie nie został zablokowany.

Przykładowy kod na https://github.com/kroki/XProbes/blob/1447f3d93b6dbf273919af15e59f35cca58fcc23/src/libxprobes.c#L156

kroki
źródło
22

Obsługiwać SIGPIPE lokalnie

Zwykle najlepiej jest obsługiwać błąd lokalnie, a nie w globalnej procedurze obsługi zdarzeń sygnału, ponieważ lokalnie będziesz miał większy kontekst, co się dzieje i jakie środki należy podjąć.

Mam warstwę komunikacyjną w jednej z moich aplikacji, która pozwala mojej aplikacji komunikować się z zewnętrznym akcesorium. Kiedy pojawia się błąd zapisu, rzucam wyjątek w warstwie komunikacyjnej i pozwalam, aby bąbelkowała do bloku try catch, aby go tam obsłużyć.

Kod:

Kod ignorujący sygnał SIGPIPE, umożliwiający obsługę go lokalnie, to:

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

Ten kod zapobiegnie podniesieniu sygnału SIGPIPE, ale podczas próby użycia gniazda pojawi się błąd odczytu / zapisu, więc musisz to sprawdzić.

Sam
źródło
czy to połączenie powinno być wykonane po podłączeniu gniazda, czy wcześniej? gdzie najlepiej jest umieścić. Dzięki
Ahmed,
@Ahmed Osobiście umieścić go w applicationDidFinishLaunching: . Najważniejsze, że powinno być przed interakcją z gniazdem.
Sam
ale podczas próby użycia gniazda pojawi się błąd odczytu / zapisu, więc musisz to sprawdzić. - Czy mogę zapytać, jak to możliwe? sygnał (SIGPIPE, SIG_IGN) działa dla mnie, ale w trybie debugowania zwraca błąd, czy można go również zignorować?
jongbanaag
@ Dreyfus15 Wydaje mi się, że właśnie zawijałem połączenia za pomocą gniazda w bloku try / catch, aby obsłużyć je lokalnie. Minęło trochę czasu, odkąd to widziałem.
Sam
15

Nie możesz uniemożliwić wyjścia z procesu na drugim końcu potoku, a jeśli zakończy się ono przed zakończeniem pisania, otrzymasz sygnał SIGPIPE. Jeśli SIG_IGN sygnał, twój zapis wróci z błędem - i musisz zanotować i zareagować na ten błąd. Samo łapanie i ignorowanie sygnału w module obsługi nie jest dobrym pomysłem - należy pamiętać, że potok jest teraz nieczynny i modyfikuje zachowanie programu, aby nie zapisywał go ponownie (ponieważ sygnał zostanie wygenerowany ponownie i zignorowany ponownie, a spróbujesz jeszcze raz, a cały proces może trwać długo i marnować dużo mocy procesora).

Jonathan Leffler
źródło
6

A może powinienem po prostu złapać SIGPIPE za pomocą programu obsługi i zignorować go?

Wierzę, że tak jest. Chcesz wiedzieć, kiedy drugi koniec zamknął swój deskryptor i tak mówi SIGPIPE.

Sam

Sam Reynolds
źródło
3

Podręcznik Linuksa powiedział:

EPIPE Lokalny koniec został zamknięty w gnieździe zorientowanym na połączenie. W takim przypadku proces otrzyma również SIGPIPE, chyba że ustawiony jest MSG_NOSIGNAL.

Ale w przypadku Ubuntu 12.04 nie jest to właściwe. Napisałem test dla tej sprawy i zawsze otrzymuję EPIPE bez SIGPIPE. SIGPIPE jest generowany, jeśli za drugim razem spróbuję napisać do tego samego uszkodzonego gniazda. Więc nie musisz ignorować SIGPIPE, jeśli ten sygnał się zdarzy, oznacza to błąd logiczny w twoim programie.

talash
źródło
1
Zdecydowanie otrzymuję SIGPIPE bez uprzedniego zobaczenia EPIPE na gnieździe w systemie Linux. To brzmi jak błąd jądra.
davmac
3

Jaka jest najlepsza praktyka, aby zapobiec awarii tutaj?

Wyłącz sigpipe jak wszyscy lub złap i zignoruj ​​błąd.

Czy istnieje sposób sprawdzenia, czy druga strona linii nadal czyta?

Tak, użyj select ().

Wydaje się, że select () nie działa tutaj, ponieważ zawsze mówi, że gniazdo można zapisać.

Musisz wybrać na odczytanych bitach. Prawdopodobnie możesz zignorować bity zapisu .

Kiedy drugi koniec zamyka uchwyt pliku, wybierz powie ci, że są dane gotowe do odczytu. Gdy to zrobisz, otrzymasz 0 bajtów, tak system operacyjny informuje, że uchwyt pliku został zamknięty.

Jedynym momentem, w którym nie można zignorować bitów zapisu, jest wysyłanie dużych woluminów i istnieje ryzyko zalegania drugiego końca, co może spowodować wypełnienie buforów. Jeśli tak się stanie, wówczas próba zapisu do uchwytu pliku może spowodować zablokowanie lub niepowodzenie programu / wątku. Testowanie select przed zapisaniem ochroni cię przed tym, ale nie gwarantuje, że drugi koniec jest zdrowy lub że twoje dane dotrą.

Zauważ, że możesz uzyskać sigpipe z close (), a także podczas pisania.

Zamknij opróżnia buforowane dane. Jeśli drugi koniec został już zamknięty, zamknięcie zakończy się niepowodzeniem, a otrzymasz sigpipe.

Jeśli używasz buforowanego protokołu TCPIP, pomyślny zapis oznacza po prostu, że dane zostały umieszczone w kolejce do wysłania, co nie oznacza, że ​​zostały wysłane. Do czasu pomyślnego połączenia blisko nie wiesz, że Twoje dane zostały wysłane.

Sigpipe mówi ci, że coś poszło nie tak, nie mówi ci, co lub co powinieneś z tym zrobić.

Ben Aveling
źródło
Sigpipe mówi ci, że coś poszło nie tak, nie mówi ci, co lub co powinieneś z tym zrobić. Dokładnie. Praktycznie jedynym celem SIGPIPE jest zburzenie potokowych narzędzi linii poleceń, gdy następny etap nie potrzebuje już danych wejściowych. Jeśli twój program działa w sieci, zwykle powinieneś po prostu zablokować lub zignorować SIGPIPE w całym programie.
Przygnębiony
Dokładnie. SIGPIPE jest przeznaczony do sytuacji takich jak „znajdź XXX | head”. Gdy głowa dopasuje 10 linii, nie ma sensu kontynuować poszukiwania. Więc głowa wychodzi, a następnym razem, gdy find spróbuje z nim porozmawiać, dostaje sigpipe i wie, że on też może wyjść.
Ben Aveling
Naprawdę, jedynym celem SIGPIPE jest umożliwienie niechlujności programów i nie sprawdzanie błędów podczas zapisu!
William Pursell
2

W nowoczesnym systemie POSIX (tj. Linux) możesz korzystać z tej sigprocmask()funkcji.

#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 ...
}

Jeśli chcesz później przywrócić poprzedni stan, pamiętaj, aby zapisać old_statebezpieczne miejsce. Jeśli wywołasz tę funkcję wiele razy, musisz albo użyć stosu, albo zapisać tylko pierwszy lub ostatni old_state... lub może mieć funkcję, która usuwa określony zablokowany sygnał.

Aby uzyskać więcej informacji, przeczytaj stronę podręcznika man .

Alexis Wilke
źródło
Nie jest konieczne odczytywanie, modyfikowanie i zapisywanie zestawu takich zablokowanych sygnałów. sigprocmaskdodaje do zestawu zablokowanych sygnałów, dzięki czemu możesz to wszystko zrobić za pomocą jednego połączenia.
Luchs,
Nie czytam, nie modyfikuję , nie piszę , czytam, aby zapisać bieżący stan, który mam, aby w old_staterazie potrzeby móc go później przywrócić. Jeśli wiesz, że nie będziesz musiał nigdy przywracać stanu, w rzeczywistości nie ma potrzeby czytania i przechowywania go w ten sposób.
Alexis Wilke
1
Ale robisz: pierwsze wezwanie do sigprocmask()odczytania starego stanu, wezwanie do jego sigaddsetmodyfikacji, drugie wezwanie do sigprocmask()zapisania go. Możesz usunąć pierwsze połączenie, zainicjować sigemptyset(&set)i zmienić drugie połączenie na sigprocmask(SIG_BLOCK, &set, &old_state).
Luchs,
Ach! Ha ha! Masz rację. Ja robię. Cóż ... w moim oprogramowaniu nie wiem, które sygnały już zablokowałem, czy nie zablokowałem. Więc robię to w ten sposób. Zgadzam się jednak, że jeśli masz tylko jeden „zestaw sygnałów w ten sposób”, wystarczy prosty zestaw clear + set.
Alexis Wilke,