Bagaimana mencegah SIGPIPE (atau menanganinya dengan benar)

261

Saya memiliki program server kecil yang menerima koneksi pada TCP atau soket UNIX lokal, membaca perintah sederhana dan, tergantung pada perintahnya, mengirimkan balasan. Masalahnya adalah bahwa klien mungkin tidak tertarik pada jawaban kadang-kadang dan keluar lebih awal, jadi menulis ke soket itu akan menyebabkan SIGPIPE dan membuat server saya crash. Apa praktik terbaik untuk mencegah kecelakaan di sini? Apakah ada cara untuk memeriksa apakah sisi lain dari garis masih membaca? (pilih () tampaknya tidak berfungsi di sini karena selalu mengatakan soket dapat ditulisi). Atau haruskah saya menangkap SIGPIPE dengan pawang dan mengabaikannya?

Jkramer
sumber

Jawaban:

255

Anda biasanya ingin mengabaikan SIGPIPEdan menangani kesalahan secara langsung dalam kode Anda. Ini karena penangan sinyal di C memiliki banyak batasan pada apa yang dapat mereka lakukan.

Cara paling portabel untuk melakukan ini adalah mengatur SIGPIPEhandler SIG_IGN. Ini akan mencegah agar soket atau pipa tidak menyebabkan SIGPIPEsinyal.

Untuk mengabaikan SIGPIPEsinyal, gunakan kode berikut:

signal(SIGPIPE, SIG_IGN);

Jika Anda menggunakan send()panggilan, opsi lain adalah menggunakan MSG_NOSIGNALopsi, yang akan mematikan SIGPIPEperilaku pada basis per panggilan. Perhatikan bahwa tidak semua sistem operasi mendukung MSG_NOSIGNALflag.

Terakhir, Anda mungkin juga ingin mempertimbangkan tanda SO_SIGNOPIPEsoket yang dapat diatur setsockopt()pada beberapa sistem operasi. Ini akan mencegah SIGPIPEdari disebabkan oleh menulis hanya ke soket itu sudah diatur.

dvorak
sumber
75
Untuk membuat ini eksplisit: sinyal (SIGPIPE, SIG_IGN);
jhclark
5
Ini mungkin diperlukan jika Anda mendapatkan kode pengembalian keluar 141 (128 + 13: SIGPIPE). SIG_IGN adalah penangan sinyal abaikan.
velcrow
6
Untuk soket, sangat mudah menggunakan send () dengan MSG_NOSIGNAL, seperti yang dikatakan @talash.
Pawel Veselov
3
Apa yang sebenarnya terjadi jika program saya menulis ke pipa yang rusak (soket dalam kasus saya)? SIG_IGN membuat program mengabaikan SIG_PIPE, tetapi apakah itu menghasilkan send () melakukan sesuatu yang tidak diinginkan?
sudo
2
Layak disebutkan, karena saya menemukannya sekali, lalu melupakannya kemudian menjadi bingung, kemudian menemukannya lagi untuk yang kedua kalinya. Debuggers (saya tahu gdb melakukan) mengesampingkan penanganan sinyal Anda, jadi sinyal yang diabaikan tidak diabaikan! Uji kode Anda di luar debugger untuk memastikan SIGPIPE tidak lagi terjadi. stackoverflow.com/questions/6821469/…
Jetski S-type
155

Metode lain adalah mengganti soket sehingga tidak pernah menghasilkan SIGPIPE saat write (). Ini lebih nyaman di perpustakaan, di mana Anda mungkin tidak menginginkan penangan sinyal global untuk SIGPIPE.

Pada kebanyakan sistem berbasis BSD (MacOS, FreeBSD ...), (dengan asumsi Anda menggunakan C / C ++), Anda dapat melakukan ini dengan:

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

Dengan ini berlaku, bukannya sinyal SIGPIPE yang dihasilkan, EPIPE akan dikembalikan.

pengguna55807
sumber
45
Ini kedengarannya bagus, tetapi SO_NOSIGPIPE tampaknya tidak ada di Linux - jadi ini bukan solusi portabel yang luas.
Nobar
1
hai semua, dapatkah Anda memberi tahu saya di mana tempat saya harus meletakkan kode ini? saya tidak mengerti terima kasih
R. Dewi
9
Di Linux, saya melihat opsi MSG_NOSIGNAL di flag of send
Aadishri
Berfungsi sempurna di Mac OS X
kirugan
116

Saya sangat terlambat ke pesta, tetapi SO_NOSIGPIPEtidak portabel, dan mungkin tidak bekerja pada sistem Anda (tampaknya menjadi hal BSD).

Alternatif yang baik jika Anda menggunakan, katakanlah, sistem Linux tanpa SO_NOSIGPIPEakan mengaturMSG_NOSIGNAL bendera pada panggilan kirim (2) Anda.

Contoh penggantian write(...)oleh send(...,MSG_NOSIGNAL)(lihat komentar nobar )

char buf[888];
//write( sockfd, buf, sizeof(buf) );
send(    sockfd, buf, sizeof(buf), MSG_NOSIGNAL );
sklnd
sumber
5
Dengan kata lain, gunakan send (..., MSG_NOSIGNAL) sebagai pengganti write () dan Anda tidak akan mendapatkan SIGPIPE. Ini harus bekerja dengan baik untuk soket (pada platform yang didukung), tetapi kirim () tampaknya terbatas untuk digunakan dengan soket (bukan pipa), jadi ini bukan solusi umum untuk masalah SIGPIPE.
nobar
@ Ray2k Siapa yang masih mengembangkan aplikasi untuk Linux <2.2? Itu sebenarnya pertanyaan setengah serius; kebanyakan distro dikirimkan dengan setidaknya 2.6.
Parthian Shot
1
@Parthian Shot: Anda harus memikirkan kembali jawaban Anda. Pemeliharaan sistem yang tertanam lama adalah alasan yang sah untuk merawat versi Linux yang lebih lama.
kirsche40
1
@ kirsche40 Saya bukan OP- Saya hanya ingin tahu.
Parthian Shot
@sklnd ini bekerja dengan baik untuk saya. Saya menggunakan QTcpSocketobjek Qt untuk membungkus penggunaan soket, saya harus mengganti writepemanggilan metode dengan OS send(menggunakan socketDescriptormetode). Adakah yang tahu opsi pembersih untuk mengatur opsi ini di QTcpSocketkelas?
Emilio González Montaña
29

Dalam posting ini saya menjelaskan solusi yang mungkin untuk kasus Solaris ketika SO_NOSIGPIPE atau MSG_NOSIGNAL tidak tersedia.

Sebagai gantinya, kita harus sementara menekan SIGPIPE di utas saat ini yang mengeksekusi kode pustaka. Inilah cara melakukannya: untuk menekan SIGPIPE, pertama-tama kita periksa apakah masih tertunda. Jika ya, ini berarti diblokir di utas ini, dan kita tidak perlu melakukan apa pun. Jika perpustakaan menghasilkan SIGPIPE tambahan, itu akan digabung dengan yang tertunda, dan itu adalah larangan. Jika SIGPIPE tidak tertunda maka kami memblokirnya di utas ini, dan juga memeriksa apakah itu sudah diblokir. Maka kita bebas untuk mengeksekusi tulisan kita. Ketika kita mengembalikan SIGPIPE ke keadaan semula, kita melakukan hal berikut: jika SIGPIPE tertunda pada awalnya, kita tidak melakukan apa-apa. Kalau tidak, kami akan memeriksa apakah sedang menunggu sekarang. Jika ya (yang berarti tindakan keluar telah menghasilkan satu atau lebih SIGPIPE), maka kami menunggu di utas ini, dengan demikian menghapus status tertunda (untuk melakukan ini kami menggunakan sigtimedwait () dengan nol waktu habis; ini adalah untuk menghindari pemblokiran dalam skenario di mana pengguna jahat mengirim SIGPIPE secara manual ke seluruh proses: dalam hal ini kita akan melihatnya menunggu, tetapi utas lainnya mungkin tangani sebelum kita punya perubahan untuk menunggu). Setelah menghapus status tertunda, kami membuka blokir SIGPIPE di utas ini, tetapi hanya jika tidak diblokir semula.

Contoh kode di https://github.com/kroki/XProbes/blob/1447f3d93b6dbf273919af15e59f35cca58fcc23/src/libxprobes.c#L156

kroki
sumber
22

Menangani SIGPIPE Secara Lokal

Biasanya lebih baik untuk menangani kesalahan secara lokal daripada dalam penangan event sinyal global karena secara lokal Anda akan memiliki lebih banyak konteks tentang apa yang terjadi dan apa yang harus diambil.

Saya memiliki lapisan komunikasi di salah satu aplikasi saya yang memungkinkan aplikasi saya untuk berkomunikasi dengan aksesori eksternal. Ketika kesalahan penulisan terjadi, saya melempar dan mengecualikan di lapisan komunikasi dan membiarkannya menggelembung ke blok coba tangkap untuk menangani di sana.

Kode:

Kode untuk mengabaikan sinyal SIGPIPE sehingga Anda dapat menanganinya secara lokal adalah:

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

Kode ini akan mencegah sinyal SIGPIPE dinaikkan, tetapi Anda akan mendapatkan kesalahan baca / tulis saat mencoba menggunakan soket, jadi Anda harus memeriksanya.

Sam
sumber
haruskah panggilan ini dilakukan setelah menghubungkan soket atau sebelum itu? di mana tempat terbaik. Terima kasih
Ahmed
@Ahmed Saya pribadi memasukkannya ke dalam applicationDidFinishLaunching: . Yang utama adalah bahwa itu harus sebelum berinteraksi dengan koneksi soket.
Sam
tetapi Anda akan mendapatkan kesalahan baca / tulis saat mencoba menggunakan soket, jadi Anda harus memeriksanya. - Bolehkah saya bertanya bagaimana ini mungkin? sinyal (SIGPIPE, SIG_IGN) bekerja untuk saya tetapi di bawah mode debug mengembalikan kesalahan apakah mungkin juga mengabaikan kesalahan itu?
jongbanaag
@ Dreyfus15 Saya yakin saya baru saja membungkus panggilan menggunakan soket di blok coba / tangkap untuk mengatasinya secara lokal. Sudah beberapa saat sejak saya melihat ini.
Sam
15

Anda tidak dapat mencegah proses di ujung pipa keluar, dan jika keluar sebelum Anda selesai menulis, Anda akan mendapatkan sinyal SIGPIPE. Jika Anda SIG_IGN sinyal, maka tulisan Anda akan kembali dengan kesalahan - dan Anda perlu mencatat dan bereaksi terhadap kesalahan itu. Hanya menangkap dan mengabaikan sinyal di handler bukanlah ide yang baik - Anda harus mencatat bahwa pipa sekarang mati dan mengubah perilaku program sehingga tidak menulis ke pipa lagi (karena sinyal akan dihasilkan lagi, dan diabaikan) lagi, dan Anda akan mencoba lagi, dan seluruh proses bisa berjalan untuk waktu yang lama dan menghabiskan banyak daya CPU).

Jonathan Leffler
sumber
6

Atau haruskah saya menangkap SIGPIPE dengan pawang dan mengabaikannya?

Saya percaya itu benar. Anda ingin tahu kapan ujung lainnya telah menutup deskriptor mereka dan itulah yang dikatakan SIGPIPE kepada Anda.

Sam

Sam Reynolds
sumber
3

Manual Linux mengatakan:

EPIPE Ujung lokal telah dimatikan pada soket yang berorientasi koneksi. Dalam hal ini prosesnya juga akan menerima SIGPIPE kecuali MSG_NOSIGNAL ditetapkan.

Tetapi untuk Ubuntu 12.04 itu tidak benar. Saya menulis tes untuk kasus itu dan saya selalu menerima EPIPE tanpa SIGPIPE. SIGPIPE dibuat jika saya mencoba menulis ke soket yang sama untuk kedua kalinya. Jadi Anda tidak perlu mengabaikan SIGPIPE jika sinyal ini terjadi itu berarti kesalahan logika dalam program Anda.

talash
sumber
1
Saya paling pasti menerima SIGPIPE tanpa terlebih dahulu melihat EPIPE pada soket di linux. Ini terdengar seperti bug kernel.
davmac
3

Apa praktik terbaik untuk mencegah kecelakaan di sini?

Nonaktifkan sigpipe sesuai setiap orang, atau tangkap dan abaikan kesalahan tersebut.

Apakah ada cara untuk memeriksa apakah sisi lain dari garis masih membaca?

Ya, gunakan select ().

pilih () tampaknya tidak berfungsi di sini karena selalu mengatakan soket dapat ditulisi.

Anda harus memilih pada bit baca . Anda mungkin dapat mengabaikan penulisan bit .

Ketika ujung jauh menutup pegangan file-nya, pilih akan memberi tahu Anda bahwa ada data yang siap dibaca. Ketika Anda pergi dan membacanya, Anda akan mendapatkan kembali 0 byte, yang merupakan cara OS memberi tahu Anda bahwa pegangan file telah ditutup.

Satu-satunya waktu Anda tidak dapat mengabaikan bit tulis adalah jika Anda mengirim volume besar, dan ada risiko ujung yang lain ditumpuk, yang dapat menyebabkan buffer Anda terisi. Jika itu terjadi, maka mencoba menulis ke pegangan file dapat menyebabkan program / utas Anda terblokir atau gagal. Tes pilih sebelum menulis akan melindungi Anda dari itu, tetapi itu tidak menjamin bahwa ujung yang lain sehat atau bahwa data Anda akan tiba.

Perhatikan bahwa Anda bisa mendapatkan sigpipe dari close (), serta saat Anda menulis.

Tutup memerah semua data yang disangga. Jika ujung lainnya sudah ditutup, maka tutup akan gagal, dan Anda akan menerima sigpipe.

Jika Anda menggunakan buffer TCPIP, maka penulisan yang berhasil berarti data Anda telah diantrekan untuk dikirim, itu tidak berarti telah dikirim. Sampai Anda berhasil menutup panggilan, Anda tidak tahu bahwa data Anda telah dikirim.

Sigpipe memberi tahu Anda ada yang tidak beres, tidak memberi tahu Anda apa, atau apa yang harus Anda lakukan.

Ben Aveling
sumber
Sigpipe memberi tahu Anda ada yang tidak beres, tidak memberi tahu Anda apa, atau apa yang harus Anda lakukan. Persis. Sebenarnya satu-satunya tujuan SIGPIPE adalah untuk menghancurkan utilitas baris perintah yang disalurkan ketika tahap berikutnya tidak lagi membutuhkan input. Jika program Anda melakukan networking, biasanya Anda hanya perlu memblokir atau mengabaikan program SIGPIPE.
DepressedDaniel
Tepat. SIGPIPE ditujukan untuk situasi seperti "find XXX | head". Setelah head telah mencocokkan 10 barisnya, tidak ada gunanya melanjutkan pencarian. Jadi kepala keluar, dan kali berikutnya menemukan mencoba untuk berbicara dengannya, ia mendapatkan sigpipe dan tahu bahwa itu juga dapat keluar.
Ben Aveling
Sungguh, satu-satunya tujuan SIGPIPE adalah untuk memungkinkan program menjadi ceroboh dan tidak memeriksa kesalahan penulisan!
William Pursell
2

Di bawah sistem POSIX modern (yaitu Linux), Anda dapat menggunakan sigprocmask()fungsinya.

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

Jika Anda ingin mengembalikan keadaan sebelumnya nanti, pastikan untuk menyimpan old_statetempat yang aman. Jika Anda memanggil fungsi itu beberapa kali, Anda harus menggunakan tumpukan atau hanya menyimpan yang pertama atau terakhirold_state ... atau mungkin memiliki fungsi yang menghilangkan sinyal yang diblokir tertentu.

Untuk info lebih lanjut baca halaman manual .

Alexis Wilke
sumber
Tidak perlu membaca-memodifikasi-menulis set sinyal yang diblokir seperti ini. sigprocmaskmenambah set sinyal yang diblokir, sehingga Anda dapat melakukan semua ini dengan satu panggilan.
Luchs
Saya tidak membaca-memodifikasi-menulis , saya membaca untuk menyimpan keadaan saat ini yang saya simpan old_statesehingga saya bisa mengembalikannya nanti, jika saya mau. Jika Anda tahu Anda tidak perlu lagi memulihkan kondisi, memang tidak perlu membaca dan menyimpannya dengan cara ini.
Alexis Wilke
1
Tetapi Anda melakukannya: panggilan pertama untuk sigprocmask()membaca keadaan lama, panggilan untuk sigaddsetmengubahnya, panggilan kedua untuk sigprocmask()menulisnya. Anda dapat menghapus untuk panggilan pertama, menginisialisasi dengan sigemptyset(&set)dan mengubah panggilan kedua ke sigprocmask(SIG_BLOCK, &set, &old_state).
Luchs
Ah! Ha ha! Kamu benar. Saya lakukan. Ya ... di perangkat lunak saya, saya tidak tahu sinyal mana yang sudah saya blokir atau belum blokir. Jadi saya melakukannya dengan cara ini. Namun, saya setuju bahwa jika Anda hanya memiliki satu "set sinyal seperti ini" maka set + jelas sederhana sudah cukup.
Alexis Wilke