So verhindern Sie SIGPIPEs (oder behandeln sie richtig)

260

Ich habe ein kleines Serverprogramm, das Verbindungen über einen TCP- oder lokalen UNIX-Socket akzeptiert, einen einfachen Befehl liest und je nach Befehl eine Antwort sendet. Das Problem ist, dass der Client manchmal kein Interesse an der Antwort hat und vorzeitig beendet wird. Wenn Sie also in diesen Socket schreiben, wird ein SIGPIPE ausgelöst und mein Server stürzt ab. Was ist die beste Vorgehensweise, um den Absturz hier zu verhindern? Gibt es eine Möglichkeit zu überprüfen, ob die andere Seite der Zeile noch liest? (select () scheint hier nicht zu funktionieren, da immer gesagt wird, dass der Socket beschreibbar ist). Oder sollte ich das SIGPIPE einfach mit einem Handler fangen und ignorieren?

jkramer
quelle

Antworten:

254

Im Allgemeinen möchten Sie SIGPIPEden Fehler ignorieren und direkt in Ihrem Code behandeln. Dies liegt daran, dass Signalhandler in C viele Einschränkungen hinsichtlich ihrer Möglichkeiten haben.

Der portabelste Weg, dies zu tun, besteht darin, den SIGPIPEHandler auf einzustellen SIG_IGN. Dadurch wird verhindert, dass Socket- oder Pipe-Schreibvorgänge ein SIGPIPESignal verursachen.

SIGPIPEVerwenden Sie den folgenden Code, um das Signal zu ignorieren :

signal(SIGPIPE, SIG_IGN);

Wenn Sie den send()Anruf verwenden, können Sie auch die MSG_NOSIGNALOption verwenden, mit der das SIGPIPEVerhalten pro Anruf deaktiviert wird. Beachten Sie, dass nicht alle Betriebssysteme das MSG_NOSIGNALFlag unterstützen.

Zuletzt möchten Sie möglicherweise auch das SO_SIGNOPIPESocket-Flag berücksichtigen , mit dem setsockopt()auf einigen Betriebssystemen gesetzt werden kann. Dies verhindert SIGPIPE, dass Schreibvorgänge nur in die Sockets verursacht werden, auf die es gesetzt ist.

dvorak
quelle
75
Um dies explizit zu machen: Signal (SIGPIPE, SIG_IGN);
Jhclark
5
Dies kann erforderlich sein, wenn Sie einen Exit-Rückkehrcode von 141 (128 + 13: SIGPIPE) erhalten. SIG_IGN ist der Ignoriersignal-Handler.
Velcrow
6
Für Sockets ist es wirklich einfacher, send () mit MSG_NOSIGNAL zu verwenden, wie @talash sagte.
Pawel Veselov
3
Was genau passiert, wenn mein Programm in eine kaputte Pipe schreibt (in meinem Fall einen Socket)? SIG_IGN lässt das Programm SIG_PIPE ignorieren, aber führt dies dann dazu, dass send () etwas Unerwünschtes tut?
Sudo
2
Erwähnenswert, da ich es einmal gefunden habe, es später vergessen und verwirrt wurde und es dann ein zweites Mal entdeckte. Debugger (ich weiß, dass GDB dies tut) überschreiben Ihre Signalbehandlung, sodass ignorierte Signale nicht ignoriert werden! Testen Sie Ihren Code außerhalb eines Debuggers, um sicherzustellen, dass das SIGPIPE nicht mehr auftritt. stackoverflow.com/questions/6821469/…
Jetski S-Typ
155

Eine andere Methode besteht darin, den Socket so zu ändern, dass beim Schreiben () niemals SIGPIPE generiert wird. Dies ist in Bibliotheken praktischer, in denen Sie möglicherweise keinen globalen Signalhandler für SIGPIPE benötigen.

Auf den meisten BSD-basierten Systemen (MacOS, FreeBSD ...) (vorausgesetzt, Sie verwenden C / C ++) können Sie dies folgendermaßen tun:

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

Damit wird anstelle des generierten SIGPIPE-Signals EPIPE zurückgegeben.

user55807
quelle
45
Das hört sich gut an, aber SO_NOSIGPIPE scheint unter Linux nicht zu existieren - es ist also keine allgemein tragbare Lösung.
Nobar
1
Hallo zusammen, kannst du mir sagen, wo ich diesen Code platzieren muss? Ich verstehe nicht danke
R. Dewi
9
Unter Linux sehe ich die Option MSG_NOSIGNAL in den Flags von send
Aadishri
Funktioniert perfekt unter Mac OS X
Kirugan
116

Ich bin super spät zur Party, aber SO_NOSIGPIPEnicht portabel und funktioniert möglicherweise nicht auf Ihrem System (es scheint eine BSD-Sache zu sein).

Eine gute Alternative, wenn Sie beispielsweise ein Linux-System ohne verwenden SO_NOSIGPIPE, besteht darin, das MSG_NOSIGNALFlag bei Ihrem send (2) -Aufruf zu setzen.

Beispiel ersetzen write(...)durch send(...,MSG_NOSIGNAL)(siehe Kommentar von Nobar )

char buf[888];
//write( sockfd, buf, sizeof(buf) );
send(    sockfd, buf, sizeof(buf), MSG_NOSIGNAL );
sklnd
quelle
5
Mit anderen Worten, verwenden Sie send (..., MSG_NOSIGNAL) als Ersatz für write (), und Sie erhalten kein SIGPIPE. Dies sollte für Sockets (auf unterstützten Plattformen) gut funktionieren, aber send () scheint auf die Verwendung mit Sockets (nicht Pipes) beschränkt zu sein, sodass dies keine allgemeine Lösung für das SIGPIPE-Problem ist.
Nobar
@ Ray2k Wer in aller Welt entwickelt noch Anwendungen für Linux <2.2? Das ist eigentlich eine halb ernste Frage; Die meisten Distributionen werden mit mindestens 2.6 ausgeliefert.
Parthian Shot
1
@Parthian Shot: Du solltest deine Antwort überdenken. Die Wartung alter eingebetteter Systeme ist ein triftiger Grund, sich um ältere Linux-Versionen zu kümmern.
Kirsche40
1
@ kirsche40 Ich bin nicht der OP- Ich war nur neugierig.
Parthian Shot
@sklnd das hat bei mir perfekt funktioniert. Ich verwende ein Qt- QTcpSocketObjekt, um die Verwendung von Sockets zu verpacken. Ich musste den writeMethodenaufruf durch ein Betriebssystem ersetzen send(mithilfe der socketDescriptorMethode). Kennt jemand eine sauberere Option, um diese Option in einer QTcpSocketKlasse festzulegen?
Emilio González Montaña
29

In diesem Beitrag habe ich eine mögliche Lösung für den Solaris-Fall beschrieben, wenn weder SO_NOSIGPIPE noch MSG_NOSIGNAL verfügbar sind.

Stattdessen müssen wir SIGPIPE im aktuellen Thread, der den Bibliothekscode ausführt, vorübergehend unterdrücken. So geht's: Um SIGPIPE zu unterdrücken, prüfen wir zunächst, ob es aussteht. Wenn dies der Fall ist, bedeutet dies, dass es in diesem Thread blockiert ist und wir nichts tun müssen. Wenn die Bibliothek zusätzliches SIGPIPE generiert, wird es mit dem ausstehenden zusammengeführt, und das ist ein No-Op. Wenn SIGPIPE nicht ansteht, blockieren wir es in diesem Thread und prüfen auch, ob es bereits blockiert wurde. Dann können wir unsere Schreibvorgänge ausführen. Wenn wir SIGPIPE in seinen ursprünglichen Zustand zurückversetzen möchten, gehen wir wie folgt vor: Wenn SIGPIPE ursprünglich ausstand, tun wir nichts. Ansonsten prüfen wir, ob es jetzt ansteht. Wenn dies der Fall ist (was bedeutet, dass unsere Aktionen ein oder mehrere SIGPIPEs generiert haben), warten wir in diesem Thread darauf. Auf diese Weise wird der Status "Ausstehend" gelöscht (dazu verwenden wir sigtimedwait () ohne Zeitlimit). Dies soll verhindern, dass ein Szenario blockiert wird, in dem böswillige Benutzer SIGPIPE manuell an einen gesamten Prozess gesendet haben. In diesem Fall wird der Status "Ausstehend" angezeigt, in einem anderen Thread jedoch möglicherweise behandeln Sie es, bevor wir eine Änderung hatten, um darauf zu warten). Nach dem Löschen des Status "Ausstehend" entsperren wir SIGPIPE in diesem Thread, jedoch nur, wenn es ursprünglich nicht blockiert wurde.

Beispielcode unter https://github.com/kroki/XProbes/blob/1447f3d93b6dbf273919af15e59f35cca58fcc23/src/libxprobes.c#L156

Kroki
quelle
22

SIGPIPE lokal handhaben

Es ist normalerweise am besten, den Fehler lokal und nicht in einem globalen Signalereignishandler zu behandeln, da Sie lokal mehr Kontext darüber haben, was vor sich geht und welchen Rückgriff Sie nehmen müssen.

Ich habe eine Kommunikationsschicht in einer meiner Apps, die es meiner App ermöglicht, mit einem externen Zubehör zu kommunizieren. Wenn ein Schreibfehler auftritt, werfe ich eine Ausnahme in die Kommunikationsschicht und lasse sie zu einem Try-Catch-Block aufblasen, um sie dort zu behandeln.

Code:

Der Code zum Ignorieren eines SIGPIPE-Signals, damit Sie es lokal verarbeiten können, lautet:

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

Dieser Code verhindert, dass das SIGPIPE-Signal ausgelöst wird. Beim Versuch, den Socket zu verwenden, wird jedoch ein Lese- / Schreibfehler angezeigt. Daher müssen Sie dies überprüfen.

Sam
quelle
Sollte dieser Anruf nach dem Anschließen einer Steckdose oder davor erfolgen? wo ist es am besten zu platzieren. Danke
Ahmed
@Ahmed Ich persönlich habe es in applicationDidFinishLaunching eingefügt : . Die Hauptsache ist, dass es vor der Interaktion mit einer Socket-Verbindung sein sollte.
Sam
Beim Versuch, den Socket zu verwenden, wird jedoch ein Lese- / Schreibfehler angezeigt. Daher müssen Sie dies überprüfen. - Darf ich fragen, wie das möglich ist? Das Signal (SIGPIPE, SIG_IGN) funktioniert für mich, aber im Debug-Modus wird ein Fehler zurückgegeben. Ist es möglich, diesen Fehler auch zu ignorieren?
Jongbanaag
@ Dreyfus15 Ich glaube, ich habe gerade Anrufe mit dem Socket in einen Try / Catch-Block eingeschlossen, um sie lokal zu verarbeiten. Es ist schon eine Weile her, seit ich das gesehen habe.
Sam
15

Sie können nicht verhindern, dass der Prozess am anderen Ende einer Pipe beendet wird. Wenn er beendet wird, bevor Sie mit dem Schreiben fertig sind, erhalten Sie ein SIGPIPE-Signal. Wenn Sie das Signal SIG_IGN, wird Ihr Schreibvorgang mit einem Fehler zurückgegeben - und Sie müssen diesen Fehler notieren und darauf reagieren. Nur das Signal in einem Handler abzufangen und zu ignorieren, ist keine gute Idee. Sie müssen beachten, dass die Pipe jetzt nicht mehr funktioniert, und das Verhalten des Programms so ändern, dass es nicht erneut in die Pipe schreibt (da das Signal erneut generiert und ignoriert wird erneut, und Sie werden es erneut versuchen, und der gesamte Vorgang kann lange dauern und viel CPU-Leistung verschwenden.

Jonathan Leffler
quelle
6

Oder sollte ich das SIGPIPE einfach mit einem Handler fangen und ignorieren?

Ich glaube das ist richtig. Sie möchten wissen, wann das andere Ende den Deskriptor geschlossen hat, und das sagt Ihnen SIGPIPE.

Sam

Sam Reynolds
quelle
3

Das Linux-Handbuch sagte:

EPIPE Das lokale Ende wurde an einem verbindungsorientierten Socket heruntergefahren. In diesem Fall erhält der Prozess auch ein SIGPIPE, sofern nicht MSG_NOSIGNAL festgelegt ist.

Aber für Ubuntu 12.04 ist es nicht richtig. Ich habe einen Test für diesen Fall geschrieben und erhalte immer EPIPE ohne SIGPIPE. SIGPIPE wird generiert, wenn ich beim zweiten Mal versuche, in denselben defekten Socket zu schreiben. Sie müssen SIGPIPE also nicht ignorieren, wenn dieses Signal auftritt. Dies bedeutet einen logischen Fehler in Ihrem Programm.

Talash
quelle
1
Ich erhalte definitiv SIGPIPE, ohne EPIPE zuerst auf einem Socket unter Linux zu sehen. Das klingt nach einem Kernel-Bug.
Davmac
3

Was ist die beste Vorgehensweise, um den Absturz hier zu verhindern?

Deaktivieren Sie entweder die Sigpipes für alle oder fangen Sie den Fehler ab und ignorieren Sie ihn.

Gibt es eine Möglichkeit zu überprüfen, ob die andere Seite der Zeile noch liest?

Ja, verwenden Sie select ().

select () scheint hier nicht zu funktionieren, da immer angegeben wird, dass der Socket beschreibbar ist.

Sie müssen sich auf die Auswahl lesen Bits. Sie können die Schreibbits wahrscheinlich ignorieren .

Wenn das entfernte Ende sein Dateihandle schließt, zeigt Ihnen select an, dass Daten zum Lesen bereit sind. Wenn Sie das lesen, erhalten Sie 0 Bytes zurück. Auf diese Weise teilt Ihnen das Betriebssystem mit, dass das Dateihandle geschlossen wurde.

Das einzige Mal, wenn Sie die Schreibbits nicht ignorieren können, ist, wenn Sie große Volumes senden, und es besteht die Gefahr, dass das andere Ende überlastet wird, was dazu führen kann, dass sich Ihre Puffer füllen. In diesem Fall kann der Versuch, in das Dateihandle zu schreiben, dazu führen, dass Ihr Programm / Thread blockiert oder fehlschlägt. Das Testen von select vor dem Schreiben schützt Sie davor, garantiert jedoch nicht, dass das andere Ende fehlerfrei ist oder dass Ihre Daten eintreffen.

Beachten Sie, dass Sie eine Sigpipe sowohl von close () als auch beim Schreiben erhalten können.

Durch Schließen werden alle gepufferten Daten gelöscht. Wenn das andere Ende bereits geschlossen wurde, schlägt das Schließen fehl und Sie erhalten ein Sigpipe.

Wenn Sie gepuffertes TCPIP verwenden, bedeutet ein erfolgreicher Schreibvorgang nur, dass Ihre Daten zum Senden in die Warteschlange gestellt wurden. Dies bedeutet nicht, dass sie gesendet wurden. Bis Sie erfolgreich close anrufen, wissen Sie nicht, dass Ihre Daten gesendet wurden.

Sigpipe sagt Ihnen, dass etwas schief gelaufen ist, es sagt Ihnen nicht, was oder was Sie dagegen tun sollten.

Ben Aveling
quelle
Sigpipe sagt Ihnen, dass etwas schief gelaufen ist, es sagt Ihnen nicht, was oder was Sie dagegen tun sollten. Genau. Praktisch der einzige Zweck von SIGPIPE besteht darin, Pipeline-Befehlszeilen-Dienstprogramme herunterzufahren, wenn die nächste Stufe die Eingabe nicht mehr benötigt. Wenn Ihr Programm vernetzt ist, sollten Sie SIGPIPE normalerweise programmweit blockieren oder ignorieren.
DepressedDaniel
Genau. SIGPIPE ist für Situationen wie "find XXX | head" gedacht. Sobald der Kopf seine 10 Linien erreicht hat, macht es keinen Sinn, mit dem Fund fortzufahren. Der Kopf wird also verlassen, und wenn find das nächste Mal versucht, mit ihm zu sprechen, erhält er ein Sigpipe und weiß, dass auch er austreten kann.
Ben Aveling
Der einzige Zweck von SIGPIPE besteht darin, dass Programme schlampig sind und beim Schreiben nicht auf Fehler überprüft werden!
William Pursell
2

Unter einem modernen POSIX-System (dh Linux) können Sie die sigprocmask()Funktion verwenden.

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

Wenn Sie den vorherigen Status später wiederherstellen möchten, stellen Sie sicher, dass Sie ihn an einem old_statesicheren Ort aufbewahren. Wenn Sie diese Funktion mehrmals aufrufen, müssen Sie entweder einen Stapel verwenden oder nur den ersten oder letzten speichern old_state... oder eine Funktion haben, die ein bestimmtes blockiertes Signal entfernt.

Weitere Informationen finden Sie auf der Manpage .

Alexis Wilke
quelle
Es ist nicht notwendig, den Satz blockierter Signale wie folgt zu lesen, zu ändern, zu schreiben. sigprocmaskFügt den Satz blockierter Signale hinzu, sodass Sie dies alles mit einem einzigen Anruf erledigen können.
Luchs
Ich lese, ändere und schreibe nicht , ich lese, um den aktuellen Status zu speichern, den ich behalte, old_statedamit ich ihn später wiederherstellen kann, wenn ich möchte. Wenn Sie wissen, dass Sie den Status niemals wiederherstellen müssen, müssen Sie ihn auf diese Weise nicht lesen und speichern.
Alexis Wilke
1
Aber Sie tun es: Der erste Aufruf zum sigprocmask()Lesen des alten Status, der Aufruf zum sigaddsetÄndern, der zweite Aufruf zum sigprocmask()Schreiben. Sie können den ersten Anruf entfernen, mit initialisieren sigemptyset(&set)und den zweiten Anruf in ändern sigprocmask(SIG_BLOCK, &set, &old_state).
Luchs
Ah! Ha ha! Du hast recht. Ich mache. Nun ... in meiner Software weiß ich nicht, welche Signale ich bereits blockiert oder nicht blockiert habe. Also mache ich es so. Ich stimme jedoch zu, dass, wenn Sie nur ein "Set-Signal auf diese Weise" haben, ein einfaches Clear + Set ausreicht.
Alexis Wilke