mergerfs/vendored/libfuse/include/fuse_msgbuf.hpp
Antonio S. Musumeci 8eed256747 Restore splice zero-copy I/O as runtime options
Reinstate the splice read/write/move paths removed in 84592a9f, now
gated behind runtime options with negotiated pipe sizing and fallback
logging.

- Transport: msgbuf-piped splice receive, fd-referenced splice reply
  (vmsplice + splice), splice-move pipe-bounce, and restored
  fuse_buf_copy/fuse_buf_flags plumbing for copy fallbacks
- Negotiation: new pipe-max-size option (auto-raised at INIT when
  possible; the sysctl read/write is factored into src/pipe_max_size.cpp
  so fuse_init.cpp and option_parser.cpp share one implementation);
  fuse-msg-size clamped to what the pipe can hold; splice is disabled
  at INIT with a warning if the sysctl can't fit even the minimum,
  retracting the FUSE_CAP_SPLICE_* bits already advertised to the
  kernel in that case
- Read replies and splice-move are gated at 128 KiB (below that the
  syscalls cost more than the copy they avoid); every fallback path
  degrades to the copy path and logs once per process via SysLog
- msgbuf pipe cache is process-global and mutex-guarded (mutex_lockguard)
  so pipes can move between read and worker threads under
  process-thread mode
- API simplified after review: collapsed the three per-axis options
  (splice-read/write/move) into a single splice boolean (default on);
  no-splice-read/write/move and the bare splice-read/write/move
  spellings stay registered as accepted no-ops so existing mount
  options don't fail the mount, but they do NOT alter the splice
  knob - upgraders with no-splice-* in their config get splice
  enabled and must set splice=false explicitly (see
  deprecated_options.md)
- Fixed several correctness issues found in parallel review: a
  dropped write_buf registration that silently corrupted writes, a
  dirty-pipe reuse bug in msgbuf_free, an iov double-send in
  fuse_reply_data's splice-move fallback, and pipe-capacity math that
  assumed a 4096-byte page size
- Modernized the transport/negotiation code to this codebase's C++
  style: RAII (unique_ptr/DEFER) instead of malloc/free and manual fd
  close(), std::from_chars/to_chars instead of stoul/snprintf,
  _ReplyPipe's pipe ops as methods instead of free functions closing
  over a thread_local global, and vendored/libfuse/lib/buffer.cpp (a
  near-verbatim port of upstream's buffer.c) brought in line with the
  rest of the fork
- Documented splice and pipe-max-size options (mkdocs, FAQ,
  deprecated-options list); added tools/bench-splice.sh
- Benchmarked on tmpfs: seq_write +21%, seq_read +52%, small_write
  +5%, small_read +12%; validated with a 1h soak (~271 GiB
  hash-verified I/O, zero integrity failures), a live-mount round-trip
  of I/O above the splice threshold, and the existing unit/smoke
  suites at their pre-existing baseline

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 11:24:16 -04:00

97 lines
3.8 KiB
C++

/*
ISC License
Copyright (c) 2022, Antonio SJ Musumeci <trapexit@spawn.link>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#pragma once
#include "base_types.h"
#include "fuse_msgbuf_t.h"
u32 msgbuf_get_pagesize();
void msgbuf_set_bufsize(const u64 size);
u64 msgbuf_get_bufsize();
fuse_msgbuf_t *msgbuf_alloc();
fuse_msgbuf_t *msgbuf_alloc_page_aligned();
void msgbuf_free(fuse_msgbuf_t *msgbuf);
int msgbuf_ensure_pipe(fuse_msgbuf_t *msgbuf);
void msgbuf_drain_pipe(fuse_msgbuf_t *msgbuf);
void msgbuf_clear();
void msgbuf_gc();
u64 msgbuf_alloc_count();
/* Shared splice-transport plumbing used by both fuse_msgbuf.cpp and
fuse_lowlevel.cpp/fuse.cpp - factored here so the two translation
units can't independently drift on pipe sizing/logging. */
/* Below this size the extra splice/vmsplice syscalls cost more than
the copy they avoid (benchmark rationale: see src/config.cpp's
comment on the `splice` option). Shared by the read-reply gate
(fuse.cpp) and the write/splice-move gate (fuse_lowlevel.cpp) so
the two thresholds can't silently drift apart. */
constexpr u32 FUSE_SPLICE_MIN_SIZE = 131072u;
/* Read /proc/sys/fs/pipe-max-size once per process (cached) and
return it, clamped to UINT32_MAX; returns 1048576 (the historical
assumed default) if the sysctl can't be read or parsed. */
u32 fuse_effective_pipe_max();
enum class splice_fallback_tag_t
{
PIPE_MAX_READ_FALLBACK,
MSGBUF_FSETPIPE_EP,
MSGBUF_FSETPIPE_ANY,
MSGBUF_PIPE_CREATE,
REPLY_PIPE_FSETPIPE_EP,
REPLY_PIPE_FSETPIPE_ANY,
REPLY_PIPE_ENSURE,
REPLY_PIPE_CAP,
REPLY_VMSPLICE_HDR,
REPLY_FD_SPLICE,
RECEIVE_MSGBUF_PIPE,
RECEIVE_SPLICE_ERR,
REPLY_SPLICE_MOVE_WRITEV,
};
/* Warn once per process per tag so a busy mount doesn't spam
per-request logs when splice degrades to the copy path. */
void fuse_splice_fallback_log(splice_fallback_tag_t tag);
/* Size a pipe's kernel buffer via F_SETPIPE_SZ, clamping the request
to INT_MAX and retrying once at the 1MiB default on failure -
unconditionally when retry_unconditionally is set, otherwise only
when the first attempt failed with EPERM. Logs tag_any once if the
sized request failed at all, tag_ep once if every attempt failed.
Returns the granted size (>0) or -errno of the last attempt (<=0). */
int fuse_pipe_set_size(int fd,
u32 cap,
bool retry_unconditionally,
splice_fallback_tag_t tag_any,
splice_fallback_tag_t tag_ep);
/* Bounded, non-blocking pipe drain: consume exactly the bytes FIONREAD
reports as queued, reading into scratch (capped at scratch_size),
retrying on EINTR. Never blocks - the write end is assumed still
held by the caller, so a blocking read on an already-drained empty
pipe would wedge the thread forever. No-op if fd is -1. Shared by
msgbuf_drain_pipe (this file) and _ReplyPipe::drain
(fuse_lowlevel.cpp) so the receive-side and reply-side pipes can't
independently drift on drain semantics. */
void fuse_drain_pipe_fd(int fd, void *scratch, size_t scratch_size);