Registrasi epoll edge-triggered dapat berhenti menghasilkan notifikasi sementara byte yang belum dibaca masih tersisa di socket atau pipe. Descriptor tetap dapat digunakan untuk I/O, tetapi event loop sudah mengonsumsi notifikasi yang terkait dengan transisi readiness. Jika handler hanya membaca sebagian data yang tersedia lalu kembali ke epoll_wait(), tidak ada keharusan muncul transisi baru, sehingga byte yang tertunda dapat dibiarkan tanpa diproses tanpa batas.
Perilaku ini membuat EPOLLET lebih dari sekadar preferensi notifikasi. Ia mengubah kontrak antara ready list kernel dan application state. Loop level-triggered dapat terus menerima descriptor selama kondisi I/O yang diminta tetap benar. Loop edge-triggered harus mempertahankan cukup local state untuk memperlakukan event yang diterima sebagai kewajiban menguras I/O nonblocking yang saat itu tersedia, biasanya sampai operasi mengembalikan EAGAIN.
Boundary-nya adalah readiness, bukan message completion, request completion, atau application-level progress.
Notifikasi bukan hitungan operasi yang tersedia
Sebuah instance epoll mempertahankan interest list dan ready list. Interest list mencatat target yang dimonitor beserta event mask. Ready list mencatat reference yang saat ini memiliki event untuk dikirimkan.
Dengan level-triggered behavior, descriptor yang tetap readable dapat terus dilaporkan pada beberapa wait. Ini menyerupai readiness semantics poll(): kondisi itu sendiri cukup untuk membuat descriptor tetap eligible untuk dilaporkan.
EPOLLET mengubah hubungan tersebut. Notifikasi dikaitkan dengan perubahan readiness, bukan pengulangan terus-menerus atas kondisi yang masih ready. Bayangkan pipe menerima 2048 byte. Event loop menerima EPOLLIN, membaca 1024 byte, lalu langsung menunggu lagi. Sisa 1024 byte tidak menjadi transisi baru hanya karena aplikasi membiarkannya.
State yang dihasilkan dapat digambarkan seperti ini:
empty
|
| writer menambah 2048 byte
v
readable ---- event dikirim
|
| reader mengonsumsi 1024 byte
v
still readable ---- tidak wajib ada edge baruKarena itu event bukan token yang mengizinkan satu kali read. Ia adalah bukti bahwa descriptor masuk atau mengalami state yang relevan dengan interest yang didaftarkan. Kode aplikasi yang memetakan satu event ke satu bounded I/O call dapat kehilangan forward progress walaupun peer maupun kernel tidak gagal.
I/O nonblocking menentukan batas pengurasan yang aman
Menguras descriptor edge-triggered membutuhkan stopping condition. Blocking I/O tidak cocok untuk peran tersebut karena handler dapat mengonsumsi semua input yang langsung tersedia lalu block menunggu data yang belum datang. Pada satu event-loop thread, ini dapat mencegah descriptor ready lain dilayani.
Mode nonblocking menyediakan boundary lokal yang jelas. Read dapat terus dilakukan selama data tersedia tanpa menunggu. Begitu tidak ada lagi data yang dapat dibaca tanpa blocking, read() gagal dengan EAGAIN atau EWOULDBLOCK. Pada titik itu, handler telah menguras current readable state sesuai kontrak interface dan dapat mengembalikan kontrol ke event loop.
Read path ringkas berbentuk seperti ini:
for (;;) {
ssize_t n = read(fd, buf, sizeof buf);
if (n > 0) {
consume(buf, (size_t)n);
continue;
}
if (n == 0) {
close_connection(fd);
break;
}
if (errno == EAGAIN || errno == EWOULDBLOCK)
break;
if (errno == EINTR)
continue;
fail_connection(fd);
break;
}Loop ini tidak berarti semua pekerjaan aplikasi harus berjalan tanpa batas. Ia hanya menetapkan readiness boundary dengan benar. Parsing, request execution, atau downstream processing dapat dijadwalkan terpisah setelah byte dipindahkan keluar dari jalur I/O yang berhadapan langsung dengan kernel.
Partial read memiliki arti berbeda untuk stream dan record
Aturan drain berinteraksi dengan jenis descriptor yang dipantau. Interface berorientasi stream mengekspos byte stream tanpa mempertahankan application message boundary. Short read dapat menunjukkan bahwa data stream yang saat itu tersedia sudah habis ketika requested count lebih besar daripada jumlah yang langsung tersedia, tunduk pada documented behavior interface spesifik.
Sumber berorientasi record memerlukan lebih banyak perhatian. Datagram socket mempertahankan message boundary, dan jenis descriptor lain dapat memiliki read semantics sendiri. Menganggap short transfer sebagai pengganti universal untuk EAGAIN dapat mengodekan asumsi yang tidak berlaku untuk semua monitored object.
Abstraksi yang kuat bukan “read sekali” atau “berhenti setelah short read”. Abstraksinya adalah “pastikan tidak ada lagi I/O nonblocking relevan yang dapat maju sekarang”. Untuk handler edge-triggered umum, mencapai EAGAIN membuat state tersebut eksplisit.
Perbedaan ini penting pada reusable event-loop code. Handler yang dirancang untuk stream socket dapat tanpa sengaja dipakai pada pipe, terminal, datagram socket, atau device descriptor dengan transfer semantics yang berbeda secara material. epoll melaporkan readiness; ia tidak menormalisasi I/O contract target descriptor.
Write readiness menciptakan kewajiban simetris
Mekanisme yang sama berlaku untuk output. Nonblocking socket dapat menerima sebagian byte, mencapai kapasitas, lalu mengembalikan EAGAIN. Event loop yang tertarik pada EPOLLOUT dapat melanjutkan write ketika socket menjadi writable lagi.
Dengan edge triggering, writable notification tidak seharusnya dianggap sebagai izin tepat untuk satu kali write attempt. Handler biasanya memajukan pending output sampai socket tidak dapat menerima data lagi tanpa blocking atau sampai aplikasi tidak lagi memiliki output.
Ini menciptakan dua completion condition berbeda:
application queue empty
kernel would blockYang pertama berarti aplikasi tidak punya lagi data untuk dikirim. Yang kedua berarti kernel-facing output path sementara sudah terkuras. Mencampurkan keduanya dapat menyebabkan EPOLLOUT interest yang tidak perlu atau output yang macet.
Karena itu, desain event loop yang umum melacak pending output secara eksplisit. Jika application queue kosong, write interest mungkin tidak lagi berguna. Jika write() mencapai EAGAIN sementara byte masih antre, write readiness tetap relevan karena capacity transition berikutnya dapat melanjutkan progress.
Readiness API tidak mengelola application queue. Ia hanya mengekspos apakah underlying descriptor saat ini dapat membuat progress untuk operasi yang diminta.
One-shot delivery menambahkan explicit rearm boundary
EPOLLONESHOT memperkenalkan state transition lain. Setelah event dilaporkan untuk descriptor yang didaftarkan dengan flag ini, registrasi tersebut dinonaktifkan untuk pengiriman event berikutnya sampai user space melakukan rearm dengan epoll_ctl() dan EPOLL_CTL_MOD.
Ini terpisah dari edge triggering. EPOLLET mengubah notification behavior di sekitar readiness transition. EPOLLONESHOT mengubah apakah registrasi tetap enabled setelah sebuah event dikirim.
Menggabungkan keduanya berguna ketika beberapa worker thread berbagi event source. Worker dapat menerima event, memiliki interval processing descriptor, menguras I/O nonblocking relevan, memperbarui connection state, lalu me-rearm registrasi. Operasi rearm menjadi explicit handoff boundary.
Boundary ini juga menciptakan failure mode. Jika worker keluar melalui error path tanpa me-rearm descriptor yang masih hidup, descriptor dapat tetap valid dan bahkan ready tetapi tidak menghasilkan event lagi melalui one-shot registration tersebut. Tidak adanya event kemudian mencerminkan interest yang disabled, bukan koneksi idle.
State machine yang memakai one-shot delivery perlu membuat state enabled, processing, closing, dan rearmed cukup eksplisit sehingga setiap jalur yang masih hidup memiliki transition yang jelas.
Identitas descriptor melampaui nilai integer
Sebuah epoll interest entry terkait dengan kombinasi nomor file descriptor dan underlying open file description. Ini penting karena integer descriptor dapat diduplikasi dan digunakan kembali.
Menutup satu descriptor tidak selalu menghapus underlying open file description dari interest list epoll jika descriptor lain masih merujuk ke open file description yang sama. Event dapat tetap dilaporkan sampai semua reference yang terkait dengan open file description tersebut ditutup, kecuali registrasi secara eksplisit dihapus lebih dulu.
Properti ini menciptakan boundary tajam untuk lifecycle code. Event payload yang hanya menyimpan nilai integer descriptor dapat menjadi ambigu jika process menutup descriptor tersebut lalu kernel menggunakan ulang integer yang sama untuk open file lain. Event lama yang sudah terkumpul dan resource baru kemudian dapat membawa integer yang sama sementara lifetime-nya berbeda.
Application sering memasang pointer, index, atau generation-bearing token melalui epoll_event.data daripada menganggap nomor descriptor sebagai identitas lengkap. Strategi yang tepat bergantung pada object ownership dan reclamation rule, tetapi invariant-nya lebih luas: event delivery dapat hidup lebih lama daripada asosiasi integer-to-object yang terlalu sederhana.
Batched event dapat berisi application reference yang sudah stale
epoll_wait() dapat mengembalikan beberapa event dalam satu call. Memproses satu event dapat menutup atau membuat invalid application state yang terkait dengan event lain yang sudah ada di batch hasil tersebut.
Kernel sudah menyalin event record ke array milik caller. Menghapus registrasi tidak secara retroaktif menghapus elemen lain dari user-space batch. Karena itu event-loop code membutuhkan cara mendeteksi bahwa object yang dirujuk batch entry berikutnya sudah retired selama pemrosesan entry sebelumnya.
Ini adalah masalah application lifetime, bukan kontradiksi readiness semantics. Event array merupakan snapshot delivery yang dipilih untuk wait call tersebut; application callback dapat mengubah dunia saat iterasi berlangsung.
Generation counter, stable connection object dengan deferred reclamation, atau explicit retired marker merupakan cara umum untuk mencegah batch iteration melakukan dereference state yang invalid. Mekanismenya dapat berbeda, tetapi resource lifetime harus tetap dipisahkan dari event-array lifetime.
Draining dapat bersaing dengan fairness
Loop ketat drain-until-EAGAIN dapat menghabiskan waktu besar pada satu descriptor yang sibuk. Jika input terus datang cukup cepat, descriptor ready lain dapat menunggu lebih lama daripada yang diinginkan aplikasi.
Tegangan ini tidak diselesaikan dengan meninggalkan kontrak edge-triggered readiness. Event loop dapat memisahkan kernel readiness dari scheduling policy. Setelah descriptor diketahui ready, user space dapat mencatatnya di application-level ready queue dan menerapkan bounded work budget di seluruh connection.
Handler berbasis budget yang berhenti sebelum mencapai EAGAIN tidak boleh sekadar melupakan descriptor lalu mengandalkan kernel edge lain. Ia masih memiliki pengetahuan bahwa readiness tertunda. Application-level queue harus mempertahankan fakta itu dan menjadwalkan descriptor lagi walaupun epoll tidak mengeluarkan event baru.
Ini menghasilkan model dua lapis yang berguna:
kernel readiness transition
|
v
application ready state
|
v
bounded processing slices
|
v
EAGAIN clears local ready stateKernel memasok transition. Scheduler memasok fairness. Mencampur kedua peran ini menyebabkan stall halus karena keputusan scheduling untuk berhenti memproses dapat keliru dianggap sebagai bukti bahwa readiness sudah berakhir.
Edge triggering memindahkan penyimpanan state ke user space
Level-triggered notification berulang kali mengekspos kondisi yang tetap benar. Edge-triggered notification mengurangi pengulangan tersebut tetapi mengharuskan aplikasi mengingat lebih banyak state.
Setelah edge dikirim, user space harus mempertahankan fakta bahwa pekerjaan mungkin masih tersisa sampai I/O nonblocking membuktikan exhaustion. Jika processing dibagi ke callback, worker thread, atau fairness slice, retained state tersebut menjadi bagian dari correctness model event loop.
Inilah trade-off utamanya. EPOLLET tidak membuat readiness menjadi lebih presisi dan tidak mengubah event menjadi message. Ia mengubah lokasi penyimpanan persistent knowledge tentang readiness. Kernel melaporkan transition; aplikasi meneruskan kewajibannya sampai jalur I/O relevan mencapai state yang would block.