Dlaczego istnieje SIGPIPE?

94

Z mojego zrozumienia SIGPIPEmoże wystąpić tylko jako wynik a write(), które może (i robi) zwracać -1 i ustawiać errnona EPIPE... Dlaczego więc mamy dodatkowy narzut sygnału? Za każdym razem, gdy pracuję z rurami, ignoruję SIGPIPEi nigdy nie odczuwałem bólu, czy czegoś mi brakuje?

Shea Levy
źródło

Odpowiedzi:

112

Nie kupuję wcześniej zaakceptowanej odpowiedzi. SIGPIPEjest generowany dokładnie wtedy, gdy wystąpi writeawaria EPIPE, a nie wcześniej - w rzeczywistości jednym bezpiecznym sposobem uniknięcia SIGPIPEbez zmiany globalnych dyspozycji sygnału jest tymczasowe zamaskowanie go pthread_sigmask, wykonanie write, a następnie wykonanie sigtimedwait(z zerowym limitem czasu), aby skonsumować każdy oczekujący SIGPIPEsygnał (który jest wysyłany do wątek wywołujący, a nie proces) przed ponownym zdemaskowaniem.

Uważam, że przyczyna SIGPIPEjest znacznie prostsza: ustanowienie rozsądnego zachowania domyślnego dla czystych programów „filtrujących”, które stale odczytują dane wejściowe, w jakiś sposób je przekształcają i zapisują dane wyjściowe. Bez SIGPIPEtych programów, chyba że te programy jawnie obsługują błędy zapisu i natychmiast kończą pracę (co i tak może nie być pożądanym zachowaniem dla wszystkich błędów zapisu), będą działać do momentu wyczerpania danych wejściowych, nawet jeśli ich potok wyjściowy został zamknięty. Oczywiście możesz zduplikować zachowanie programu SIGPIPE, jawnie sprawdzając EPIPEi kończąc, ale głównym celem SIGPIPEbyło osiągnięcie tego zachowania domyślnie, gdy programista jest leniwy.

R .. GitHub PRZESTAŃ POMÓC LODOWI
źródło
15
+1. Wskazówka polega na tym, że SIGPIPE domyślnie cię zabija - nie jest przeznaczony do przerywania wywołania systemowego, jest przeznaczony do kończenia programu! Jeśli jesteś w stanie obsłużyć sygnał w programie obsługi sygnału, równie dobrze możesz obsłużyć kod powrotu write.
Nicholas Wilson,
2
Masz rację, nie wiem, dlaczego to zaakceptowałem. Ta odpowiedź ma sens, chociaż IMO to dziwne, że np. W Linuksie to lenistwo jest osiągane przez jądro, a nie przez libc.
Shea Levy
5
wygląda na to, że ta odpowiedź sprowadza się w zasadzie do: „ponieważ nie mieliśmy wyjątków”. Jednak ludzie ignorujący kody powrotu w C to znacznie szerszy problem niż zwykłe wywołania metody write (). Co sprawia, że ​​pisanie jest tak wyjątkowe, że potrzebuje własnego sygnału? być może czyste programy filtrujące są dużo bardziej powszechne, niż sobie wyobrażam.
Arvid
@Arvid SIGPIPE został wymyślony przez ludzi z Uniksa, aby rozwiązać problem, który mieli w swoim środowisku, w którym programy filtrujące są niezwykle powszechne. Wszystko, co musimy zrobić, to przeczytać skrypty startowe, które uruchamiają system.
Kaz
@SheaLevy Które systemy uniksowe implementują SIGPIPE wyłącznie w swoim libc?
Kaz
23

Ponieważ Twój program może czekać na we / wy lub w inny sposób zawieszony. SIGPIPE przerywa program asynchronicznie, kończąc wywołanie systemowe, dzięki czemu może być obsłużony natychmiast.

Aktualizacja

Rozważ rurociąg A | B | C.

Dla jasności przyjmiemy, że B jest kanoniczną pętlą kopiowania:

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

Bjest blokowany w wywołaniu read (2) oczekującym na dane od Amomentu Czakończenia. Jeśli czekasz na kod powrotu z write (2) , kiedy B go zobaczy? Odpowiedź oczywiście nie brzmi, dopóki A nie zapisze większej ilości danych (co może oznaczać długie oczekiwanie - a co, jeśli A zostanie zablokowane przez coś innego?). Zauważ przy okazji, że to również pozwala nam na prostszy, czystszy program. Jeśli polegałeś na kodzie błędu z zapisu, potrzebujesz czegoś takiego:

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

Kolejna aktualizacja

Aha, jesteś zdezorientowany co do zachowania pisma. Widzisz, kiedy deskryptor pliku z oczekującym zapisem jest zamknięty, SIGPIPE dzieje się natychmiast. Podczas gdy zapis w końcu zwróci -1 , celem sygnału jest asynchroniczne powiadomienie, że zapis nie jest już możliwy. Jest to część tego, co sprawia, że ​​cała elegancka współrutyna struktura potoków działa w systemie UNIX.

Mógłbym teraz wskazać Ci całą dyskusję w dowolnej z kilku książek o programowaniu w systemach UNIX, ale jest lepsza odpowiedź: możesz to sprawdzić samodzielnie. Napisz prosty Bprogram [1] - masz już odwagę, wszystko czego potrzebujesz to a, maina niektóre zawierają - i dodaj obsługę sygnału dla SIGPIPE. Uruchom potok, taki jak

cat | B | more

aw innym oknie terminala dołącz debugger do B i umieść punkt przerwania wewnątrz procedury obsługi sygnału B.

Teraz zabij więcej, a B powinien włamać się do twojego modułu obsługi sygnału. zbadaj stos. Przekonasz się, że odczyt jest nadal w toku. niech obsługa sygnału kontynuuje i wraca, i spójrz na wynik zwrócony przez write - który będzie wtedy wynosił -1.

[1] Oczywiście napiszesz swój program B w C. :-)

Charlie Martin
źródło
3
Dlaczego B miałby zobaczyć wypowiedzenie C wcześniej z SIGPIPE? B pozostanie zablokowany podczas odczytu, dopóki coś nie zostanie zapisane na jego STDIN, w którym to momencie wywoła metodę write () i tylko wtedy SIGPIPE zostanie podniesione / -1 zostanie zwrócone.
Shea Levy
2
Naprawdę podoba mi się odpowiedź: SIGPIPE umożliwia natychmiastową propagację śmierci z wyjściowego końca potoku. Bez tego jeden cykl programu kopiującego dla każdego z N elementów potoku zabija potok i powoduje, że strona wejściowa generuje N linii, które nigdy nie osiągają końca.
Yttrill
18
Ta odpowiedź jest nieprawidłowa. SIGPIPEnie jest dostarczany podczas odczytu, ale podczas write. Nie trzeba napisać program w C, aby go przetestować, wystarczy uruchomić cat | head, a pkill headw oddzielnym terminalu. Zobaczysz, że catszczęśliwie żyje czekając w swoim - read()tylko wtedy, gdy coś wpiszesz i naciśniesz enter, catumiera z pękniętą potoką, dokładnie dlatego, że próbował napisać wyjście.
user4815162342
5
-1 SIGPIPEnie można dostarczyć doB podczas gdy Bjest zablokowany, readponieważ SIGPIPEnie jest generowany, dopóki nie Bspróbuje write. Żaden wątek nie może „czekać na I / O lub w inny sposób zawieszony” podczas jednoczesnego wywoływania write.
Dan Molding
3
Czy możesz opublikować pełny program, który pokazuje SIGPIPE został zgłoszony, gdy jest zablokowany na read? W ogóle nie mogę odtworzyć tego zachowania (i właściwie nie jestem pewien, dlaczego zaakceptowałem to w pierwszej kolejności)
Shea Levy
7

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

Ten link mówi:

Rura lub FIFO muszą być otwarte na obu końcach jednocześnie. Jeśli czytasz z potoku lub pliku FIFO, który nie ma żadnych procesów zapisujących do niego (być może dlatego, że wszystkie zamknęły plik lub zakończyły pracę), odczyt zwraca koniec pliku. Zapis do potoku lub FIFO, który nie ma procesu odczytu, jest traktowany jako warunek błędu; generuje sygnał SIGPIPE i kończy się niepowodzeniem z kodem błędu EPIPE, jeśli sygnał jest obsługiwany lub blokowany.

- Makro: int SIGPIPE

Złamana rura. Jeśli używasz potoków lub FIFO, musisz zaprojektować aplikację tak, aby jeden proces otwierał potok do odczytu, zanim inny zacznie pisać. Jeśli proces odczytu nigdy się nie rozpoczyna lub kończy się nieoczekiwanie, zapis do potoku lub FIFO wywołuje sygnał SIGPIPE. Jeśli SIGPIPE jest blokowany, obsługiwany lub ignorowany, wywołanie powodujące naruszenie kończy się niepowodzeniem z EPIPE.

Potoki i pliki specjalne FIFO są omówione bardziej szczegółowo w potokach i FIFO.

Ankur Agarwal
źródło
5

Myślę, że jest to poprawna obsługa błędów bez konieczności pisania dużej ilości kodu we wszystkim, co jest zapisywane w potoku.

Niektóre programy ignorują zwracaną wartość write(); bezSIGPIPE nich generowałyby bezużytecznie cały wynik.

Programy, które sprawdzają zwracaną wartość write()prawdopodobnie wyświetlają komunikat o błędzie, jeśli to się nie powiedzie; jest to niewłaściwe w przypadku pękniętej rury, ponieważ w rzeczywistości nie jest to błąd dla całego rurociągu.

Jilles
źródło
2

Informacje o maszynie:

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

gcc w wersji 4.6.3 (Ubuntu / Linaro 4.6.3-1ubuntu5)

Napisałem ten kod poniżej:

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

}

Wynik:

$ ./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

Możesz zobaczyć, że w każdym przypadku SIGPIPEjest odbierany dopiero po tym, jak więcej niż 3 znaki są (próbowano) napisać przez proces pisania.

Czy to nie dowodzi, że SIGPIPEnie jest generowany natychmiast po zakończeniu odczytu, ale po próbie zapisania większej ilości danych do zamkniętego potoku?

Ankur Agarwal
źródło