Gangguan panggilan sistem ketika sinyal ditangkap

29

Dari membaca halaman manual pada read()dan write()panggilan tampaknya panggilan ini akan terganggu oleh sinyal terlepas dari apakah mereka harus memblokir atau tidak.

Secara khusus, asumsikan

  • suatu proses menetapkan handler untuk beberapa sinyal.
  • perangkat dibuka (katakanlah, terminal) dengan O_NONBLOCK tidak diatur (yaitu beroperasi dalam mode pemblokiran)
  • proses kemudian membuat read()panggilan sistem untuk membaca dari perangkat dan sebagai hasilnya menjalankan jalur kontrol kernel di ruang-kernel.
  • sementara precess mengeksekusi read()di dalam ruang kernel, sinyal di mana handler dipasang sebelumnya dikirim ke proses itu dan handler sinyal dipanggil.

Membaca halaman manual dan bagian yang sesuai di SUSv3 'Volume Antarmuka Sistem (XSH)' , orang menemukan bahwa:

saya. Jika a read()terganggu oleh sinyal sebelum membaca data apa pun (yaitu harus memblokir karena tidak ada data), ia mengembalikan -1 dengan errnoset ke [EINTR].

ii. Jika a read()terputus oleh sinyal setelah berhasil membaca beberapa data (yaitu mungkin untuk segera mulai melayani permintaan), ia mengembalikan jumlah byte yang dibaca.

Pertanyaan A): Apakah saya benar mengasumsikan bahwa dalam kedua kasus (blok / tidak ada blok) pengiriman dan penanganan sinyal tidak sepenuhnya transparan kepada read()?

Kasus i. tampaknya dapat dimengerti karena pemblokiran read()biasanya akan menempatkan proses dalam TASK_INTERRUPTIBLEkeadaan sehingga ketika suatu sinyal dikirimkan, kernel menempatkan proses ke dalam TASK_RUNNINGkeadaan.

Namun ketika read()tidak perlu memblokir (kasus ii.) Dan sedang memproses permintaan di kernel-space, saya akan berpikir bahwa kedatangan sinyal dan penanganannya akan transparan seperti kedatangan dan penanganan yang tepat dari HW akan mengganggu. Khususnya saya akan berasumsi bahwa setelah pengiriman sinyal, proses akan sementara ditempatkan ke mode pengguna untuk menjalankan penangan sinyal dari mana ia akan kembali pada akhirnya untuk menyelesaikan pemrosesan yang terputus read()(dalam ruang kernel) sehingga read()menjalankan Tentu saja sampai selesai setelah proses kembali ke titik tepat setelah panggilan ke read()(dalam ruang pengguna), dengan semua byte yang tersedia dibaca sebagai hasilnya.

Tapi ii. tampaknya menyiratkan bahwa read()terputus, karena data tersedia segera, tetapi mengembalikan hanya beberapa data (bukan semua).

Ini membawa saya ke pertanyaan kedua (dan terakhir) saya:

Pertanyaan B): Jika asumsi saya di bawah A) benar, mengapa read()bisa terganggu, meskipun tidak perlu diblokir karena ada data yang tersedia untuk memenuhi permintaan segera? Dengan kata lain, mengapa yang read()tidak dilanjutkan setelah mengeksekusi penangan sinyal, akhirnya menghasilkan semua data yang tersedia (yang tersedia setelah semua) dikembalikan?

darbehdar
sumber

Jawaban:

29

Ringkasan: Anda benar bahwa menerima sinyal tidak transparan, baik dalam kasus i (terputus tanpa membaca apa pun) maupun dalam kasus ii (terputus setelah membaca sebagian). Untuk melakukan sebaliknya jika saya perlu melakukan perubahan mendasar pada arsitektur sistem operasi dan arsitektur aplikasi.

Tampilan implementasi OS

Pertimbangkan apa yang terjadi jika panggilan sistem terganggu oleh sinyal. Penangan sinyal akan menjalankan kode mode pengguna. Tetapi syscall handler adalah kode kernel dan tidak mempercayai kode mode pengguna apa pun. Jadi mari kita jelajahi pilihan untuk penangan syscall:

  • Hentikan panggilan sistem; melaporkan berapa banyak yang dilakukan terhadap kode pengguna. Terserah kode aplikasi untuk me-restart panggilan sistem dengan cara tertentu, jika diinginkan. Begitulah cara kerja unix.
  • Simpan status panggilan sistem, dan izinkan kode pengguna untuk melanjutkan panggilan. Ini bermasalah karena beberapa alasan:
    • Ketika kode pengguna sedang berjalan, sesuatu dapat terjadi untuk membatalkan keadaan yang disimpan. Misalnya, jika membaca dari file, file tersebut mungkin terpotong. Jadi kode kernel akan membutuhkan banyak logika untuk menangani kasus-kasus ini.
    • Status yang disimpan tidak dapat diizinkan untuk menyimpan kunci apa pun, karena tidak ada jaminan bahwa kode pengguna akan melanjutkan kembali syscall, dan kemudian kunci akan ditahan selamanya.
    • Kernel harus mengekspos antarmuka baru untuk melanjutkan atau membatalkan syscalls yang sedang berlangsung, di samping antarmuka normal untuk memulai syscall. Ini adalah banyak komplikasi untuk kasus yang jarang terjadi.
    • Status yang disimpan perlu menggunakan sumber daya (memori, setidaknya); sumber daya tersebut perlu dialokasikan dan dipegang oleh kernel tetapi dihitung berdasarkan penjatahan proses. Ini tidak dapat diatasi, tetapi ini adalah komplikasi.
      • Perhatikan bahwa penangan sinyal mungkin membuat panggilan sistem yang terputus; jadi Anda tidak bisa hanya memiliki penjatahan sumber daya statis yang mencakup semua syscalls yang mungkin.
      • Dan bagaimana jika sumber daya tidak dapat dialokasikan? Maka syscall harus gagal juga. Yang berarti aplikasi harus memiliki kode untuk menangani kasus ini, sehingga desain ini tidak akan menyederhanakan kode aplikasi.
  • Tetap dalam proses (tetapi ditangguhkan), buat utas baru untuk penangan sinyal. Ini, sekali lagi, bermasalah:
    • Implementasi unix awal memiliki satu utas per proses.
    • Penangan sinyal akan mengambil risiko melangkahi sepatu syscall. Ini memang masalah, tetapi dalam desain unix saat ini, ini terkandung.
    • Sumber daya perlu dialokasikan untuk utas baru; Lihat di atas.

Perbedaan utama dengan interupsi adalah bahwa kode interupsi dipercaya, dan sangat dibatasi. Biasanya tidak diperbolehkan mengalokasikan sumber daya, atau berlari selamanya, atau mengambil kunci dan tidak melepaskannya, atau melakukan hal-hal buruk lainnya; sejak interrupt handler ditulis oleh implementor OS sendiri, dia tahu itu tidak akan melakukan hal buruk. Di sisi lain, kode aplikasi dapat melakukan apa saja.

Tampilan desain aplikasi

Ketika aplikasi terputus di tengah-tengah panggilan sistem, haruskah syscall terus selesai? Tidak selalu. Misalnya, pertimbangkan program seperti shell yang membaca garis dari terminal, dan pengguna menekan Ctrl+C, memicu SIGINT. Pembacaan tidak harus lengkap, itu yang menjadi sinyal. Perhatikan bahwa contoh ini menunjukkan bahwa readsyscall harus interruptible walaupun belum ada byte yang dibaca.

Jadi harus ada cara bagi aplikasi untuk memberitahu kernel untuk membatalkan panggilan sistem. Di bawah desain unix, itu terjadi secara otomatis: sinyal membuat panggilan sistem kembali. Desain lain akan membutuhkan cara bagi aplikasi untuk melanjutkan atau membatalkan syscall di waktu luangnya.

The readsystem call adalah cara itu karena itu adalah primitif yang masuk akal, mengingat desain umum dari sistem operasi. Artinya, kira-kira, "baca sebanyak yang Anda bisa, hingga batas (ukuran buffer), tetapi berhenti jika sesuatu yang lain terjadi". Untuk benar-benar membaca buffer penuh melibatkan berjalan readdalam satu lingkaran sampai byte sebanyak mungkin telah dibaca; ini adalah fungsi tingkat yang lebih tinggi fread(3),. Berbeda dengan read(2)yang merupakan panggilan sistem, freadadalah fungsi perpustakaan, diimplementasikan di ruang pengguna di atas read. Ini cocok untuk aplikasi yang membaca untuk file atau mati saat mencoba; itu tidak cocok untuk juru bahasa baris perintah atau untuk program jaringan yang harus membatasi koneksi dengan bersih, juga untuk program jaringan yang memiliki koneksi bersamaan dan tidak menggunakan utas.

Contoh membaca dalam satu lingkaran disediakan dalam Pemrograman Sistem Linux Robert Love:

ssize_t ret;
while (len != 0 && (ret = read (fd, buf, len)) != 0) {
  if (ret == -1) {
    if (errno == EINTR)
      continue;
    perror ("read");
    break;
  }
  len -= ret;
  buf += ret;
}

Itu mengurus case idan case iidan beberapa lagi.

Gilles 'SANGAT berhenti menjadi jahat'
sumber
Terima kasih banyak Gilles untuk jawaban yang sangat singkat dan jelas yang menguatkan pandangan serupa yang diajukan dalam sebuah artikel tentang filosofi desain UNIX. Tampak sangat meyakinkan bagi saya bahwa perilaku gangguan syscall ada hubungannya dengan filosofi desain UNIX daripada kendala teknis atau hambatan
darbehdar
@darbehdar Semuanya bertiga: filosofi desain unix (di sini terutama bahwa proses kurang dipercaya dari kernel dan dapat menjalankan kode arbitrer, juga bahwa proses dan utas tidak dibuat secara implisit), kendala teknis (pada alokasi sumber daya), dan desain aplikasi (di sana adalah kasus ketika sinyal harus membatalkan panggilan sys).
Gilles 'SO- stop being evil'
2

Untuk menjawab pertanyaan A :

Ya, pengiriman dan penanganan sinyal tidak sepenuhnya transparan untuk read().

The read()setengah berlari dapat menduduki beberapa sumber sementara itu terganggu oleh sinyal. Dan pengendali sinyal dapat memanggil yang lain read()(atau syscall safe-signal async lainnya ) juga. Jadi read()terganggu oleh sinyal harus dihentikan terlebih dahulu untuk melepaskan sumber daya yang digunakannya, jika tidak read()dipanggil dari penangan sinyal akan mengakses sumber daya yang sama dan menyebabkan masalah reentrant.

Karena panggilan sistem selain read()dapat dipanggil dari penangan sinyal dan mereka juga dapat menempati rangkaian sumber daya yang sama seperti read()halnya. Untuk menghindari masalah reentrant di atas, desain yang paling sederhana dan teraman adalah menghentikan interupsi read()setiap kali ketika sinyal terjadi selama menjalankannya.

Justin
sumber