Edge Triggering Linux epoll Melaporkan Transisi Readiness, Bukan Unit Kerja
Dengan EPOLLET, sebuah interest epoll tidak berperilaku seperti antrean yang berisi satu event untuk setiap byte, paket, koneksi, atau pesan aplikasi. Mekanisme ini melaporkan perubahan status readiness. Setelah sebuah file descriptor berada dalam status ready, pekerjaan tambahan dapat terakumulasi tanpa menghasilkan edge lain yang dapat diandalkan aplikasi. Handler karena itu perlu mengonsumsi pekerjaan yang tersedia sampai operasi nonblocking melaporkan bahwa progres berikutnya akan memblokir.
Batas ini memisahkan readiness kernel dari framing aplikasi. epoll_wait() dapat menunjukkan bahwa suatu operasi dapat dilakukan tanpa blocking, tetapi tidak menyatakan jumlah pekerjaan yang tersedia dan tidak memetakan notifikasi ke pesan protokol.
Level dan edge triggering memiliki kontrak observasi yang berbeda
Perilaku default epoll adalah level-triggered. Jika descriptor yang dipantau tetap ready, pemanggilan epoll_wait() berikutnya dapat melaporkannya lagi. Handler dapat mengonsumsi hanya sebagian input yang tersedia dan tetap menerima laporan readiness lain selama kondisi ready bertahan.
EPOLLET mengubah pola observasi tersebut. Kernel melaporkan perubahan readiness, bukan terus melaporkan kondisi yang tetap benar. Dokumentasi Linux karena itu merekomendasikan descriptor nonblocking untuk operasi edge-triggered dan pemrosesan sampai read() atau write() mengembalikan EAGAIN.
Pertimbangkan pipe nonblocking. Jika writer menaruh dua kilobyte ke pipe kosong, sisi baca berubah dari not ready menjadi ready. Reader yang diberi notifikasi melalui EPOLLET lalu hanya mengonsumsi satu kilobyte akan meninggalkan data yang belum dibaca. Memperlakukan satu notifikasi sebagai satu unit kerja dapat membuat byte sisanya tertahan karena descriptor dapat tetap ready tanpa transisi baru yang menghasilkan notifikasi lain.
Unit yang lebih tepat adalah satu episode readiness: notifikasi memulai pemrosesan, sedangkan EAGAIN menandai titik ketika status ready saat ini telah dikuras untuk operasi tersebut.
EAGAIN menjadi batas sinkronisasi
Untuk descriptor nonblocking, EAGAIN atau EWOULDBLOCK menunjukkan bahwa operasi yang diminta tidak dapat langsung membuat progres. Dalam read loop edge-triggered, hasil ini bukan sekadar penanganan error rutin. Hasil tersebut menutup interval pemrosesan yang terkait dengan readiness yang telah diamati.
for (;;) {
ssize_t n = read(fd, buf, sizeof buf);
if (n > 0) {
consume(buf, (size_t)n);
continue;
}
if (n == 0) {
handle_eof();
break;
}
if (errno == EINTR)
continue;
if (errno == EAGAIN || errno == EWOULDBLOCK)
break;
handle_error(errno);
break;
}Loop ini tidak mengasumsikan bahwa satu read() menguras sebuah stream. End-of-file juga tetap dibedakan dari kondisi sementara tanpa progres. Prinsip yang sama berlaku pada accept loop nonblocking: setelah listening socket menjadi ready, accept4() umumnya diulang sampai melaporkan EAGAIN.
Operasi spesifik tetap menentukan semantik. Stream socket, datagram socket, pipe, listening socket, dan objek pollable lain memiliki batas data serta kondisi error yang berbeda. epoll menyediakan informasi readiness; mekanisme ini tidak menghapus aturan spesifik dari tiap interface.
Readiness event tidak mempertahankan batas pesan aplikasi
TCP memperlihatkan ketidakcocokan ini dengan jelas. TCP menyajikan byte stream. Satu notifikasi EPOLLIN dapat berkaitan dengan byte dari sebagian satu frame aplikasi, beberapa frame, atau pengelompokan lain yang diizinkan oleh delivery stream. Sebaliknya, satu frame logis dapat memerlukan beberapa episode readiness jika byte-nya tiba secara bertahap.
Event loop karena itu memerlukan state terpisah untuk transport readiness dan parsing protokol. Sisi baca menguras byte ke state yang dikelola aplikasi, sementara parser mengambil frame lengkap berdasarkan length, delimiter, atau aturan state machine milik protokol.
Datagram socket mempertahankan batas datagram pada API socket, tetapi jumlah notifikasi tetap tidak merepresentasikan jumlah datagram. Beberapa datagram dapat siap dibaca dalam satu episode readiness. Handler tetap perlu melakukan receive sampai batas nonblocking tercapai jika desain bergantung pada notifikasi edge-triggered.
Partial write menciptakan masalah output yang simetris
Writable readiness juga merupakan status, bukan janji bahwa seluruh buffer aplikasi dapat diterima. write() atau send() nonblocking dapat mengonsumsi byte lebih sedikit dari jumlah yang diminta. Aplikasi harus mempertahankan suffix yang belum terkirim dan melanjutkan dari offset yang tepat.
Dengan notifikasi edge-triggered, kode output umumnya mencoba melakukan flush terhadap byte yang mengantre sampai antrean kosong atau operasi melaporkan EAGAIN. Jika masih ada byte tersisa, interest pada writable readiness harus tetap diatur agar transisi berikutnya dapat melanjutkan flush.
Membiarkan EPOLLOUT aktif terus-menerus dapat menghasilkan wakeup yang tidak perlu pada desain level-triggered karena socket sering berada dalam status writable. Desain edge-triggered mengurangi laporan berulang untuk status ready yang tidak berubah, tetapi bookkeeping state menjadi lebih ketat: ownership output yang mengantre, offset, perubahan interest mask, dan jalur close harus sepakat mengenai ada atau tidaknya pekerjaan output yang tersisa.
EPOLLONESHOT menambahkan rearming eksplisit ke model ownership
EPOLLONESHOT terpisah dari EPOLLET. Setelah sebuah event dikirim untuk descriptor yang didaftarkan dengan EPOLLONESHOT, descriptor tersebut dinonaktifkan pada interest list sampai aplikasi melakukan rearm dengan epoll_ctl(..., EPOLL_CTL_MOD, ...).
Mekanisme ini dapat mendukung ownership oleh worker. Satu worker menerima readiness, menguras atau memproses descriptor sesuai desain, memperbarui state koneksi, lalu melakukan rearm. Perilaku one-shot kernel mencegah delivery berulang biasa untuk registrasi tersebut sebelum rearm, tetapi tidak menggantikan sinkronisasi bagi data aplikasi yang dibagikan melalui jalur lain.
Kombinasi EPOLLONESHOT dan EPOLLET karena itu menghasilkan dua kewajiban berbeda: menguras readiness sesuai semantik edge-triggered dan melakukan rearm secara eksplisit ketika aplikasi siap menerima notifikasi berikutnya.
Identitas dan masa hidup file descriptor tetap merupakan persoalan terpisah
Event loop juga memerlukan kebijakan lifetime. Nomor file descriptor adalah integer lokal proses dan dapat digunakan kembali setelah close(). State aplikasi yang mengaitkan descriptor numerik dengan koneksi perlu mencegah pekerjaan lama bertindak pada objek baru yang kebetulan memperoleh nomor sama.
Registrasi epoll memiliki semantik objek kernel yang lebih spesifik daripada lookup integer biasa, tetapi antrean aplikasi, pesan worker, timer, dan tabel koneksi sering membawa identifier masing-masing. Generation counter, objek koneksi stabil, atau token lifetime lain dapat mencegah lapisan tersebut mencampur descriptor reuse dengan identitas koneksi.
Jalur close dan deregistration juga perlu dikoordinasikan dengan pekerjaan aplikasi yang masih tertunda. Notifikasi readiness tidak menyelesaikan ownership ataupun reclamation dengan sendirinya.
Edge triggering memindahkan repetisi dari interface kernel ke state aplikasi
Perbedaan praktis antara level dan edge triggering bukan sekadar opsi performa. Level triggering memungkinkan aplikasi mengamati kondisi ready secara berulang. Edge triggering menekan repetisi tersebut dan mengharuskan aplikasi menyimpan state yang cukup untuk menuntaskan pekerjaan yang terkait dengan setiap transisi readiness.
State itu mencakup data transport yang belum dibaca, frame protokol parsial, offset output yang belum terkirim, status rearming one-shot bila digunakan, serta lifetime koneksi. Event loop edge-triggered yang benar memperlakukan epoll sebagai mekanisme transisi readiness dan menyimpan perhitungan unit kerja pada interface yang memang mendefinisikan pekerjaan tersebut.