epoll di Linux memungkinkan satu thread menunggu perubahan readiness pada banyak file descriptor tanpa memindai seluruh descriptor pada setiap iterasi. Interface ini umum dipakai pada server jaringan, proxy, runtime, dan program lain yang mempertahankan banyak socket aktif.

Mode registrasinya berpengaruh langsung pada pola kerja event loop. Operasi level-triggered terus melaporkan descriptor selama kondisi terkait masih ready. Operasi edge-triggered melaporkan transisi readiness dan mengharuskan aplikasi mengonsumsi pekerjaan yang tersedia sampai descriptor akan memblokir.

Keduanya memakai state readiness kernel yang sama, tetapi kewajiban event loop berbeda. Loop yang memperlakukan descriptor edge-triggered seperti level-triggered dapat meninggalkan byte yang belum dibaca atau kapasitas write yang belum dipakai tanpa notifikasi baru untuk membawa descriptor itu kembali ke loop.

Readiness bukan completion

Event epoll menyatakan bahwa operasi dapat membuat progres pada kondisi yang dilaporkan. Event tersebut tidak menyatakan bahwa satu pesan aplikasi sudah tersedia penuh, sebuah write dapat menerima payload dengan ukuran bebas, atau peer pasti tetap terhubung setelah event dikirim.

Pada socket TCP yang dilaporkan readable, recv() dapat mengembalikan sebagian byte, mencapai akhir stream, gagal, atau pada akhirnya menghasilkan EAGAIN pada descriptor nonblocking. Notifikasi writable juga hanya menyatakan bahwa sebagian output dapat diterima tanpa blocking pada saat itu.

Pemisahan ini membuat logika event loop tetap mengikuti hasil system call. Notifikasi memilih descriptor yang layak dilayani; operasi read atau write berikutnya menentukan seberapa jauh progres dapat dilakukan.

Mode level-triggered mempertahankan kondisi ready agar tetap terlihat

Perilaku level-triggered menjadi default ketika registrasi tidak menyertakan EPOLLET. Jika socket masih memiliki data yang belum dibaca setelah handler selesai, pemanggilan epoll_wait() berikutnya dapat melaporkan socket itu lagi karena kondisi readable masih ada.

Visibilitas berulang ini membuat konsumsi parsial lebih toleran. Handler dapat membaca data dengan batas tertentu, mengembalikan kontrol ke loop, lalu sisa readiness dapat menghasilkan event berikutnya.

Pola serupa berlaku pada descriptor writable. Jika kapasitas output masih tersedia dan descriptor terus didaftarkan untuk EPOLLOUT, wakeup berulang dapat terjadi. Pada server sibuk, memantau writable readiness secara permanen dapat menghasilkan aktivitas loop yang tidak perlu karena socket sering berada dalam kondisi writable.

Desain yang umum hanya menambahkan write interest ketika antrean output berisi data yang belum dapat dikirim seluruhnya. Setelah antrean kosong, loop menghapus write interest sampai aplikasi menghasilkan output baru.

Mode edge-triggered melaporkan transisi readiness

Menambahkan EPOLLET mengubah semantik notifikasi. Loop diberi event ketika readiness berubah ke kondisi yang relevan, bukan menerima laporan berulang hanya karena kondisi tersebut masih true.

Bayangkan socket TCP nonblocking menerima 32 KiB. Handler edge-triggered hanya membaca 4 KiB lalu berhenti meski masih ada byte di receive queue. Descriptor dapat tetap readable, tetapi tidak harus terjadi transisi readiness baru. Sisa byte dapat tertahan sementara event loop menunggu event yang belum tentu muncul.

Pola yang kokoh adalah terus membaca sampai recv() mengembalikan -1 dengan errno bernilai EAGAIN atau EWOULDBLOCK. Pada titik itu, receive queue sudah dikonsumsi sejauh yang saat ini diizinkan interface nonblocking.

Prinsip yang sama berlaku pada write. Saat socket menjadi writable, handler dapat menguras output yang mengantre sampai antrean kosong atau send() menyatakan progres akan memblokir. Sisa yang belum terkirim tetap berada di antrean aplikasi dan write interest tetap aktif.

Descriptor nonblocking membuat proses pengurasan aman

Kode edge-triggered biasanya dipasangkan dengan O_NONBLOCK. Menguras data menggunakan operasi blocking akan merusak sifat event loop: satu descriptor dapat menahan thread sementara descriptor ready lain menunggu.

Dengan I/O nonblocking, EAGAIN menjadi batas yang berguna. Nilai ini menyatakan readiness saat ini sudah dikonsumsi tanpa membuat thread tidur.

Jalur read sederhana dapat berbentuk seperti 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 == EAGAIN || errno == EWOULDBLOCK)
        break;

    if (errno == EINTR)
        continue;

    close_connection(fd);
    break;
}

Kode produksi juga perlu mengoordinasikan state parser, masa hidup koneksi, backpressure, dan error, tetapi batas drain-until-EAGAIN tetap menjadi bagian utama pelayanan edge-triggered.

Menguras readiness tidak berarti pekerjaan aplikasi tanpa batas

Mengonsumsi readiness kernel dan menjalankan pekerjaan aplikasi merupakan dua persoalan scheduling yang berbeda. Satu koneksi dapat mengirim cukup banyak data untuk memenuhi loop dalam waktu lama jika parsing, decompression, routing, atau business logic dijalankan inline untuk setiap byte yang diterima.

Event loop dapat menguras socket ke buffer user-space yang dibatasi, kemudian menjadwalkan pemrosesan yang lebih berat di tempat lain atau menerapkan budget per koneksi. Jika batas memori mencegah intake tambahan, program dapat sengaja berhenti membaca dan mengelola interest sesuai rancangan backpressure.

Kondisi ini berbeda dari berhenti tanpa sengaja saat descriptor masih ready. Suspensi yang disengaja memerlukan state eksplisit agar aplikasi dapat melanjutkan koneksi ketika kapasitas kembali tersedia.

Fairness berasal dari kebijakan event loop, bukan dari membiarkan descriptor edge-triggered hanya terkuras sebagian dengan asumsi notifikasi lain akan memperbaiki keputusan scheduling tersebut.

Registrasi one-shot menambahkan langkah rearm eksplisit

EPOLLONESHOT menonaktifkan descriptor yang terdaftar setelah sebuah event dikirim. Aplikasi harus melakukan rearm melalui epoll_ctl(..., EPOLL_CTL_MOD, ...) sebelum event berikutnya dapat dilaporkan.

Mode ini berguna ketika beberapa worker thread berbagi satu instance epoll dan program ingin satu worker memiliki koneksi selama proses pelayanan. Worker dapat menangani readiness saat ini, memperbarui state koneksi, lalu melakukan rearm setelah state konsisten.

Perilaku one-shot terpisah dari edge triggering. Keduanya dapat dipakai bersama, tetapi EPOLLONESHOT tidak menghapus kewajiban menguras readiness edge-triggered dengan benar. Rearm juga memerlukan urutan yang cermat agar perubahan state antara pelayanan dan rearm tidak hilang.

Hangup dan error tetap memerlukan penanganan I/O normal

Event mask dapat menyertakan EPOLLERR, EPOLLHUP, atau EPOLLRDHUP bersama readiness biasa. Flag tersebut tidak seharusnya membuat handler langsung membuang data yang sudah berada di antrean koneksi.

Peer dapat menutup sisi pengirimannya setelah mengirim byte terakhir. Socket lokal dapat memiliki data readable sekaligus indikasi shutdown. Membaca sampai stream mencapai hasil terminal mempertahankan byte terakhir tersebut.

Hasil system call tetap menjadi sumber state konkret. Event bit mengarahkan dispatch, sedangkan recv(), send(), dan operasi terkait memperlihatkan kondisi yang benar-benar ditemui saat pelayanan.

Perubahan registrasi memerlukan disiplin state koneksi

Event loop nyata mengubah registrasi saat state koneksi berubah. Socket dapat dimulai dengan read interest, menambahkan write interest ketika antrean output bertambah, menghapusnya setelah flush, lalu keluar dari epoll set ketika koneksi ditutup.

Transisi tersebut harus selaras dengan ownership aplikasi. Menutup descriptor saat task lain masih menyimpan nilai numeriknya berisiko karena kernel dapat memakai ulang nomor descriptor itu untuk resource lain. Implementasi event loop biasanya mengaitkan object koneksi yang stabil atau generation state dengan registrasi, bukan menganggap integer descriptor sebagai identitas permanen.

Desain multithread juga memerlukan aturan jelas mengenai worker yang boleh memanggil epoll_ctl, mengubah antrean, menutup socket, dan melakukan rearm pada registrasi one-shot. Interface kernel menyediakan notifikasi readiness; sinkronisasi state koneksi tetap menjadi tanggung jawab aplikasi.

Mode registrasi menentukan kontrak pelayanan

epoll level-triggered berulang kali menampilkan kondisi yang masih ready sehingga handler dapat membuat progres parsial dalam beberapa iterasi loop. epoll edge-triggered mengurangi notifikasi berulang untuk readiness yang tidak berubah, tetapi handler harus mendorong I/O nonblocking sampai batas saat ini.

Tidak ada satu mode yang otomatis membuat server lebih cepat. Performa bergantung pada workload, pola wakeup, biaya handler, batching, perilaku memori, lock contention, dan arsitektur di sekitarnya.

Perbedaan praktisnya berada pada kontrak pelayanan. Dengan registrasi level-triggered, readiness yang bertahan tetap terlihat. Dengan registrasi edge-triggered, handler harus menangani pekerjaan yang terkait dengan transisi dan berhenti pada batas nonblocking yang disengaja. Event loop tetap andal ketika kontrak tersebut tercermin langsung pada kode read, write, backpressure, dan lifecycle koneksi.