Descriptor yang didaftarkan dengan EPOLLET dapat tetap readable setelah sebuah event dikirim tanpa muncul lagi pada epoll_wait() berikutnya. Kernel melaporkan transisi readiness; kernel tidak menjanjikan pengulangan notifikasi yang sama hanya karena masih ada data yang belum dibaca. Perbedaan ini menjadikan edge-triggered epoll sebagai kontrak transisi state antara kernel dan event loop.

Konsekuensinya bersifat struktural. Handler tidak dapat memperlakukan satu event sebagai izin untuk satu read() lalu langsung kembali ke wait loop. Pada edge-triggered monitoring, handler harus memperhitungkan seluruh state I/O yang segera tersedia sebelum bergantung pada transisi berikutnya.

Ready list bukan buffer descriptor

Sebuah instance epoll memiliki interest list dan ready list. Interest list mencatat descriptor yang dipantau beserta event yang diminta. Ready list berisi referensi descriptor yang state I/O-nya membuat descriptor tersebut siap dikirim sebagai event.

Kedua list tersebut bukan socket receive queue, pipe buffer, atau device buffer. Mengonsumsi event epoll tidak mengonsumsi byte dari descriptor yang mendasarinya. Sebaliknya, membaca hanya sebagian byte tidak mengharuskan kernel membuat edge baru untuk state yang tidak pernah kembali menjadi not-ready.

Misalkan pipe berubah dari kosong menjadi berisi 2048 byte. Transisi itu dapat menempatkan read descriptor pada ready list epoll. Jika epoll_wait() melaporkannya dan handler hanya membaca 1024 byte, pipe dapat tetap readable. Wait berikutnya tidak memiliki kewajiban umum untuk melaporkan readiness yang sama lagi pada EPOLLET, karena state terkait tidak perlu melintasi batas not-ready ke ready untuk kedua kalinya.

I/O nonblocking menetapkan batas exhaustion yang aman

Handler edge-triggered umumnya dipasangkan dengan O_NONBLOCK. Handler menjalankan operasi terkait berulang kali sampai mencapai hasil yang menyatakan kesempatan I/O saat ini telah habis.

Untuk read, batas tersebut biasanya berupa EAGAIN atau EWOULDBLOCK:

for (;;) {
    ssize_t n = read(fd, buf, sizeof buf);

    if (n > 0) {
        consume(buf, (size_t)n);
        continue;
    }
    if (n == 0) {
        close_peer(fd);
        break;
    }
    if (errno == EAGAIN || errno == EWOULDBLOCK) {
        break;
    }
    if (errno == EINTR) {
        continue;
    }

    fail_peer(fd, errno);
    break;
}

Flag nonblocking penting karena probe terakhir harus dapat menyatakan bahwa tidak ada operasi yang bisa segera dilanjutkan. Descriptor blocking justru dapat menahan thread event loop setelah input yang tersedia habis, sehingga thread tersebut tidak dapat melayani descriptor ready lain.

Loop tersebut bukan kewajiban untuk menjalankan kerja aplikasi tanpa batas. Fungsinya adalah menyelaraskan state readiness kernel dengan bookkeeping di user space. Implementasi dapat menetapkan processing budget, tetapi kemudian harus menyimpan ready state sendiri agar descriptor yang baru dilayani sebagian tetap terjadwal walaupun tidak ada edge kernel baru.

Readiness lebih luas daripada transfer payload yang berhasil

EPOLLIN menunjukkan bahwa file terkait tersedia untuk operasi read; flag ini bukan janji bahwa pemanggilan berikutnya menghasilkan payload byte positif. Stream shutdown, error, dan semantik khusus descriptor memengaruhi hasil aktual.

Karena itu, event loop perlu memproses return value dan kondisi terminal, bukan memperlakukan event mask sebagai state machine yang lengkap. EPOLLRDHUP dapat membuat deteksi peer half-close eksplisit untuk stream socket, sedangkan EPOLLERR dan EPOLLHUP dapat dilaporkan terlepas dari mask biasa yang diminta.

Event readiness mempersempit kemungkinan hasil blocking. Operasi I/O tetap menjadi sumber otoritatif untuk jumlah byte yang ditransfer, end-of-file, dan error.

Write readiness memiliki persoalan transisi yang sama

Kontrak yang sama berlaku untuk output. Stream socket nonblocking dapat berubah dari tidak memiliki kapasitas send buffer menjadi dapat menerima lebih banyak data, lalu menghasilkan edge EPOLLOUT. Jika handler hanya menulis sebagian data aplikasi yang antre dan membiarkan descriptor tetap writable, handler tidak dapat mengasumsikan edge identik lain akan datang hanya untuk mengingatkan sisa antrean.

Jalur output yang kuat karena itu menggabungkan readiness kernel dengan queue di sisi aplikasi. Selama descriptor menerima data, byte dalam antrean dapat ditulis. Setelah write() menghasilkan EAGAIN, event loop telah mencapai batas kernel saat ini dan dapat menunggu transisi writable berikutnya.

Pemisahan ini juga membedakan dua bentuk backpressure. Queue aplikasi mencatat data yang belum diterima kernel, sedangkan EAGAIN mencatat ketidakmampuan descriptor saat ini untuk menerima lebih banyak data tanpa blocking. Mencampurkan keduanya dapat menghasilkan busy loop atau antrean yang berhenti bergerak.

EPOLLONESHOT menambahkan batas rearm eksplisit

EPOLLONESHOT mengubah lifecycle sekali lagi. Setelah event dilaporkan untuk descriptor yang terdaftar dengan flag tersebut, epoll menonaktifkan pengiriman event lanjutan untuk registrasi itu sampai user space melakukan rearm melalui EPOLL_CTL_MOD.

Sifat ini berbeda dari edge-triggering. EPOLLET mengubah saat notifikasi readiness dihasilkan. EPOLLONESHOT mengubah apakah registrasi tetap aktif setelah event dikirim. Keduanya sering digabungkan pada dispatcher multithread karena one-shot delivery memberi user space titik eksplisit untuk memindahkan ownership descriptor ke worker lalu mengembalikannya ke polling set.

Operasi rearm harus terjadi setelah handler menetapkan state yang ingin diamati oleh wait berikutnya. Rearm terlalu dini dapat membuka concurrent handling yang seharusnya dicegah oleh ownership protocol; tidak melakukan rearm membuat descriptor tetap diam walaupun state I/O-nya kemudian berubah.

Fairness memerlukan scheduling state di user space

Menguras descriptor sibuk sampai EAGAIN dapat mendominasi satu iterasi event loop jika producer terus memasok kerja. Interface epoll tidak menetapkan fairness policy aplikasi.

Server dapat membatasi byte, message, atau operasi yang diproses per giliran. Pada semantik edge-triggered, berhenti sebelum exhaustion berarti server harus mengingat descriptor tersebut sebagai runnable di queue miliknya. Descriptor tidak dapat begitu saja dilupakan sampai epoll_wait() mengulang event.

Hal ini memisahkan tanggung jawab secara jelas: epoll melaporkan transisi readiness di sisi kernel, sedangkan event loop mengatur scheduling di antara work item yang sudah diketahui runnable. Local ready queue dapat menjaga fairness tanpa meminta kernel membuat ulang notifikasi untuk state yang tidak berubah.

Edge-triggered epoll memindahkan pelacakan state ke event loop

Level-triggered epoll dapat berulang kali melaporkan descriptor selama kondisi yang diminta tetap benar. Edge-triggered epoll menghapus pengingat berulang tersebut dan menjadikan transisi sebagai batas notifikasi utama.

Pengurangan pengulangan ini membawa invariant user space yang lebih kuat: setelah menerima edge, event loop harus membawa descriptor ke batas not-ready yang diketahui seperti EAGAIN, mencapai kondisi terminal, atau menyimpan runnable state eksplisit untuk pemrosesan berikutnya. Kernel melacak transisi readiness; aplikasi melacak kerja yang belum selesai. Correctness bergantung pada keselarasan kedua state machine tersebut.