Dengan EPOLLET, sebuah file descriptor dapat tetap memiliki data yang belum dibaca setelah event readiness-nya sudah dikonsumsi. Pemanggilan epoll_wait() berikutnya tidak wajib melaporkan descriptor itu lagi hanya karena state readable sebelumnya masih bertahan.
Perilaku ini merupakan batas utama edge-triggered epoll: notifikasi mengikuti perubahan readiness, sedangkan objek I/O di bawahnya mempertahankan state sendiri secara terpisah.
Readiness dan pengiriman event merupakan state terpisah
Sebuah epoll instance memiliki interest list dan ready list. Registrasi melalui epoll_ctl() menentukan open file description yang dipantau dan kelas event yang relevan. epoll_wait() mengembalikan entry yang sudah mencapai ready list.
Pada mode level-triggered yang menjadi default, descriptor yang tetap readable dapat terus dilaporkan. Kondisi readiness itu sendiri cukup untuk terlihat lagi pada wait berikutnya.
EPOLLET mengubah model notifikasi tersebut. Event terkait dengan perubahan state yang dipantau, bukan dengan setiap pengamatan atas state yang tetap benar. Ketika input membuat stream menjadi readable, epoll dapat melaporkan transisi itu. Mengonsumsi hanya sebagian input tidak selalu menghasilkan transisi baru.
Perbedaan ini terlihat ketika pekerjaan aplikasi dan state buffer kernel tidak bergerak bersamaan.
Partial read dapat menyisakan data tanpa edge baru
Ambil sebuah pipe nonblocking yang terdaftar dengan EPOLLIN | EPOLLET. Writer menaruh 2048 byte ke pipe. Pipe berubah dari tidak memiliki data readable menjadi memiliki data readable, lalu epoll_wait() melaporkan read end.
Jika consumer hanya membaca 1024 byte, 1024 byte lain masih berada di buffer. Descriptor tetap readable; state-nya belum kembali menjadi non-readable.
Wait berikutnya karena itu dapat memblokir walaupun data masih tersedia. Tidak ada keharusan muncul transisi readiness baru di antara kedua wait tersebut.
Batas yang sama berlaku pada stream socket. Satu notifikasi readiness dapat mewakili jumlah data stream yang bebas ukurannya. Jumlah event bukan jumlah byte, jumlah message, atau jumlah record aplikasi.
EAGAIN menandai batas saat progres langsung habis
Dokumentasi Linux merekomendasikan descriptor nonblocking untuk edge-triggered epoll dan memperlakukan descriptor yang dilaporkan sebagai ready sampai operasi I/O mengembalikan EAGAIN.
Untuk read, pola tersebut menguras input yang saat itu tersedia tanpa risiko blocking setelah data yang tersedia sudah habis:
for (;;) {
ssize_t n = read(fd, buf, sizeof buf);
if (n > 0) {
consume(buf, (size_t)n);
continue;
}
if (n == 0) {
peer_closed(fd);
break;
}
if (errno == EAGAIN || errno == EWOULDBLOCK)
break;
handle_read_error(fd, errno);
break;
}EAGAIN memiliki peran yang spesifik. Hasil itu menetapkan bahwa operasi nonblocking saat ini tidak dapat membuat progres tambahan. Setelah titik tersebut, perubahan berikutnya yang menyediakan input baru dapat menghasilkan edge baru.
Batas ini lebih kuat daripada asumsi bahwa satu read() per event sudah cukup. Asumsi tersebut mengikat jumlah dispatch aplikasi pada model notifikasi kernel yang tidak memberikan pemetaan semacam itu.
Mode nonblocking menjaga batas event loop
Menguras data sampai progres tidak mungkin lagi berbahaya jika descriptor bersifat blocking. Setelah consumer mengambil semua input yang tersedia, read() berikutnya dapat tidur menunggu data masa depan dan menahan thread yang bertanggung jawab atas descriptor ready lainnya.
O_NONBLOCK mengubah operasi terakhir itu menjadi hasil EAGAIN. Kontrol kembali ke event loop tepat ketika progres I/O langsung berakhir.
Kebutuhan nonblocking karena itu terkait dengan scheduling sekaligus correctness. Dispatch edge-triggered sering menjalankan beberapa operasi I/O setelah satu notifikasi, sehingga setiap operasi perlu memiliki jalur kembali yang terbatas menuju dispatcher ketika objek tidak memiliki pekerjaan yang dapat segera diproses.
Pengosongan buffer dapat berbenturan dengan fairness
Menguras descriptor yang sibuk sampai EAGAIN dapat memakai waktu CPU yang besar jika input terus tiba cukup cepat sehingga progres langsung selalu tersedia. Penanganan edge yang benar tidak berarti satu descriptor harus menguasai satu iterasi event loop.
Sebuah reactor dapat memisahkan pencatatan readiness dari batas kerja per giliran. Reactor dapat mencatat bahwa descriptor masih memiliki pekerjaan, memproses jumlah terbatas, berpindah ke descriptor ready lain, lalu kembali ke state ready yang disimpan tanpa bergantung pada edge kernel baru.
Pemisahan ini menghasilkan dua tanggung jawab: epoll mendeteksi transisi readiness, sedangkan scheduler aplikasi menentukan berapa banyak pekerjaan yang diterima satu sumber ready sebelum memberi giliran kepada sumber lain.
Menghapus state ready lokal setelah berhenti lebih awal dapat menghasilkan stall yang sama seperti partial read yang langsung diikuti epoll_wait().
EPOLLONESHOT menambahkan batas rearm eksplisit
EPOLLONESHOT terpisah dari EPOLLET. Ketika one-shot mode aktif, epoll menonaktifkan descriptor terdaftar setelah mengirim satu event. Event berikutnya tidak dilaporkan sampai aplikasi melakukan rearm pada entry tersebut melalui EPOLL_CTL_MOD.
Menggabungkan kedua flag menghasilkan dua batas. Edge-triggering mengatur perubahan readiness mana yang menghasilkan notifikasi; one-shot mode mengatur apakah registrasi tetap aktif setelah event dikirim.
Kombinasi ini cocok pada desain yang memindahkan kepemilikan connection sementara ke satu worker. Worker dapat memproses descriptor, menetapkan interest mask berikutnya, lalu melakukan rearm ketika state siap untuk dispatch berikutnya.
Rearm bukan pengganti konsumsi atau pencatatan readiness yang sudah ada. Aplikasi tetap memerlukan transisi state yang konsisten antara progres I/O, bookkeeping lokal, dan notifikasi epoll berikutnya.
Hangup tidak menghapus data stream yang sudah berada di buffer
EPOLLHUP dan sinyal peer shutdown juga perlu dibaca bersama state stream. Peer yang menutup ujung connection tidak berarti byte yang sebelumnya sudah masuk antrean ikut hilang.
Pada stream atau pipe, data yang tersisa dapat tetap readable sebelum read() akhirnya mengembalikan nol sebagai end-of-file. Menganggap notifikasi hangup sebagai alasan untuk langsung membuang descriptor karena itu dapat menghilangkan input yang masih berada di buffer.
Kode edge-triggered perlu mempertahankan dua dimensi sekaligus: state notifikasi dari epoll dan state yang dapat dikonsumsi dari file description di bawahnya.
EPOLLET mengurangi notifikasi berulang untuk state readiness yang tetap benar. Flag ini tidak mengubah readiness menjadi satu event yang dapat dikonsumsi, dan tidak membuat satu callback setara dengan satu unit I/O. Batas yang stabil adalah progres I/O itu sendiri: proses atau simpan pekerjaan ready sampai operasi nonblocking menetapkan bahwa progres langsung telah berakhir.