Warum gibt es SIGPIPE?

92

Nach meinem Verständnis SIGPIPEkann dies nur als Ergebnis von a auftreten write(), das -1 zurückgeben kann (und tut) und errnoauf EPIPE... gesetzt wird. Warum haben wir also den zusätzlichen Overhead eines Signals? Vermisse ich jedes Mal etwas, wenn ich mit Rohren arbeite, die ich ignoriere SIGPIPEund infolgedessen nie Schmerzen verspürt habe?

Shea Levy
quelle

Antworten:

111

Ich kaufe die zuvor akzeptierte Antwort nicht. SIGPIPEwird genau dann generiert, wenn das writefehlschlägt EPIPE, nicht im Voraus. Eine sichere Möglichkeit, dies zu vermeiden, SIGPIPEohne die globalen Signalanordnungen zu ändern, besteht darin, es vorübergehend zu maskieren pthread_sigmask, auszuführen writeund dann sigtimedwait(ohne Zeitüberschreitung) auszuführen , um ein anstehendes SIGPIPESignal zu verbrauchen (das an gesendet wird) den aufrufenden Thread, nicht den Prozess), bevor er erneut entlarvt wird.

Ich glaube, der Grund SIGPIPEdafür ist viel einfacher: Ein vernünftiges Standardverhalten für reine "Filter" -Programme festzulegen, die kontinuierlich Eingaben lesen, irgendwie transformieren und Ausgaben schreiben. Ohne SIGPIPE, sofern diese Programme Schreibfehler nicht explizit behandeln und sofort beenden (was ohnehin nicht das gewünschte Verhalten für alle Schreibfehler ist), werden sie so lange ausgeführt, bis ihnen die Eingabe ausgeht, selbst wenn ihre Ausgabepipe geschlossen wurde. Sicher, Sie können das Verhalten von duplizieren, SIGPIPEindem Sie explizit nach suchen EPIPEund es beenden, aber der gesamte Zweck von SIGPIPEbestand darin, dieses Verhalten standardmäßig zu erreichen, wenn der Programmierer faul ist.

R .. GitHub HÖREN SIE AUF, EIS ZU HELFEN
quelle
15
+1. Der Hinweis liegt in der Tatsache, dass SIGPIPE Sie standardmäßig tötet - es ist nicht dazu gedacht, einen Systemaufruf zu unterbrechen, sondern Ihr Programm zu beenden! Wenn Sie das Signal in einem Signalhandler verarbeiten können, können Sie auch den Rückkehrcode von verarbeiten write.
Nicholas Wilson
2
Du hast recht, ich weiß nicht, warum ich das überhaupt akzeptiert habe. Diese Antwort ist sinnvoll, obwohl IMO es seltsam ist, dass z. B. unter Linux diese Faulheit vom Kernel und nicht von der libc erreicht wird.
Shea Levy
5
Es klingt so, als würde diese Antwort im Grunde genommen auf "weil wir keine Ausnahmen hatten" hinauslaufen. Das Ignorieren von Rückkehrcodes in C ist jedoch ein viel größeres Problem als nur write () -Aufrufe. Was macht das Schreiben so besonders, dass es ein eigenes Signal benötigt? Vielleicht sind die reinen Filterprogramme viel häufiger, als ich mir vorstelle.
Arvid
@Arvid SIGPIPE wurde von Unix-Leuten erfunden, um ein Problem zu lösen, das sie in ihrer Umgebung hatten, in der Filterprogramme extrem häufig sind. Wir müssen nur die Boot-Skripte lesen, die das System aufrufen.
Kaz
@SheaLevy Welche Unix-Systeme implementieren SIGPIPE nur in ihrer Bibliothek?
Kaz
23

Weil Ihr Programm möglicherweise auf E / A wartet oder auf andere Weise angehalten wird. Ein SIGPIPE unterbricht Ihr Programm asynchron, beendet den Systemaufruf und kann so sofort verarbeitet werden.

Aktualisieren

Betrachten Sie eine Pipeline A | B | C.

Nur zur Bestimmtheit nehmen wir an, dass B die kanonische Kopierschleife ist:

while((sz = read(STDIN,bufr,BUFSIZE))>=0)
    write(STDOUT,bufr,sz);

Bwird beim Aufruf von read (2) blockiert und wartet auf Daten ab dem AZeitpunkt der CBeendigung. Wenn Sie auf den Rückkehrcode von write (2) warten , wann wird B ihn sehen? Die Antwort ist natürlich erst, wenn A mehr Daten schreibt (was eine lange Wartezeit sein kann - was ist, wenn A durch etwas anderes blockiert wird?). Beachten Sie übrigens, dass dies uns auch ein einfacheres und saubereres Programm ermöglicht. Wenn Sie beim Schreiben auf den Fehlercode angewiesen wären, benötigen Sie Folgendes:

while((sz = read(STDIN,bufr,BUFSIZE))>=0)
    if(write(STDOUT,bufr,sz)<0)
        break;

Ein weiteres Update

Aha, du bist verwirrt über das Verhalten des Schreibens. Sie sehen, wenn der Dateideskriptor mit dem ausstehenden Schreibvorgang geschlossen wird, geschieht das SIGPIPE sofort. Während der Schreibvorgang schließlich -1 zurückgibt , besteht der Sinn des Signals darin, Sie asynchron zu benachrichtigen, dass der Schreibvorgang nicht mehr möglich ist. Dies ist Teil dessen, was die gesamte elegante Co-Routine-Struktur von Pipes unter UNIX zum Funktionieren bringt.

Jetzt könnte ich Sie auf eine ganze Diskussion in einem der verschiedenen UNIX-Systemprogrammierbücher hinweisen, aber es gibt eine bessere Antwort: Sie können dies selbst überprüfen. Schreiben Sie ein einfaches BProgramm [1] - Sie haben bereits den Mut, alles, was Sie brauchen, ist ein mainund einige enthält - und fügen Sie einen Signalhandler für hinzu SIGPIPE. Führen Sie eine Pipeline wie aus

cat | B | more

Fügen Sie in einem anderen Terminalfenster einen Debugger an B hinzu und setzen Sie einen Haltepunkt in den B-Signalhandler.

Töte jetzt mehr und B sollte deinen Signalhandler einbrechen. Untersuche den Stapel. Sie werden feststellen, dass der Lesevorgang noch aussteht. Lassen Sie den Signalhandler fortfahren und zurückkehren und sehen Sie sich das vom Schreiben zurückgegebene Ergebnis an - das dann -1 ist.

[1] Natürlich schreibst du dein B-Programm in C. :-)

Charlie Martin
quelle
3
Warum sollte B die Kündigung von C mit SIGPIPE früher sehen? B bleibt beim Lesen blockiert, bis etwas in seine STDIN geschrieben wird. An diesem Punkt ruft es write () auf und erst dann wird SIGPIPE ausgelöst / -1 zurückgegeben.
Shea Levy
2
Die Antwort gefällt mir sehr gut: Mit SIGPIPE kann sich der Tod sofort vom Ausgabeende der Pipeline zurück ausbreiten. Ohne dies dauert es bis zu einem Zyklus des Kopierprogramms, bis jedes der N Elemente der Pipe die Pipeline beendet, und die Eingangsseite erzeugt N Linien, die niemals das Ende erreichen.
Yttrill
18
Diese Antwort ist falsch. SIGPIPEwird nicht während des Lesens geliefert, sondern während des write. Sie müssen kein C-Programm schreiben, um es zu testen. Führen Sie es einfach aus cat | headund pkill headin einem separaten Terminal. Sie werden sehen, dass catdas Warten glücklich davon lebt - read()nur wenn Sie etwas eingeben und die Eingabetaste drücken, catstirbt es mit einem kaputten Rohr, genau weil es versucht hat, eine Ausgabe zu schreiben.
user4815162342
5
-1 SIGPIPEkann nicht zugestellt werden, Bsolange Bes blockiert ist, readda SIGPIPEes erst generiert wird, wenn Bversucht wird, das write. Kein Thread kann "auf E / A warten oder anderweitig angehalten" sein, während er gleichzeitig anruft write.
Dan Moulding
3
Können Sie ein vollständiges Programm veröffentlichen, das zeigt SIGPIPE, dass es ausgelöst wird, während es auf einem blockiert ist read? Ich kann dieses Verhalten überhaupt nicht reproduzieren (und bin mir nicht sicher, warum ich das überhaupt akzeptiert habe)
Shea Levy
7

https://www.gnu.org/software/libc/manual/html_mono/libc.html

Dieser Link sagt:

Ein Rohr oder FIFO muss an beiden Enden gleichzeitig offen sein. Wenn Sie aus einer Pipe- oder FIFO-Datei lesen, in die keine Prozesse geschrieben wurden (möglicherweise, weil alle die Datei geschlossen oder beendet haben), gibt der Lesevorgang das Dateiende zurück. Das Schreiben in eine Pipe oder einen FIFO ohne Lesevorgang wird als Fehlerbedingung behandelt. es erzeugt ein SIGPIPE-Signal, und schlägt mit dem Fehlercode EPIPE fehl, wenn das Signal verarbeitet oder blockiert wird.

- Makro: int SIGPIPE

Rohrbruch. Wenn Sie Pipes oder FIFOs verwenden, müssen Sie Ihre Anwendung so gestalten, dass ein Prozess die Pipe zum Lesen öffnet, bevor ein anderer mit dem Schreiben beginnt. Wenn der Lesevorgang nie beginnt oder unerwartet endet, löst das Schreiben in die Pipe oder den FIFO ein SIGPIPE-Signal aus.Wenn SIGPIPE blockiert, behandelt oder ignoriert wird, schlägt der störende Anruf stattdessen mit EPIPE fehl.

Pipes und FIFO-Spezialdateien werden in Pipes und FIFOs ausführlicher behandelt.

Ankur Agarwal
quelle
5

Ich denke, es geht darum, die Fehlerbehandlung korrekt zu gestalten, ohne dass bei allem, was in eine Pipe geschrieben wird, viel Code erforderlich ist.

Einige Programme ignorieren den Rückgabewert von write(); ohne SIGPIPEsie würden sie nutzlos alle Ausgaben erzeugen.

Programme, die den Rückgabewert von write()wahrscheinlich prüfen, drucken eine Fehlermeldung, wenn dies fehlschlägt. Dies ist für ein defektes Rohr ungeeignet, da es nicht wirklich ein Fehler für die gesamte Pipeline ist.

jilles
quelle
2

Maschineninfo:

Linux 3.2.0-53-generic # 81-Ubuntu SMP Do 22. August 21:01:03 UTC 2013 x86_64 x86_64 x86_64 GNU / Linux

gcc Version 4.6.3 (Ubuntu / Linaro 4.6.3-1ubuntu5)

Ich habe diesen Code unten geschrieben:

// Writes characters to stdout in an infinite loop, also counts 
// the number of characters generated and prints them in sighandler
// writestdout.c

# include <unistd.h>
# include <stdio.h>
# include <signal.h>
# include <string.h>

int writeCount = 0;    
void sighandler(int sig) {
    char buf1[30] ;
    sprintf(buf1,"signal %d writeCount %d\n", sig, writeCount);
    ssize_t leng = strlen(buf1);
    write(2, buf1, leng);
    _exit(1);

}

int main() {

    int i = 0;
    char buf[2] = "a";

    struct sigaction ss;
    ss.sa_handler = sighandler;

    sigaction(13, &ss, NULL);

    while(1) {

        /* if (writeCount == 4) {

            write(2, "4th char\n", 10);

        } */

        ssize_t c = write(1, buf, 1);
        writeCount++;

    }

}

// Reads only 3 characters from stdin and exits
// readstdin.c

# include <unistd.h>
# include <stdio.h>

int main() {

    ssize_t n ;        
    char a[5];        
    n = read(0, a, 3);
    printf("read %zd bytes\n", n);
    return(0);

}

Ausgabe:

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 11486

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 429

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 281

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 490

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 433

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 318

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 468

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 11866

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 496

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 284

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 271

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 416

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 11268

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 427

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 8812

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 394

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 10937

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 10931

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 3554

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 499

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 283

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 11133

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 451

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 493

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 233

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 11397

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 492

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 547

$ ./writestdout | ./readstdin 
read 3 bytes
signal 13 writeCount 441

Sie können sehen, dass in jedem Fall SIGPIPEerst empfangen wird, nachdem mehr als 3 Zeichen durch den Schreibvorgang geschrieben wurden (versucht wurden).

Beweist dies nicht, dass dies SIGPIPEnicht unmittelbar nach Beendigung des Lesevorgangs generiert wird, sondern nach dem Versuch, weitere Daten in eine geschlossene Pipe zu schreiben?

Ankur Agarwal
quelle