Dalam postingan blog ini, Chris Down, seorang Insinyur Kernel di Meta, membahas kesalahan dalam menggunakan sinyal Linux di lingkungan produksi Linux dan mengapa developer harus menghindari penggunaan sinyal jika memungkinkan.
Sinyal adalah peristiwa yang dihasilkan sistem Linux sebagai tanggapan terhadap beberapa kondisi. Sinyal dapat dikirim oleh kernel ke suatu proses, oleh proses ke proses lain, atau proses itu sendiri. Setelah menerima sinyal, suatu proses dapat mengambil tindakan.
Sinyal adalah bagian inti dari lingkungan operasi mirip Unix dan telah ada sejak awal waktu. Sinyal tersebut adalah pipa untuk banyak komponen inti dari sistem operasi—timbunan inti (core dumping), pengelolaan siklus proses, dsb.—dan secara umum, sinyal telah bertahan cukup baik dalam penggunaan kita selama lima puluh tahun atau lebih. Dengan demikian, ketika seseorang berkata bahwa menggunakan sinyal untuk komunikasi antarproses (IPC) memiliki potensi bahaya, banyak orang mungkin berpikir ini adalah ocehan orang yang putus asa dan buang-buang waktu menemukan sesuatu yang sudah ditemukan orang lain. Namun, artikel ini akan menunjukkan kasus ketika sinyal adalah penyebab masalah produksi. Artikel ini juga menawarkan beberapa mitigasi dan alternatif potensial.
Sinyal mungkin tampak menarik karena standardisasinya, ketersediaannya yang luas, dan fakta bahwa sinyal tersebut tidak memerlukan dependensi tambahan apa pun di luar apa yang disediakan oleh sistem operasi. Namun, sinyal bisa sulit digunakan dengan aman. Sinyal memiliki banyak asumsi sehingga orang harus berhati-hati dalam memvalidasi agar cocok dengan persyaratan mereka. Jika tidak, orang harus berhati-hati untuk mengonfigurasikan dengan benar. Pada kenyataannya, banyak aplikasi, bahkan aplikasi yang dikenal luas, tidak melakukan validasi ataupun konfigurasi dengan benar. Akibatnya, mungkin aplikasi tersebut akan mengalami insiden yang sulit di-debug di masa mendatang.
Mari kita lihat insiden baru-baru ini yang terjadi di lingkungan produksi Meta, yang menekankan kesalahan penggunaan sinyal. Kita akan membahas secara singkat sejarah beberapa sinyal dan bagaimana sinyal tersebut membawa kita ke posisi sekarang, dan kemudian kita akan membandingkannya dengan kebutuhan dan masalah saat ini yang kita lihat dalam produksi.
Pertama-tama, mari kita mundur sedikit. Tim LogDevice membersihkan basis kode mereka, menghapus kode dan fitur yang tidak digunakan. Salah satu fitur yang tidak digunakan lagi adalah jenis catatan yang mendokumentasikan operasi tertentu yang dilakukan oleh layanan. Fitur ini akhirnya mubazir, tidak memiliki konsumen, dan karena itu dihapus. Anda dapat melihat perubahannya di sini di GitHub. Sejauh ini, bagus.
Beberapa saat berikutnya, setelah perubahan berlanjut tanpa harus banyak diceritakan, produksi terus berjalan dengan stabil dan melayani traffic seperti biasa. Beberapa minggu kemudian, diterimalah laporan bahwa node layanan hilang dengan tingkat mengejutkan. Itu ada hubungannya dengan peluncuran rilis baru, tetapi apa yang sebenarnya salah masih belum jelas. Apa yang berbeda sekarang yang menyebabkan hal-hal menjadi kacau?
Tim bersangkutan mempersempit masalah menjadi perubahan kode yang kami sebutkan terdahulu, membuat catatan-catatan ini tidak berlaku lagi. Lantas, apa alasannya? Apa yang salah dengan kode itu? Jika Anda belum mengetahui jawabannya, kami mengundang Anda untuk melihat perbedaan itu dan mencoba mencari tahu apa yang salah karena itu tidak langsung terlihat, dan itu adalah kesalahan yang bisa dilakukan siapa pun.
logrotate kurang lebih merupakan fitur standar untuk rotasi catatan saat menggunakan Linux. logrotate sudah ada selama hampir tiga puluh tahun sekarang, dan konsepnya sederhana: mengelola siklus catatan dengan merotasi dan memvakumnya.
logrotate tidak mengirim sinyal apa pun dengan sendirinya, jadi Anda tidak akan menemukan banyak, jika ada, tentang logrotate di halaman utama logrotate atau dokumentasinya. Namun, logrotate dapat mengambil perintah arbitrer untuk dieksekusi sebelum atau sesudah rotasinya. Seperti contoh dasar dari konfigurasi logrotate default di CentOS, Anda dapat melihat konfigurasi ini:
/var/log/cron /var/log/maillog /var/log/messages /var/log/secure /var/log/spooler { sharedscripts postrotate /bin/kill -HUP `cat /var/run/syslogd.pid 2> /dev/null` 2> /dev/null || true endscript }
Agak rapuh, tetapi kami akan memaafkan itu dan menganggap ini berfungsi sebagaimana mestinya. Konfigurasi ini berkata bahwa setelah logrotate merotasi file apa pun yang tercantum dalam daftar, logrotate seharusnya mengirim SIGHUP
ke PID yang ada di dalam /var/run/syslogd.pid
, yang seharusnya menjalankan instance syslogd
.
Ini semua baik untuk sesuatu dengan API publik yang stabil seperti syslog, tetapi bagaimana dengan sesuatu yang internal di mana penerapan SIGHUP
adalah detail penerapan internal yang dapat berubah sewaktu-waktu?
Salah satu masalah di sini adalah—kecuali untuk sinyal yang tidak dapat ditangkap di ruang pengguna sehingga hanya memiliki satu arti, seperti SIGKILL
dan SIGSTOP
—arti semantik dari sinyal itu terserah developer aplikasi dan pengguna untuk menafsirkan dan memprogram. Dalam beberapa kasus, perbedaannya sebagian besar bersifat akademis, seperti SIGTERM
, yang cukup banyak dipahami secara universal berarti "penghentian dengan baik sesegera mungkin". Namun dalam hal SIGHUP
, artinya secara signifikan kurang jelas.
SIGHUP
diciptakan untuk lini serial dan pada awalnya digunakan untuk menunjukkan bahwa ujung lain dari koneksi telah terputus. Saat ini, kita masih meneruskan lini ini tentu saja, jadi SIGHUP
masih dikirim untuk padanan modernnya: di mana terminal semu (pseudo-terminal) atau terminal virtual ditutup (makanya ada fitur seperti nohup
yang menutupinya).
Pada hari-hari awal Unix, ada kebutuhan untuk menerapkan pemuatan ulang daemon. Ini biasanya terdiri dari setidaknya pembukaan kembali file konfigurasi/catatan tanpa memulai ulang, dan sinyal tampak seperti cara bebas dependensi untuk mencapainya. Tentu saja, tidak ada sinyal untuk hal seperti itu, tetapi karena daemon ini tidak memiliki terminal pengontrol, seharusnya tidak ada alasan untuk menerima SIGHUP
, jadi sepertinya sinyal yang mudah untuk membonceng (piggyback) tanpa efek samping yang jelas.
Namun ada halangan kecil dengan rencana ini. Status default untuk sinyal tidak "diabaikan", tetapi spesifik sinyal. Contoh: program tidak harus mengonfigurasi SIGTERM
secara manual untuk menghentikan aplikasinya. Selama program tidak mengatur sinyal lain, kernel hanya merekam program secara gratis, tanpa kode apa pun yang diperlukan di ruang pengguna. Mudah!
Yang tidak mudah adalah bahwaSIGHUP
juga memiliki perilaku menghentikan program segera. Ini berfungsi baik untuk kasus hangup asli, ketika aplikasi ini mungkin tidak diperlukan lagi, tetapi tidak begitu bagus untuk arti baru ini.
Tentu saja ini tidak apa-apa jika kita menghapus semua tempat yang berpotensi mengirim SIGHUP
ke program. Masalahnya adalah bahwa dalam database besar dan mapan, hal itu sulit. SIGHUP
tidak seperti panggilan IPC yang dikontrol ketat sehingga Anda dapat dengan mudah memahami basis kodenya. Sinyal dapat datang dari mana saja, kapan saja, dan ada beberapa pemeriksaan pada operasinya (selain yang paling dasar "apakah Anda pengguna ini atau memiliki CAP_KILL
"). Intinya adalah sulit untuk menentukan dari mana sinyal bisa datang, tetapi dengan IPC yang lebih eksplisit, kita akan tahu bahwa sinyal ini tidak berarti apa-apa bagi kita dan harus diabaikan.
Sekarang, saya kira Anda mungkin sudah mulai menebak apa yang terjadi. Rilis LogDevice dimulai pada suatu sore berisi perubahan kode yang disebutkan di atas. Pada awalnya, tidak ada yang salah, tetapi pada tengah malam keesokan harinya, semuanya secara misterius mulai kacau. Alasannya adalah bait berikut dalam konfigurasi logrotate mesin, yang mengirimkan SIGHUP
yang kini tidak tertangani (dan karena itu, fatal) ke daemon logdevice:
/var/log/logdevice/audit.log { daily # [...] postrotate pkill -HUP logdeviced endscript }
Satu bait pendek hilang dari konfigurasi logrotate itu sangat mudah dan umum terjadi ketika menghapus fitur besar. Sayangnya, sulit juga untuk memastikan bahwa setiap sisa terakhir eksistensinya telah dihapus sekaligus. Bahkan dalam kasus yang lebih mudah untuk divalidasi daripada ini, kesalahan masih ada sisa saat melakukan pembersihan kode itu sering terjadi. Namun, biasanya, itu tanpa konsekuensi yang merusak, yaitu: sisa-sisanya mati atau kode tanpa operasi.
Secara konseptual, insiden itu sendiri dan resolusinya sederhana: jangan kirim SIGHUP
, dan sebarkan tindakan LogDevice lebih banyak dari waktu ke waktu (yaitu: jangan jalankan ini pada tengah malam secara langsung). Namun, bukan hanya nuansa kejadian yang satu ini yang harus kita fokuskan di sini. Insiden ini, lebih dari segalanya, harus berfungsi sebagai platform untuk mencegah penggunaan sinyal dalam produksi untuk apa pun selain kasus yang paling mendasar dan penting.
Pertama, penggunaan sinyal sebagai mekanisme untuk memengaruhi perubahan status proses sistem operasi itu mantap. Ini termasuk sinyal seperti SIGKILL
, yang tidak mungkin diinstal penangan sinyal dan melakukan persis seperti yang Anda harapkan, dan perilaku default-kernel dari SIGABRT
, SIGTERM
, SIGINT
, SIGSEGV
, dan SIGQUIT
dan sejenisnya, yang umumnya dipahami dengan baik oleh pengguna dan pemrogram.
Kesamaan dari semua sinyal ini adalah bahwa setelah Anda menerimanya, semua sinyal ini maju menuju status akhir terminal di dalam kernel itu sendiri. Artinya, tidak ada lagi instruksi ruang pengguna yang akan dieksekusi setelah Anda mendapatkan SIGKILL
atau SIGTERM
tanpa penangan sinyal ruang pengguna.
Status akhir terminal penting karena biasanya berarti Anda berupaya mengurangi kompleksitas stack dan kode yang saat ini sedang dieksekusi. Status lain yang diinginkan sering berakibat malah makin kompleks dan lebih sulit untuk dinalar karena konkurensi dan alur kode menjadi lebih kacau.
Anda mungkin memperhatikan bahwa kami tidak menyebutkan beberapa sinyal lain yang juga berhenti secara default. Berikut daftar semua sinyal standar yang berhenti secara default (tidak termasuk sinyal timbunan inti (core dump) seperti SIGABRT
atau SIGSEGV
karena mereka semua masuk akal):
Sepintas, ini mungkin tampak masuk akal, tetapi berikut adalah beberapa pencilan (outlier):
Jadi itu hampir sepertiga dari sinyal terminal, yang paling baik dipertanyakan dan, paling buruk, secara aktif berbahaya karena kebutuhan program berubah. Lebih buruk lagi, bahkan sinyal yang seharusnya "ditentukan pengguna" bisa menjadi bencana yang akan terjadi ketika seseorang lupa melakukan SIG_IGN
secara eksplisit. Bahkan SIGUSR1
atau SIGPOLL
yang tidak berbahaya juga dapat menyebabkan insiden.
Ini bukan hanya masalah familiaritas. Tidak peduli seberapa baik Anda mengetahui cara kerja sinyal, menulis kode sinyal yang benar untuk pertama kalinya sangatlah sulit karena, terlepas dari penampilannya, sinyal jauh lebih kompleks daripada yang terlihat.
Pemrogram umumnya tidak menghabiskan waktu mereka untuk memikirkan cara kerja sinyal. Ini berarti bahwa ketika benar-benar menerapkan penanganan sinyal, pemrogram sering melakukan hal yang salah secara halus.
Saya bahkan tidak berbicara tentang kasus "sepele", seperti keamanan dalam fungsi penanganan sinyal, yang sebagian besar diselesaikan hanya dengan menabrak sig_atomic_t
, atau menggunakan hal pagar sinyal atomik dari C++. Tidak, itu sebagian besar mudah dicari dan diingat sebagai kesalahan oleh siapa pun setelah pertama kali mengalami masalah sinyal. Yang jauh lebih sulit adalah penalaran tentang aliran kode bagian nominal dari program yang kompleks ketika menerima sinyal. Melakukannya harus memikirkan sinyal terus-menerus secara eksplisit di setiap bagian dari siklus aplikasi (hei, bagaimana dengan EINTR
, apakah SA_RESTART
cukup di sini? Alur apa yang harus kita masuki jika ini berhenti sebelum waktunya? Saya sekarang memiliki program bersamaan, apa implikasinya?), atau menyiapkan sigprocmask
atau pthread_setmask
untuk beberapa bagian dari siklus aplikasi Anda dan berharap agar alur kode tidak pernah berubah (yang tentu saja bukan tebakan yang baik dalam suasana pengembangan yang serbacepat). signalfd
atau menjalankan sigwaitinfo
di utas khusus dapat membantu agak di sini, tetapi keduanya memiliki kasus edge dan masalah kegunaan yang cukup membuatnya sulit untuk direkomendasikan.
Kami ingin percaya bahwa sebagian besar pemrogram berpengalaman sekarang tahu bahwa bahkan contoh jenaka dari menulis kode aman-utas dengan benar sangatlah sulit. Jika Anda pikir menulis kode aman-utas dengan benar itu sulit, sinyal secara signifikan lebih sulit. Penangan sinyal hanya boleh mengandalkan kode bebas-kunci dengan struktur data atomik, masing-masing, karena alur utama eksekusi ditangguhkan dan kami tidak tahu kunci apa yang dipegangnya, dan karena alur utama eksekusi dapat melakukan operasi non-atomik. Penangan sinyal juga harus sepenuhnya masuk kembali, yaitu: harus dapat berlapis di dalam diri sendiri karena penangan sinyal dapat tumpang tindih jika sinyal dikirim beberapa kali (atau bahkan dengan satu sinyal, dengan SA_NODEFER
). Itulah salah satu alasan mengapa Anda tidak dapat menggunakan fungsi seperti printf
atau malloc
dalam penangan sinyal karena fungsi-fungsi tersebut mengandalkan mutex global untuk sinkronisasi. Jika Anda menahan kunci itu saat sinyal diterima dan kemudian memanggil fungsi yang membutuhkan kunci itu lagi, aplikasi Anda akan menemui jalan buntu. Ini benar-benar sulit untuk dinalar. Itu sebabnya banyak orang hanya menulis sesuatu seperti berikut ini sebagai penanganan sinyal mereka:
static volatile sig_atomic_t received_sighup; static void sighup(int sig __attribute__((unused))) { received_sighup = 1; } static int configure_signal_handlers(void) { return sigaction( SIGHUP, &(const struct sigaction){.sa_handler = sighup, .sa_flags = SA_RESTART}, NULL); } int main(int argc, char *argv[]) { if (configure_signal_handlers()) { /* failed to set handlers */ } /* usual program flow */ if (received_sighup) { /* reload */ received_sighup = 0; } /* usual program flow */ }
Masalahnya adalah, selagi demikian, signalfd
, atau upaya lain dalam penanganan sinyal asinkron mungkin terlihat cukup sederhana dan kuat, hal ini mengabaikan fakta bahwa titik interupsi itu sama pentingnya dengan tindakan yang dilakukan setelah menerima sinyal. Contoh: anggaplah kode ruang pengguna Anda sedang melakukan I/O atau mengubah metadata objek yang berasal dari kernel (seperti inode atau FD). Dalam hal ini, Anda mungkin benar-benar berada di stack ruang kernel pada saat interupsi. Contoh: inilah tampilan utas saat mencoba menutup deskriptor file:
# cat /proc/2965230/stack [<0>] schedule+0x43/0xd0 [<0>] io_schedule+0x12/0x40 [<0>] wait_on_page_bit+0x139/0x230 [<0>] filemap_write_and_wait+0x5a/0x90 [<0>] filp_close+0x32/0x70 [<0>] __x64_sys_close+0x1e/0x50 [<0>] do_syscall_64+0x4e/0x140 [<0>] entry_SYSCALL_64_after_hwframe+0x44/0xa9
Di sini, __x64_sys_close
adalah varian x86_64 dari panggilan sistem close
, yang menutup deskriptor file. Pada titik ini dalam eksekusinya, kami sedang menunggu penyimpanan cadangan diperbarui (yaitu wait_on_page_bit
). Karena kerja I/O biasanya beberapa kali lipat lebih lambat daripada operasi lainnya, schedule
di sini adalah cara untuk secara sukarela memberi isyarat kepada penjadwal CPU kernel bahwa kita akan melakukan operasi latensi tinggi (seperti I/O disk atau jaringan) dan penjadwal harus mempertimbangkan untuk mencari proses lain untuk dijadwalkan daripada proses saat ini. Ini bagus, karena memungkinkan kita untuk memberi sinyal ke kernel bahwa melanjutkan dan memilih proses yang benar-benar akan menggunakan CPU adalah ide yang baik daripada membuang waktu untuk proses yang tidak dapat dilanjutkan sampai selesai menunggu tanggapan dari sesuatu yang mungkin memakan waktu cukup lama.
Bayangkan kita mengirim sinyal ke proses yang sedang kita jalankan. Sinyal yang telah kita kirim memiliki penangan ruang pengguna di utas penerima, jadi kita akan melanjutkan di ruang pengguna. Salah satu dari banyak kemungkinan akhir balapan ini adalah kernel akan mencoba keluar dari schedule
, lebih lanjut melepas stack dan akhirnya menampilkan kesalahan ESYSRESTART
atau EINTR
ke ruang pengguna untuk menunjukkan bahwa kita terinterupsi. Namun seberapa jauh kita berhasil menutupnya? Apa status deskriptor file sekarang?
Sekarang setelah kita kembali ke ruang pengguna, kita akan menjalankan penangan sinyal. Saat penangan sinyal keluar, kami akan menyebarkan kesalahan ke pembungkus close
libc ruang pengguna, lalu ke aplikasi—yang secara teori—dapat melakukan sesuatu tentang situasi yang dihadapi. Kami mengatakan "dalam teori" karena sangat sulit untuk mengetahui apa yang harus dilakukan dalam berbagai situasi dengan sinyal, dan banyak layanan dalam produksi tidak menangani kasus edge dengan baik. Itu mungkin tidak apa-apa di beberapa aplikasi di mana integritas data tidak begitu penting. Namun, dalam aplikasi produksi yang memang peduli tentang konsistensi dan integritas data, ini menghadirkan masalah yang signifikan: kernel tidak mengekspos cara granular apa pun untuk memahami seberapa jauh yang didapat, apa yang dicapai dan yang tidak dicapai, dan apa yang sebenarnya harus kita lakukan terhadap situasi tersebut. Lebih buruk lagi, jika close
memberikan EINTR
, status deskriptor file sekarang tidak ditentukan:
“If close() is interrupted by a signal [...] the state of [the file descriptor] is unspecified.”
Semoga berhasil mencoba dalam menalar cara menanganinya dengan aman dan selamat di aplikasi Anda. Secara umum, menangani EINTR
bahkan untuk panggilan sistem yang berperilaku baik itu rumit. Ada banyak masalah halus yang membentuk bagian besar dari alasan mengapa SA_RESTART
tidak cukup. Tidak semua panggilan sistem dapat dimulai ulang, dan mengharapkan setiap developer aplikasi Anda bisa memahami dan memitigasi nuansa mendalam dari mendapatkan sinyal untuk setiap panggilan sistem di setiap situs panggilan itu adalah meminta pemadaman. Dari man 7 signal
:
“The following interfaces are never restarted after being interrupted by a signal handler, regardless of the use of SA_RESTART; they always fail with the error EINTR [...]”
Demikian juga, menggunakan sigprocmask
dan mengharapkan alur kode tetap statis menimbulkan masalah karena developer biasanya tidak menghabiskan waktu untuk memikirkan batas-batas penanganan sinyal atau bagaimana menghasilkan atau mempertahankan kode sinyal yang benar. Hal yang sama berlaku untuk menangani sinyal di utas khusus dengan sigwaitinfo
, yang dapat dengan mudah berakhir dengan GDB dan fitur serupa tidak dapat melakukan debug proses. Alur kode yang salah secara halus atau penanganan kesalahan dapat mengakibatkan bug, crash, kerusakan yang sulit di-debug, kebuntuan, dan banyak lagi masalah yang akan membuat Anda langsung disambut hangat oleh fitur pengelolaan insiden pilihan Anda.
Jika Anda memikirkan semua pembahasan tentang konkurensi, reentrancy, dan atomisitas ini cukup buruk, memasukkan multiutas ke dalam campuran tersebut membuat segalanya menjadi lebih rumit. Ini sangat penting ketika mempertimbangkan fakta bahwa banyak aplikasi kompleks menjalankan utas terpisah secara implisit, contoh: sebagai bagian dari jemalloc, GLib, atau sejenisnya. Beberapa dari pustaka ini bahkan menginstal penangan sinyal sendiri, membuka masalah worm lainnya.
Secara keseluruhan, man 7 signal
berkata demikian tentang hal tersebut:
“A signal may be generated (and thus pending) for a process as a whole (e.g., when sent using kill(2)) or for a specific thread [...] If more than one of the threads has the signal unblocked, then the kernel chooses an arbitrary thread to which to deliver the signal.”
Lebih ringkasnya, "untuk sebagian besar sinyal, kernel mengirimkan sinyal ke utas mana pun yang tidak memblokir sinyal dengan sigprocmask
". SIGSEGV, SIGILL, dan sejenisnya menyerupai jebakan, dan memiliki sinyal yang secara eksplisit diarahkan ke utas yang menyinggung. Namun, terlepas dari apa yang mungkin dipikirkan orang, sebagian besar sinyal tidak dapat secara eksplisit dikirim ke satu utas dalam grup utas, bahkan dengan tgkill
atau pthread_kill
.
Ini berarti bahwa Anda tidak dapat dengan mudah mengubah keseluruhan karakteristik penanganan sinyal segera setelah Anda memiliki set utas. Jika suatu layanan perlu melakukan pemblokiran sinyal secara berkala dengan sigprocmask
di utas utama, Anda perlu berkomunikasi dengan utas lain secara eksternal tentang bagaimana utas lain harus menangani hal tersebut. Jika tidak, sinyal dapat ditelan oleh utas lain, tidak akan pernah terlihat lagi. Tentu saja, Anda dapat memblokir sinyal di utas anak untuk menghindari hal ini, tetapi jika mereka perlu melakukan penanganan sinyal sendiri, bahkan untuk hal-hal primitif seperti waitpid
, itu akan membuat segalanya menjadi rumit.
Sama seperti semua hal lain di sini, ini bukan masalah yang secara teknis tidak dapat diatasi. Namun, orang akan lalai dalam mengabaikan fakta bahwa kompleksitas sinkronisasi yang diperlukan agar ini berfungsi dengan benar itu merepotkan, dan meletakkan dasar untuk bug, kebingungan, dan lebih buruk lagi.
Sinyal disebarkan secara asinkron di kernel. Panggilan sistem kill
kembali segera setelah sinyal tertunda direkam untuk proses atau task_struct
utas yang dimaksud. Jadi, tidak ada jaminan pengiriman tepat waktu, meskipun sinyal tidak diblokir.
Bahkan jika ada itu pengiriman sinyal yang tepat waktu, tidak ada cara untuk berkomunikasi kembali ke penerbit sinyal tentang status permintaan tindakan mereka. Dengan demikian, setiap tindakan yang berarti tidak boleh dikirimkan oleh sinyal, karena mereka hanya menerapkan fire-and-forget (tembak dan lupakan) tanpa mekanisme nyata untuk melaporkan keberhasilan atau kegagalan pengiriman dan tindakan selanjutnya. Seperti yang telah kita lihat di atas, bahkan sinyal yang tampaknya tidak berbahaya pun bisa berbahaya jika tidak dikonfigurasi di ruang pengguna.
Siapa pun yang menggunakan Linux cukup lama pasti akan mengalami kasus di mana mereka ingin mematikan beberapa proses tetapi menemukan bahwa prosesnya tidak responsif bahkan terhadap sinyal yang seharusnya selalu fatal seperti SIGKILL
. Masalahnya adalah bahwa secara menyesatkan, tujuan kill(1) bukanlah untuk mematikan proses, tetapi hanya untuk mengantrekan permintaan ke kernel (tanpa indikasi kapan akan dilayani) bahwa seseorang telah meminta beberapa tindakan untuk diambil.
Tugas panggilan sistem kill
adalah menandai sinyal sebagai tertunda dalam metadata tugas kernel, yang berhasil bahkan ketika tugas SIGKILL tidak mati. Dalam hal SIGKILL
khususnya, kernel menjamin bahwa tidak ada lagi instruksi mode pengguna yang akan dieksekusi, tetapi kami mungkin masih harus mengeksekusi instruksi dalam mode kernel untuk menyelesaikan tindakan yang dapat mengakibatkan kerusakan data atau untuk merilis sumber daya. Untuk alasan ini, kami masih berhasil bahkan jika statusnya D (tidur tidak terputus). KILL sendiri tidak gagal kecuali Anda memberikan sinyal yang tidak valid, Anda tidak memiliki izin untuk mengirim sinyal itu atau PID yang Anda minta untuk mengirim sinyal tidak ada dan dengan demikian tidak berguna untuk menyebarkan status non-terminal secara andal ke aplikasi.
Cara untuk menghindari masalah penanganan sinyal adalah dengan tidak menanganinya sama sekali. Namun, untuk aplikasi yang menangani pemrosesan status yang harus melakukan sesuatu tentang kasus seperti SIGTERM
, idealnya gunakan API level tinggi seperti folly::AsyncSignalHandler
yang sejumlah warts-nya telah dibuat lebih intuitif.
sigprocmask
, mengurangi jumlah kode yang perlu diperiksa secara teratur apakah sinyal sudah benar. Ingatlah bahwa jika alur kode atau strategi utas berubah, sigprocmask mungkin tidak memiliki efek yang Anda inginkan.signal(SIGHUP, SIG_IGN); signal(SIGQUIT, SIG_IGN); signal(SIGUSR1, SIG_IGN); signal(SIGUSR2, SIG_IGN);
Perilaku sinyal sangat rumit untuk dinalar, bahkan dalam program yang ditulis dengan baik, dan penggunaannya menghadirkan risiko yang tidak perlu dalam aplikasi yang memiliki alternatif lain. Secara umum, jangan gunakan sinyal untuk berkomunikasi dengan bagian ruang pengguna dari program Anda. Sebagai gantinya, mintalah program secara transparan menangani peristiwa itu sendiri (contoh: dengan inotify), atau gunakan komunikasi ruang pengguna yang dapat melaporkan kembali kesalahan ke penerbit dan dapat dihitung serta ditunjukkan pada waktu kompilasi, seperti Thrift, gRPC, atau sejenisnya.
Saya harap artikel ini telah menunjukkan kepada Anda bahwa sinyal, meskipun mungkin tampak sederhana, pada kenyataannya tidak sederhana. Estetika kesederhanaan yang mempromosikan penggunaannya sebagai API untuk perangkat lunak ruang pengguna memungkiri serangkaian keputusan desain implisit yang tidak sesuai dengan sebagian besar kasus penggunaan produksi di era modern.
Mari kita perjelas: ada kasus penggunaan yang valid untuk sinyal. Sinyal itu oke untuk komunikasi dasar dengan kernel tentang status proses yang diinginkan ketika tidak ada komponen ruang pengguna, contoh: bahwa suatu proses harus dimatikan. Namun, sangatlah sulit untuk langsung berhasil menulis kode yang benar untuk sinyal yang diperkirakan akan terjebak di ruang pengguna.
Sinyal mungkin tampak menarik karena standardisasinya, ketersediaannya yang luas, dan kurangnya dependensi, tetapi sinyal membawa banyak bahaya yang hanya akan meningkatkan kekhawatiran seiring pertumbuhan proyek Anda. Semoga artikel ini telah memberi Anda beberapa mitigasi dan strategi alternatif yang akan memungkinkan Anda untuk tetap mencapai tujuan Anda, tetapi dengan cara yang lebih aman, tidak terlalu rumit, dan lebih intuitif.
Untuk mempelajari selengkapnya tentang Meta Open Source, kunjungi situs sumber terbuka kami, silakan berlangganan saluran YouTube kami, atau ikuti kami di Twitter, dan Facebook, dan LinkedIn.