Consumer epoll edge-triggered dapat terblokir ketika data yang belum dibaca masih tersimpan di buffer. Kegagalan ini muncul ketika sebuah event dikonsumsi, hanya sebagian input yang tersedia dibaca, lalu event loop kembali ke epoll_wait() dengan harapan menerima notifikasi lain untuk byte yang tersisa.
Perilaku ini mengikuti langsung semantik EPOLLET. Notifikasi edge-triggered melaporkan perubahan readiness, bukan terus-menerus melaporkan kondisi ready. Setelah sebuah transisi readiness menghasilkan event, membiarkan file descriptor tetap ready tidak dengan sendirinya menciptakan transisi baru.
Partial read dapat menghabiskan notifikasi tetapi tidak datanya
Pertimbangkan pipe nonblocking yang didaftarkan dengan EPOLLIN | EPOLLET. Writer menempatkan 2048 byte ke dalam pipe. epoll_wait() melaporkan read end sebagai ready, lalu consumer hanya membaca 1024 byte.
Pada titik itu, dua fakta berlaku sekaligus:
- 1024 byte masih dapat dibaca.
- readiness edge yang terkait dengan write tersebut sudah dilaporkan.
Pemanggilan epoll_wait() berikutnya karena itu dapat tertidur meskipun pipe masih berisi data. Tidak ada kontradiksi: descriptor memang ready, tetapi mode edge-triggered bukan pernyataan readiness yang terus berlaku.
Batas yang sama penting untuk stream socket. Receive buffer dapat menyimpan lebih banyak byte daripada yang dikonsumsi satu operasi tingkat aplikasi. Menganggap satu event EPOLLIN sebagai izin untuk tepat satu pemanggilan read() dapat membuat input yang sudah ada di buffer tidak pernah diproses.
EAGAIN menandai batas edge yang berguna
Dengan descriptor nonblocking, read berulang pada akhirnya mencapai salah satu dari beberapa keadaan. Nilai balik positif berarti sejumlah byte telah dikonsumsi. Nilai nol pada stream menunjukkan end of file. Nilai negatif dengan EAGAIN atau EWOULDBLOCK berarti tidak ada input lain yang dapat dikonsumsi saat itu.
Untuk event loop edge-triggered, keadaan terakhir ini penting. Mencapai EAGAIN memastikan bahwa kondisi readable saat ini telah di-drain. Data yang datang kemudian dapat mengubah descriptor dari not-ready menjadi ready dan menciptakan edge baru.
Receive path yang ringkas berbentuk seperti ini:
for (;;) {
ssize_t n = read(fd, buf, sizeof buf);
if (n > 0) {
process_bytes(buf, (size_t)n);
continue;
}
if (n == 0) {
close_connection(fd);
break;
}
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break;
}
if (errno == EINTR) {
continue;
}
handle_read_error(fd, errno);
break;
}Loop tersebut bukan sekadar detail optimisasi. Ia mempertahankan model event: ketika event loop berhenti melayani descriptor yang readable, descriptor seharusnya telah mencapai keadaan ketika read langsung berikutnya tidak dapat dilanjutkan.
Mode nonblocking mencegah satu descriptor menguasai loop
Draining bukan berarti menjalankan blocking read sampai traffic jaringan berikutnya tiba. EPOLLET biasanya dipasangkan dengan file descriptor nonblocking agar operasi drain berhenti pada EAGAIN, bukan menangguhkan thread di dalam read().
Tanpa mode nonblocking, handler yang terus membaca dapat menghabiskan data yang tersedia saat ini lalu terblokir menunggu data berikutnya. Hal itu menghentikan pemrosesan semua descriptor lain yang ditangani thread event loop yang sama.
Kedua properti ini bekerja bersama:
edge-triggered notification -> process current readiness fully
nonblocking I/O -> stop when current readiness is exhaustedPasangan ini memisahkan pemrosesan readiness dari proses menunggu. Waiting tetap dilakukan di epoll_wait(); setiap pemanggilan I/O hanya mengonsumsi pekerjaan yang tersedia saat itu juga.
Write readiness memiliki batas state yang sama
Mekanisme yang sama berlaku untuk EPOLLOUT. Socket nonblocking dapat menerima sebagian output lalu mengembalikan EAGAIN ketika send buffer tidak lagi memiliki ruang yang dapat digunakan. Dengan notifikasi edge-triggered, aplikasi perlu menyimpan byte yang belum terkirim dan memperlakukan descriptor sebagai writable sampai write mencapai batas tersebut.
Partial write saja tidak berarti socket telah menjadi not-writable. Langsung kembali ke epoll_wait() setelah write pendek yang dipilih aplikasi dapat menyisakan kapasitas tambahan yang belum digunakan tanpa menghasilkan transisi readiness baru.
Karena itu, penanganan output membutuhkan pending state yang eksplisit. Ketika pending buffer kosong, interest pada EPOLLOUT mungkin tidak lagi berguna. Ketika write mencapai EAGAIN, transisi berikutnya kembali ke writable dapat menghasilkan event baru.
EPOLLONESHOT menambahkan kebutuhan rearm yang terpisah
EPOLLONESHOT mengubah bagian lain dari lifecycle. Setelah sebuah event dikirim untuk descriptor yang didaftarkan dengan flag ini, registration tersebut dinonaktifkan sampai epoll_ctl() dengan EPOLL_CTL_MOD melakukan rearm.
Draining dan rearming menyelesaikan masalah state yang berbeda. Draining mengonsumsi readiness yang direpresentasikan oleh edge. Rearming mengaktifkan kembali delivery setelah one-shot suppression. Handler yang menggunakan EPOLLET dan EPOLLONESHOT biasanya perlu menyelesaikan pekerjaan I/O nonblocking saat ini, memperbarui state aplikasi, lalu melakukan rearm pada descriptor ketika event berikutnya perlu diterima.
Perbedaan ini penting pada dispatcher multithreaded. One-shot registration dapat mencegah beberapa worker memproses descriptor yang sama secara bersamaan, sedangkan batas drain tetap menentukan apakah buffered I/O sudah dilayani sepenuhnya.
Fairness dapat membatasi pekerjaan tanpa membuang readiness state
Descriptor dengan traffic berkelanjutan dapat menghasilkan cukup banyak pekerjaan untuk menguasai satu iterasi event loop. Draining tanpa kebijakan scheduling karena itu dapat menimbulkan starvation meskipun logika readiness sudah benar.
Event loop yang dibatasi dapat mempertahankan ready queue di tingkat aplikasi. Jika handler mencapai batas byte, message, atau waktu sebelum mencapai EAGAIN, aplikasi mencatat bahwa descriptor masih memiliki pekerjaan pending dan menjadwalkannya kembali secara lokal, alih-alih mengandalkan kernel edge baru.
Pendekatan tersebut mempertahankan invarian penting: descriptor yang diketahui masih ready tidak dilupakan hanya karena handler melakukan yield demi fairness.
epoll edge-triggered dengan demikian merupakan protokol transisi antara readiness state kernel dan scheduling state aplikasi. EAGAIN adalah titik normal ketika aplikasi dapat berhenti membawa state readable atau writable secara lokal dan kembali mengandalkan transisi readiness berikutnya untuk menandai adanya pekerjaan baru.