Linux io_uring Memisahkan Submission dari Kepemilikan Completion

Sebuah request io_uring dapat tetap in flight setelah aplikasi selesai membentuk submission queue entry. Kondisi ini menciptakan batas lifetime yang tidak ada pada call sinkron sederhana: metadata request dapat menjadi stabil saat submission, sedangkan memori yang menjadi payload I/O masih dapat diakses sampai operasi selesai. Completion queue karena itu bukan sekadar kanal hasil. Untuk banyak operasi, completion menandai titik ketika state operasi milik aplikasi dapat di-reclaim atau digunakan kembali.

Pembedaan ini penting karena submission queue dan completion queue merupakan shared ring, tetapi keduanya mewakili fase berbeda. SQE mendeskripsikan pekerjaan yang akan diterbitkan. CQE melaporkan hasil pekerjaan yang mencapai state completion. Menganggap kedua fase tersebut sebagai satu interval kepemilikan dapat menghasilkan race penggunaan ulang buffer, data korelasi stale, atau tekanan queue yang tidak tampak pada lokasi call yang menyiapkan request.

Submission memindahkan deskripsi, bukan setiap byte yang direferensikan

SQE memuat field yang mendeskripsikan operasi: opcode, file descriptor, offset, panjang, flag, dan nilai seperti user_data. Beberapa operasi juga menunjuk ke memori aplikasi.

Pada kernel yang menyediakan stable submission semantics, struktur tambahan yang hanya dipakai untuk mendeskripsikan request perlu tetap valid sampai submission berhasil, bukan sampai completion akhir. Data yang benar-benar ditransfer memiliki lifetime berbeda. Buffer yang diberikan kepada read atau write yang masih in flight harus tetap sesuai bagi operasi sampai kernel selesai menggunakannya.

Perbedaan tersebut tampak pada request write:

struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_write(sqe, fd, buf, len, offset);
io_uring_sqe_set_data(sqe, op);

io_uring_submit(&ring);

Kembalinya io_uring_submit() tidak berarti buf tersedia untuk penggunaan ulang secara bebas. Call tersebut berarti pemrosesan submission telah bergerak sesuai mode ring dan kontrak library. Hasil write datang secara terpisah sebagai CQE.

Ini adalah aturan kepemilikan, bukan properti sintaks pointer C. Pointer tetap berupa alamat userspace biasa, tetapi operasi mempunyai dependensi yang masih aktif terhadap storage yang direferensikan.

CQE mengorelasikan hasil asinkron dengan state aplikasi

Urutan completion tidak harus sama dengan urutan submission untuk request yang independen. Aplikasi karena itu memerlukan state korelasi yang tidak bergantung pada posisi queue.

Field user_data pada SQE disediakan untuk kebutuhan ini. Nilai 64-bit tersebut disalin ke CQE yang terkait. Aplikasi lazim menaruh identifier di sana atau mengodekan pointer menuju objek operasi yang lifetime-nya mencakup completion.

Loop completion kemudian dapat mengambil state secara eksplisit:

struct io_uring_cqe *cqe;

if (io_uring_wait_cqe(&ring, &cqe) == 0) {
    struct operation *op = io_uring_cqe_get_data(cqe);

    op->result = cqe->res;
    finish_operation(op);

    io_uring_cqe_seen(&ring, cqe);
}

Posisi queue membawa ordering transport di dalam CQ. user_data membawa identitas aplikasi. Mencampurkan kedua peran itu menciptakan asumsi rapuh bahwa request nomor N harus selesai sebelum request nomor N+1.

Konsumsi completion terpisah dari selesainya operasi

CQE dapat sudah berada di completion ring saat userspace belum memprosesnya. Pada titik tersebut kernel telah menghasilkan result, tetapi aplikasi masih memiliki bookkeeping untuk mengonsumsi result tersebut dan memajukan CQ head.

Kondisi ini menciptakan dua batas yang berkaitan. Operation completion melepaskan dependensi yang hanya diperlukan selama operasi kernel masih in flight. Konsumsi CQ melepaskan slot completion ring agar dapat dipakai kembali sesuai protokol ring.

Aplikasi yang berhenti mengonsumsi CQE karena itu dapat menciptakan backpressure meskipun operasi I/O dasarnya sudah selesai. Ukuran ring mengubah kapasitas, bukan kebutuhan untuk menguras completion.

Desain shared-memory juga berarti update head dan tail queue memerlukan ordering semantics yang ditetapkan interface. Implementasi yang memakai liburing biasanya mengandalkan helper library alih-alih menulis sendiri detail memory-order tersebut.

Request multishot memperpanjang lifetime completion

Model one-shot yang umum memetakan satu request yang disubmit ke satu completion. Operasi multishot sengaja mengubah relasi tersebut: satu SQE dapat menghasilkan beberapa CQE.

Untuk request multishot, IORING_CQE_F_MORE menandakan bahwa completion tambahan masih dapat datang dari request yang sama. CQE tanpa flag tersebut menandai akhir sequence multishot. State aplikasi yang terikat pada request persisten karena itu tidak dapat di-reclaim hanya karena CQE pertama telah tiba.

Model lifetime berubah dari:

submit -> satu completion -> reclaim

menjadi:

submit -> completion + MORE
       -> completion + MORE
       -> completion akhir
       -> reclaim

Flag tersebut merupakan bagian dari kontrak completion. Menganggap CQE pertama sebagai final dapat mengubah request kernel persisten menjadi risiko use-after-free di userspace jika completion berikutnya masih membawa nilai korelasi yang sama.

Cancellation juga selesai secara asinkron

Meminta cancellation tidak menghapus operasi dari riwayat pada saat request cancellation disubmit. Cancellation sendiri direpresentasikan sebagai operasi, dan race tetap mungkin terjadi antara progres target dan pemrosesan cancellation.

Aplikasi harus menginterpretasikan result completion untuk target dan request cancellation sesuai kontrak opcode yang digunakan. Target mungkin sudah selesai, mungkin dibatalkan, atau mungkin berada pada state ketika upaya cancellation tidak dapat menghasilkan efek yang diminta.

Untuk kepemilikan resource, batas penting tetap berupa state completion yang dapat diamati, bukan saat cancellation diminta. Menggunakan ulang buffer target segera setelah menerbitkan cancellation akan mengasumsikan guarantee sinkronisasi yang tidak diberikan hanya oleh submission cancellation.

Shared ring memindahkan sinkronisasi ke struktur data

I/O sinkron tradisional menempatkan penerbitan request, blocking, dan return value di sekitar satu batas syscall. io_uring dapat memisahkan event tersebut antara publikasi queue, eksekusi kernel, dan konsumsi completion kemudian. Batching dan mode polling dapat mengubah kapan syscall terjadi tanpa menghapus transisi kepemilikan.

Pemisahan tersebut merupakan constraint engineering utama. Kapasitas SQ membatasi ruang publikasi outstanding, kapasitas CQ membatasi ruang result yang belum dikonsumsi, dan memori aplikasi dapat memiliki lifetime yang mencakup aktivitas user dan kernel. Interface memperoleh fleksibilitas dengan membuat fase-fase ini eksplisit.

Desain yang kokoh karena itu mengikat lifetime pada state operasi, bukan pada lexical scope yang menyiapkan SQE. Submission menetapkan bahwa pekerjaan telah diserahkan ke ring. Completion melaporkan hasilnya. Konsumsi CQ mengembalikan kapasitas completion. Memisahkan batas-batas tersebut mencegah mekanisme queue berubah diam-diam menjadi bug kepemilikan memori.