Почему существует SIGPIPE?


94

Насколько я понимаю, это SIGPIPEможет произойти только в результате a write(), который может (и действительно) вернуть -1 и установить errnoв EPIPE... Итак, почему у нас есть дополнительные накладные расходы на сигнал? Каждый раз, когда я работаю с трубами, я игнорирую SIGPIPEи в результате никогда не чувствовал боли, я что-то упускаю?

Ответы:


112

Я не куплюсь на ранее принятый ответ. SIGPIPEгенерируется именно тогда, когда происходит writeсбой EPIPE, а не заранее - на самом деле, один безопасный способ избежать SIGPIPEбез изменения расположения глобальных сигналов - это временно замаскировать его pthread_sigmask, выполнить write, а затем выполнить sigtimedwait(с нулевым таймаутом), чтобы поглотить любой ожидающий SIGPIPEсигнал (который отправляется в вызывающий поток, а не процесс), прежде чем снова его демаскировать.

Я считаю, что причина SIGPIPEсуществует гораздо проще: установить разумное поведение по умолчанию для чистых «фильтрующих» программ, которые постоянно читают ввод, каким-то образом его преобразуют и записывают вывод. Без этого SIGPIPE, если эти программы явно не обрабатывают ошибки записи и не завершают работу немедленно (что в любом случае может быть нежелательным для всех ошибок записи), они будут продолжать работу до тех пор, пока не закончатся входные данные, даже если их выходной канал был закрыт. Конечно, вы можете дублировать поведение SIGPIPE, явно проверяя наличие EPIPEи завершая работу, но вся цель SIGPIPEзаключалась в том, чтобы добиться этого поведения по умолчанию, когда программист ленив.


15
+1. Подсказка в том, что SIGPIPE убивает вас по умолчанию - он не предназначен для прерывания системного вызова, он предназначен для завершения вашей программы! Если вы можете обрабатывать сигнал в обработчике сигнала, вы можете также обработать код возврата write.
Николас Уилсон

2
Вы правы, я не знаю, почему я вообще это принял. Этот ответ имеет смысл, хотя, как мне кажется, странно, что, например, в Linux эта лень достигается ядром, а не libc.
Shea Levy

5
похоже, что этот ответ сводится к следующему: «потому что у нас не было исключений». Однако люди, игнорирующие коды возврата в C, представляют собой гораздо более широкую проблему, чем просто вызовы write (). Что делает запись настолько особенной, что ей нужен собственный сигнал? возможно, программы с чистыми фильтрами встречаются гораздо чаще, чем я предполагаю.
Arvid

@Arvid SIGPIPE был изобретен людьми Unix, чтобы решить проблему, с которой они столкнулись в своей среде, в которой программы фильтрации чрезвычайно распространены. Все, что нам нужно сделать, это прочитать сценарии загрузки, которые вызывают систему.
Каз

@SheaLevy Какие системы Unix реализуют SIGPIPE только в своей libc?
Каз

23

Поскольку ваша программа может ожидать ввода-вывода или иным образом приостановлена. SIGPIPE асинхронно прерывает вашу программу, прерывая системный вызов, и поэтому может быть обработан немедленно.

Обновить

Рассмотрим трубопровод A | B | C .

Для определенности предположим, что B - это канонический цикл копирования:

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

Bблокируется при вызове read (2), ожидающем данных с Aмомента Cзавершения. Если вы дождетесь кода возврата от write (2) , когда B его увидит? Ответ, конечно, не будет до тех пор, пока A не запишет больше данных (что может занять долгое время - что, если A заблокирован чем-то другим?). Заметьте, кстати, что это также позволяет нам более простую и чистую программу. Если вы зависели от кода ошибки при записи, вам понадобится что-то вроде:

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

Еще одно обновление

Ага, вы запутались в поведении пишите. Видите ли, когда дескриптор файла с ожидающей записью закрывается, сразу же происходит SIGPIPE. Пока запись вернет -1 конечном итоге , весь смысл сигнала состоит в том, чтобы асинхронно уведомить вас о том, что запись больше невозможна. Это часть того, что заставляет всю элегантную структуру совместных подпрограмм работать в UNIX.

Теперь я мог бы указать вам на целое обсуждение в любой из нескольких книг по системному программированию UNIX, но есть лучший ответ: вы можете проверить это сами. Напишите простую Bпрограмму [1] - у вас уже есть смелость, все, что вам нужно, это mainи некоторые включает - и добавьте обработчик сигнала для SIGPIPE. Запустите конвейер как

cat | B | more

а в другом окне терминала подключите отладчик к B и поместите точку останова в обработчик сигнала B.

Теперь убейте больше, и B должен сломать ваш обработчик сигналов. изучить стек. Вы обнаружите, что чтение еще не завершено. позвольте обработчику сигнала продолжить и вернуться, и посмотрите на результат, возвращаемый записью, который тогда будет -1.

[1] Естественно, вы будете писать свою программу B на C. :-)


3
Почему B увидит прекращение C раньше с помощью SIGPIPE? B будет оставаться заблокированным при чтении до тех пор, пока что-то не будет записано в его STDIN, после чего он вызовет write (), и только тогда будет поднят SIGPIPE / будет возвращено -1.
Ши Леви,

2
Мне очень нравится ответ: SIGPIPE позволяет мгновенно распространиться обратно с выходного конца конвейера. Без этого требуется до одного цикла программы копирования для каждого из N элементов конвейера, чтобы убить конвейер, и заставляет входную сторону генерировать N строк, которые никогда не достигают конца.
Yttrill

18
Это неверный ответ. SIGPIPEэто не поставляется во время чтения, но во время write. Вам не нужно написать программу C , чтобы проверить это, просто запустить cat | head, и pkill headв отдельном терминале. Вы увидите, что он catсчастливо живет, ожидая в своем read()- только когда вы вводите что-то и нажимаете Enter, catумирает со сломанным каналом именно потому, что он пытался записать вывод.
user4815162342

5
-1 SIGPIPEне может быть доставлен, Bпока Bзаблокирован, readпотому что SIGPIPEне создается до тех пор, пока не Bпопытается write. Ни один поток не может «ждать ввода-вывода или иным образом приостановлен» при одновременном вызове write.
Дэн Молдинг

3
Можете ли вы опубликовать полную программу, в которой показано, SIGPIPEчто вас поднимают при блокировке read? Я вообще не могу воспроизвести это поведение (и вообще не уверен, почему я принял это в первую очередь)
Ши Леви,

7

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

Эта ссылка говорит:

Труба или FIFO должны быть открыты с обоих концов одновременно. Если вы читаете из канала или файла FIFO, в который не записывают никакие процессы (возможно, потому, что все они закрыли файл или вышли из него), при чтении возвращается конец файла. Запись в канал или FIFO, в котором нет процесса чтения, рассматривается как состояние ошибки; он генерирует сигнал SIGPIPE, и выдает ошибку с кодом EPIPE, если сигнал обрабатывается или блокируется.

- Макрос: int SIGPIPE

Сломанная труба. Если вы используете каналы или FIFO, вы должны спроектировать свое приложение так, чтобы один процесс открывал канал для чтения, прежде чем другой начнет запись. Если процесс чтения никогда не начинается или неожиданно завершается, запись в канал или FIFO вызывает сигнал SIGPIPE.Если SIGPIPE блокируется, обрабатывается или игнорируется, вызывающий нарушение вызов терпит неудачу с EPIPE.

Каналы и специальные файлы FIFO более подробно обсуждаются в разделе Каналы и FIFO.


5

Я думаю, что это нужно для правильной обработки ошибок, не требуя большого количества кода при записи в канал.

Некоторые программы игнорируют возвращаемое значение write(); безSIGPIPE них они бесполезно генерируют весь вывод.

Программы, которые проверяют возвращаемое значение, write()скорее всего, выдают сообщение об ошибке в случае сбоя; это не подходит для сломанной трубы, так как это не ошибка для всего трубопровода.


2

Информация о машине:

Linux 3.2.0-53-generic # 81-Ubuntu SMP Чт, 22 августа 21:01:03 UTC 2013 x86_64 x86_64 x86_64 GNU / Linux

gcc версии 4.6.3 (Ubuntu / Linaro 4.6.3-1ubuntu5)

Я написал этот код ниже:

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

}

Выход:

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

Вы можете видеть, что в каждом случае SIGPIPEон получен только после того, как в процессе записи было записано более 3 символов (попытка записать).

Разве это не доказывает, что SIGPIPEгенерируется не сразу после завершения процесса чтения, а после попытки записи дополнительных данных в закрытый канал?

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.