Dengan EPOLLET, event loop dapat menerima satu notifikasi, membaca hanya sebagian data yang tersedia, lalu menunggu tanpa batas meskipun masih ada byte yang belum dibaca di buffer socket. Descriptor tetap ready, tetapi tidak ada transisi readiness baru yang memicu edge berikutnya.
Perilaku tersebut membuat epoll edge-triggered menjadi kontrak antara semantik notifikasi dan I/O nonblocking. Event menyatakan bahwa readiness berubah; event bukan janji bahwa kernel akan terus mengulang notifikasi yang sama sampai aplikasi menyelesaikan pekerjaannya.
Edge merepresentasikan transisi state
Operasi level-triggered melaporkan descriptor selama kondisi yang dipantau masih benar. Jika socket masih memiliki data yang dapat dibaca, pemanggilan epoll_wait() berikutnya dapat melaporkannya lagi. Operasi edge-triggered mengubah hubungan tersebut. Dengan EPOLLET, aplikasi mendapat notifikasi ketika state yang dipantau berubah ke arah yang relevan.
Misalkan socket stream nonblocking menerima 8 KiB. Handler membaca 1 KiB lalu kembali ke event loop. Sisa 7 KiB dapat membuat socket terus berada dalam state readable. Karena descriptor tidak sempat menjadi non-readable lalu readable lagi, mengandalkan edge baru untuk byte yang tersisa bukan kontrak yang aman.
Handler perlu mengonsumsi state readiness saat ini sampai operasi menyatakan bahwa progres langsung tidak lagi dapat dilakukan.
EAGAIN menandai batas drain
Pada socket nonblocking, read() atau recv() mengembalikan -1 dengan errno bernilai EAGAIN atau EWOULDBLOCK ketika tidak ada data yang dapat dibaca tanpa blocking. Dalam loop edge-triggered, hasil tersebut merupakan batas normal antara menguras readiness saat ini dan kembali ke epoll_wait().
Loop pembacaan ringkas memiliki bentuk berikut:
for (;;) {
ssize_t n = recv(fd, buf, sizeof buf, 0);
if (n > 0) {
consume(buf, (size_t)n);
continue;
}
if (n == 0) {
close_connection(fd);
break;
}
if (errno == EINTR)
continue;
if (errno == EAGAIN || errno == EWOULDBLOCK)
break;
fail_connection(fd, errno);
break;
}Loop ini tidak mengharuskan satu recv() untuk setiap event. Event diperlakukan sebagai izin untuk mencoba progres, lalu konsumsi berlanjut sampai kernel menyatakan bahwa descriptor saat ini tidak memiliki data yang langsung dapat dibaca.
EINTR memiliki semantik berbeda. Nilai tersebut menunjukkan interupsi oleh signal sebelum operasi selesai sesuai permintaan, sehingga mencoba kembali operasi berbeda dari menunggu edge readiness baru.
Mode nonblocking merupakan bagian dari kontrak
Menguras hingga habis aman bagi thread event loop hanya ketika file descriptor menggunakan I/O nonblocking. Descriptor blocking dapat menghentikan seluruh loop jika pembacaan berikutnya menunggu data setelah byte yang tersedia saat ini habis dikonsumsi.
Untuk socket, aplikasi umumnya menetapkan O_NONBLOCK melalui fcntl() atau memperoleh descriptor nonblocking melalui interface yang mendukung flag saat pembuatan. EPOLLET mengubah perilaku notifikasi; flag tersebut tidak mengubah mode blocking descriptor dengan sendirinya.
Kedua pengaturan menangani bagian mekanisme yang berbeda. EPOLLET membatasi pelaporan readiness berulang, sedangkan O_NONBLOCK memungkinkan handler melakukan probe sampai progres memerlukan penantian.
Partial write membentuk persoalan state yang sama
Writable readiness juga memerlukan state eksplisit. send() nonblocking dapat menerima byte lebih sedikit daripada jumlah yang diminta, lalu pada akhirnya dapat mengembalikan EAGAIN. Aplikasi harus menyimpan suffix yang belum terkirim dan melanjutkan dari offset tersebut setelah writable readiness tersedia lagi.
Koneksi dengan output queue 32 KiB tidak dapat memperlakukan satu event EPOLLOUT sebagai jaminan bahwa seluruh 32 KiB muat ke buffer pengiriman socket. Invariant yang relevan lebih sempit: selama penulisan menghasilkan progres, majukan queue; ketika EAGAIN muncul, pertahankan byte yang tersisa dan tunggu transisi writable berikutnya.
Membiarkan EPOLLOUT aktif secara permanen juga dapat menghasilkan wakeup yang tidak diperlukan pada desain dengan socket yang biasanya writable. Banyak event loop mengaktifkan interest writable hanya selama output tertunda masih ada, lalu menghapus interest tersebut setelah queue terkuras. Strategi registrasi persisnya bergantung pada aplikasi, tetapi kepemilikan byte yang tertunda harus tetap eksplisit.
Readiness tidak mereservasi data untuk satu worker
Event readiness bukan reservasi. Di antara notifikasi dan system call berikutnya, thread atau proses lain yang berbagi akses ke objek dasar yang sama dapat mengonsumsi data atau mengubah state. I/O nonblocking mencegah race tersebut berubah menjadi penantian blocking yang tidak disengaja.
Perbedaan ini juga relevan ketika beberapa thread menunggu pada instance epoll yang sama atau ketika descriptor diduplikasi. Pengiriman event dan state objek saling terkait, tetapi event tidak memindahkan kepemilikan eksklusif atas byte yang ready.
Karena itu, handler perlu memperlakukan operasi I/O sebagai sumber state yang otoritatif. Jumlah byte yang dikembalikan, EOF, EAGAIN, atau error lain menggambarkan state pada batas system call secara lebih presisi dibanding notifikasi readiness yang diterima sebelumnya.
Registrasi one-shot menambahkan batas rearm terpisah
EPOLLONESHOT dapat digabungkan dengan operasi edge-triggered. Setelah event dikirim untuk descriptor yang diregistrasikan dengan EPOLLONESHOT, descriptor tersebut dinonaktifkan dari interest list sampai aplikasi melakukan rearm melalui epoll_ctl() dengan EPOLL_CTL_MOD.
Mekanisme ini berguna ketika koneksi diserahkan kepada worker dan penanganan konkuren terhadap registrasi yang sama perlu dibatasi. Mekanisme tersebut juga menambahkan transisi state lain: menguras I/O saja tidak cukup; worker harus melakukan rearm ketika koneksi siap menerima notifikasi berikutnya.
Rearm terlalu dini dapat memungkinkan worker lain melihat aktivitas ketika worker saat ini masih memiliki mutable connection state. Tidak melakukan rearm akan menekan notifikasi berikutnya sepenuhnya. Protokol kepemilikan di sekitar koneksi karena itu sama pentingnya dengan flag registrasi kernel.
Loop edge-triggered bergantung pada state progres eksplisit
epoll edge-triggered mengurangi notifikasi berulang untuk kondisi yang tetap ready, tetapi memindahkan lebih banyak tanggung jawab state ke aplikasi. Read handler memerlukan batas drain, write handler memerlukan offset atau queue yang dipertahankan, dan desain one-shot memerlukan titik rearm yang terdefinisi.
Kernel melaporkan transisi readiness; aplikasi mencatat pekerjaan yang belum selesai. Pemisahan peran tersebut mencegah failure mode ketika byte masih tersedia tetapi tidak ada edge berikutnya yang dijamin akan membawa handler kembali.