Linux splice() dapat memindahkan byte antar-file descriptor tanpa mengalirkan byte tersebut melalui buffer user space, tetapi interface ini bukan primitive penyalinan generik dari sembarang descriptor ke descriptor lain. Sedikitnya satu endpoint harus berupa pipe. Syarat tersebut membuat state pipe menjadi bagian dari kontrak transfer: kapasitas, data yang dapat dibaca, keberadaan writer, mode blocking, dan progres parsial dapat memengaruhi jalur data yang tampak sederhana.

Batas yang relevan bukan sekadar “salinan kernel versus salinan user.” splice() mengubah bentuk ownership dan flow control. Kode aplikasi tidak lagi memiliki array byte perantara, tetapi tetap memiliki control loop yang menghitung byte yang sudah dipindahkan, menangani readiness, dan mempertahankan semantik offset.

Pipe adalah endpoint transfer yang eksplisit

System call ini menerima descriptor input, descriptor output, jumlah byte maksimum, offset opsional, dan flags. Satu atau kedua descriptor dapat merujuk ke pipe, tetapi pemanggilan tanpa endpoint pipe tidak valid.

Transfer file ke socket karena itu lazim memakai dua operasi dengan pipe di antaranya:

file -> splice -> pipe -> splice -> socket

Pipe perantara bukan sekadar sintaks yang diwajibkan API. Pipe adalah objek kernel berbatas kapasitas dengan state readable dan writable miliknya sendiri. Operasi pertama dapat berhenti karena pipe tidak dapat menerima data tambahan. Operasi kedua dapat berhenti karena tujuan belum dapat menerima seluruh jumlah data. Progres pada jalur lengkap harus dicatat pada kedua batas tersebut.

Hal ini berbeda dari buffer aplikasi pada sisi control plane. Dengan read() lalu write(), proses memiliki region memori yang berisi byte di antara kedua pemanggilan. Dengan splice(), byte tersebut dapat tetap direpresentasikan oleh buffer pipe yang dikelola kernel, sehingga logika aplikasi mencatat kuantitas dan state descriptor alih-alih memeriksa payload secara langsung.

Pemanggilan yang sukses melaporkan progres, bukan penyelesaian niat

Return value positif adalah jumlah byte yang dipindahkan oleh pemanggilan tersebut. Nilainya dapat lebih kecil daripada jumlah yang diminta. Control logic yang benar karena itu memperlakukan ukuran permintaan sebagai batas atas, bukan jaminan penyelesaian.

Misalnya sebuah relay hendak memindahkan 256 KiB dari file ke pipe. Pemanggilan dengan jumlah tersebut dapat memindahkan lebih sedikit. Tahap berikutnya hanya boleh mengonsumsi jumlah yang benar-benar masuk ke pipe, sedangkan operasi luar harus menyimpan sisa jumlah byte logis.

ssize_t n = splice(in_fd, &off, pipefd[1], NULL, wanted, 0);
if (n > 0) {
    ssize_t pending = n;
    while (pending > 0) {
        ssize_t m = splice(pipefd[0], NULL, out_fd, NULL,
                           (size_t)pending, 0);
        if (m > 0) {
            pending -= m;
            continue;
        }
        /* handle EOF, retryable state, or error */
    }
}

Fragmen tersebut hanya menggambarkan pencatatan progres. Kode produksi perlu menentukan respons terhadap pemanggilan yang terinterupsi, hasil nonblocking, kegagalan tujuan, shutdown, dan cancellation. Invarian pentingnya adalah byte yang sudah diterima pipe tetap berstatus pending untuk output sampai tahap downstream mencatatnya.

Return value nol memiliki arti yang bergantung pada endpoint. Untuk input non-pipe, nilai itu menunjukkan akhir input. Untuk input pipe, nol menunjukkan tidak ada data tersisa dan tidak ada writer yang terhubung, sehingga menunggu tidak dapat menghasilkan byte tambahan dari pipe tersebut.

Pointer offset memisahkan posisi transfer dari posisi descriptor

Untuk input non-pipe, pointer offset null membuat splice() memakai dan memajukan file offset milik descriptor. Pointer non-null memakai posisi eksplisit; nilai yang ditunjuk maju sementara file offset descriptor sendiri tidak berubah. Sisi output memiliki aturan analog ketika objeknya seekable.

Perbedaan ini penting ketika descriptor dipakai bersama. Penggunaan posisi descriptor membuat progres transfer ikut berada dalam state open file description. Operasi lain yang memakai posisi yang sama dapat berinteraksi dengan state tersebut. Offset eksplisit menempatkan posisi operasi splice pada storage milik caller.

Descriptor pipe berbeda: argumen offset-nya harus null karena pipe bukan file byte-addressed yang seekable. Pipeline yang mencampur file dan pipe karena itu memiliki semantik posisi yang asimetris walaupun setiap endpoint direpresentasikan oleh integer file descriptor.

Asimetri ini penting saat meninjau kontrak API. Fakta bahwa kedua parameter berupa file descriptor tidak berarti keduanya menyediakan operasi atau state yang sama. Tipe descriptor tetap menjadi bagian kontrak.

Mode nonblocking berlaku pada lebih dari satu batas

SPLICE_F_NONBLOCK meminta perilaku nonblocking untuk operasi pipe pada splice. Flag tersebut tidak menetapkan bahwa setiap endpoint yang mendasarinya mustahil melakukan blocking. Interface Linux memisahkan flag splice dari state O_NONBLOCK pada descriptor yang terlibat.

Konsekuensinya terlihat pada kode event-driven. Relay dapat melihat pipe dalam kondisi writable dan tetap harus memperhitungkan perilaku sisi sumber. Sebaliknya, data readable di pipe tidak berarti tujuan socket dapat menerima seluruh jumlah pending.

Saat operasi akan blocking berdasarkan kondisi nonblocking yang berlaku, EAGAIN adalah hasil flow control, bukan indikasi data rusak. Control loop harus mempertahankan pencatatan byte pending dan melanjutkan hanya ketika state endpoint terkait memungkinkan progres tambahan.

Kapasitas pipe juga mencegah producer upstream maju tanpa batas ketika sisi downstream tersendat. Setelah pipe berbatas kapasitas tidak dapat menerima data tambahan, splice dari upstream berhenti membuat progres. Backpressure dengan demikian direpresentasikan langsung oleh objek kernel perantara, bukan oleh antrean aplikasi tanpa batas.

Tanpa salinan user space bukan jaminan zero-copy universal

Kontrak terdokumentasi menyatakan bahwa splice() memindahkan data tanpa penyalinan antara address space kernel dan address space user. Pernyataan tersebut lebih sempit daripada janji bahwa tidak ada penyalinan data di bagian mana pun pada jalur kernel atau perangkat.

Implementasi kernel dapat merepresentasikan buffer pipe memakai referensi ke page dan pada kondisi tertentu dapat menghindari penyalinan byte payload saat menghubungkan producer dan consumer yang didukung. Jalur persisnya bergantung pada tipe endpoint dan implementasi kernel. Filesystem juga dapat menolak operasi splice tertentu.

SPLICE_F_MOVE tidak mengubah peluang implementasi tersebut menjadi jaminan portabel. Flag itu adalah hint, dan perilaku terdokumentasi saat ini tidak memungkinkan correctness aplikasi bergantung pada perpindahan page secara fisik. Desain yang memerlukan correctness semantik harus mengandalkan jumlah byte yang ditransfer dan error yang terlihat melalui API, bukan asumsi mengenai jumlah salinan internal.

Batas ini juga memisahkan splice() dari klaim performa. Penghilangan buffer transfer user space dapat mengurangi sebagian penyalinan dan traffic memori pada jalur yang sesuai, tetapi throughput serta biaya CPU aktual bergantung pada kernel, filesystem, jalur socket, workload, dan control logic di sekitarnya. Kontrak system call menyediakan semantik, bukan percepatan yang berlaku sama untuk semua workload.

Ownership buffer pipe mengubah observability

read() konvensional ke memori aplikasi menciptakan titik inspeksi. Proses dapat melakukan parsing, hashing, transformasi, logging, atau menyimpan byte sebelum meneruskannya dengan write. Jalur splice sengaja menghilangkan titik inspeksi biasa tersebut.

Sifat ini berguna bagi relay opaque, tetapi sekaligus menjadi constraint. Jika aplikasi harus mengubah framing, menghitung digest atas byte payload, memeriksa field protokol, atau menyimpan salinan persis di memori user, jalur splice murni tidak lagi menyediakan representasi data yang dibutuhkan operasi tersebut.

Pilihannya karena itu bersifat arsitektural, bukan kosmetik. Mempertahankan data di buffer yang dikelola kernel dapat menyederhanakan jalur forwarding opaque, sedangkan pemrosesan yang sadar payload membutuhkan batas interface lain. Penggabungan kedua model sering berarti secara sengaja mengalirkan data tertentu melalui user space, bukan memperlakukan splice() sebagai pengganti universal untuk read() dan write().

Control loop tetap memiliki correctness transfer

splice() memindahkan batas penyimpanan byte ke kernel, tetapi tidak memindahkan semantik transaksi ke sana. Kernel melaporkan progres lokal antar-endpoint. Kernel tidak menentukan apakah message tingkat lebih tinggi sudah lengkap, apakah peer downstream telah memproses data secara durable, atau apakah stream yang baru diteruskan sebagian harus dicoba kembali setelah komponen lain gagal.

Sejumlah kewajiban karena itu tetap berada pada caller: menjaga jumlah byte secara tepat, membedakan akhir input dari kondisi tanpa progres yang dapat dicoba lagi, mengoordinasikan readiness untuk pipe berbatas kapasitas dan endpoint downstream, mempertahankan semantik posisi file yang dimaksud, serta menetapkan perilaku kegagalan setelah byte memasuki pipe perantara.

Batas desain yang dihasilkan cukup presisi. splice() dapat menghilangkan buffer payload user space dari jalur data Linux yang didukung, sementara kapasitas pipe dan state descriptor menjadi bagian utama mekanisme transfer. Payload dapat tetap berada di luar memori aplikasi, tetapi progres, ordering, cancellation, dan penyelesaian tingkat lebih tinggi tetap menjadi state yang dimiliki aplikasi.