libc: implement splice(2), vmsplice(2), tee(2) - #1425
Conversation
There was a problem hiding this comment.
Pull request overview
Adds missing Linux splice-family syscalls to OSv libc (copy-based implementation) to unblock applications that use pipe-based high-throughput fd transfers, and wires them into syscall dispatch, tracing, symbol exports, and the test suite.
Changes:
- Implement
splice(2),vmsplice(2), andtee(2)inlibc/(withtee()returningENOSYS). - Register the new syscalls and tracepoints, and export the symbols from OSv’s libc and musl loader.
- Add
tst-splicecoverage for splice/vmsplice behavior and expected error paths.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/tst-splice.cc | New test covering splice/vmsplice success paths and error returns. |
| syscalls/syscalls.cc.in | Adds syscall table entries for splice, vmsplice, tee. |
| syscalls/syscall_tracepoints.cc.in | Adds tracepoints for the new syscalls. |
| modules/tests/Makefile | Builds and includes tst-splice.so in the tests module. |
| Makefile | Adds splice.o into libc build. |
| libc/splice.cc | Implements splice, vmsplice, tee. |
| exported_symbols/osv_libc.so.6.symbols | Exports splice, tee, vmsplice from libc. |
| exported_symbols/osv_ld-musl.so.1.symbols | Exports splice, tee, vmsplice from the musl loader. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Rebased onto current master and squashed into one coherent commit (the branch had drifted and its old base spuriously touched linux.cc/signal.cc/pthread.cc; that's gone - it now touches only the splice files). Addressed the review:
The change was independently reviewed by two reviewers (systems + API/semantics); the vmsplice |
|
The CHANGES_REQUESTED here predates my fixes - all of these are already addressed in the current head (rebased onto master):
Single squashed commit; builds/runs on Linux too. |
|
Rebased onto current master. The review items from the last round are addressed: the lingering 'ponytail' word is removed, non-seekable fds with an offset return ESPIPE, len>SSIZE_MAX returns EINVAL, a w==0 no-progress guard avoids a spin, vmsplice bounds nr_segs>UIO_MAXIOV and rejects the wrong direction (EBADF), and tee() returns ENOSYS. Squashed to a single commit. Ready for another look. |
|
Thanks for the careful read. Point-by-point:
I'll push the comment tightening for (2) and (4); (1) and (3) are already in the current tip. None of these change behavior, only make the documented contract match what the code does. |
Linux implements these as zero-copy transfers through a pipe's page buffers. OSv has no user/kernel boundary and the copies are cheap, so this is a correct (not zero-copy) implementation over the existing read/write and pread/pwrite paths, moving bytes through a bounce buffer. This unblocks the many programs that use splice()/sendfile-style loops without caring how the bytes move. - splice(): loops read_at -> write_at in <=64 KiB chunks; a non-null offset uses pread/pwrite and advances *off (leaving the fd position untouched), a null offset uses the fd position. Short reads / EOF end the transfer; a legal zero-byte (no-progress) write stops the loop rather than spinning. Rejects len > SSIZE_MAX (EINVAL) and a non-null offset on a non-seekable (pipe/socket) fd (ESPIPE), matching Linux. It does not enforce Linux's 'exactly one end must be a pipe' rule because OSv's anonymous pipe is not reliably distinguishable via fstat here; documented in the source. - vmsplice(): implements the memory -> pipe/fd direction (a write of the iovec into the fd); the pipe -> memory direction is rejected naturally by write() returning EBADF on a read-only fd. Bounds nr_segs at UIO_MAXIOV and guards the running byte total against ssize_t overflow. - tee(): returns ENOSYS - a true non-consuming tee needs to peek pipe data, which OSv's pipe does not support; better an honest ENOSYS than silently consuming the input. Wired into the syscall table, exported from libc and the musl loader, and covered by tst-splice (file->pipe->file round-trip, offset semantics, the EINVAL/ESPIPE/overflow guards, vmsplice, and tee's ENOSYS); it builds and runs on Linux too. TODO: a pipe-page-sharing zero-copy fast path could be added if splice ever shows up as a hot path; correctness does not need it.
What
splice(2),vmsplice(2), andtee(2)were missing entirely, so programs thatmove data between an fd and a pipe via splice (a common high-throughput idiom)
could not run.
How
OSv has no user/kernel boundary and copies are cheap, so this is a correct
(not zero-copy) implementation built on the existing read/write and pread/pwrite
paths:
splice(): move up tolenbytes fromfd_intofd_outthrough abounce buffer, honoring the optional
off_in/off_outoffset pointers(pread/pwrite when set, read/write otherwise), looping over short writes. A
pipe-page-sharing fast path can be added later if it ever shows up hot;
correctness does not need it.
vmsplice(): implement the common memory -> fd direction as a write of theiovec segments into the fd.
tee(): duplicating pipe data without consuming it needs a non-consumingpipe peek, which OSv's pipe does not support, so it returns
ENOSYSratherthan silently consuming the input.
Unknown flags return
EINVAL. Wired asSYS_splice/SYS_vmsplice/SYS_teewith tracepoints, symbols exported from
libc.so.6andld-musl.so.1(thedeclarations and
SPLICE_F_*flags are already in the musl headers).Testing
tests/tst-splice.cc: file -> pipe -> file with byte-integrity verification andoffset-pointer handling, vmsplice memory -> pipe round-trip,
teeENOSYS, andthe
EINVALpath. Passes on OSv under KVM.