Чому існує SIGPIPE?


93

Наскільки я розумію, це 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
Ви маєте рацію, я не знаю, чому я прийняв це взагалі. Ця відповідь має сенс, хоча IMO дивно, що, наприклад, в Linux ця лінь досягається ядром, а не libc.
Ши Леві

5
схоже, ця відповідь в основному зводиться до: "тому що у нас не було винятків". Однак, ігнорування кодів повернення на мові C є набагато ширшим питанням, ніж просто написання () дзвінків. Що робить запис настільки особливим, що йому потрібен власний сигнал? можливо, чисті програми фільтрів набагато частіше, ніж я уявляю.
Арвід

@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блокується на читання (2) очікування виклику для даних з , Aколи Cзавершується. Якщо ви дочекаєтесь коду повернення із write (2) , коли B побачить його? Відповідь, звичайно, не до тих пір, поки А не напише більше даних (а це може бути довгим очікуванням - а що, якщо А заблоковано чимось іншим?). До речі, зауважте, що це також дозволяє нам зробити більш просту, чисту програму. Якщо ви залежали від коду помилки при записі, вам знадобиться щось на зразок:

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. Вам не потрібно писати програму на С, щоб перевірити її, просто запустіть cat | headі pkill headв окремому терміналі. Ви побачите, що catщасливо живе, чекаючи у своєму read()- лише коли ви щось набираєте і натискаєте enter, справді catвмирає із зламаною трубою, саме тому, що він намагався записати вихідні дані.
користувач4815162342

5
-1 SIGPIPEнеможливо доставити, Bпоки Bзаблоковано, readоскільки SIGPIPEне генерується до Bспроб write. Жоден потік не може одночасно "чекати вводу-виводу або призупинено" write.
Dan Molding

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 Thu Aug 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.