mergerfs/vendored/libfuse/lib/fuse_msgbuf.cpp

654 lines
19 KiB
C++
Raw Permalink Normal View History

2022-11-29 17:24:09 -06:00
/*
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.
*/
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-06 00:15:30 -04:00
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <atomic>
#include "fuse_msgbuf.hpp"
#include "fatal.hpp"
#include "fuse.h"
#include "fuse_kernel.h"
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-06 00:15:30 -04:00
#include "syslog.hpp"
/* Warn-once-per-process-per-tag logger shared with fuse_lowlevel.cpp
(declared in fuse_msgbuf.hpp) so the two translation units don't
each hand-roll their own copy of this mechanism. Each tag belongs
to one of two "domains" - the msgbuf/receive-side pipe plumbing in
this file, or the reply/splice-move plumbing in fuse_lowlevel.cpp -
which only differ in the fixed prefix/description text; the tag
name itself (embedded in both lines) still identifies the exact
call site. */
struct SpliceFallbackInfo
{
const char *name;
bool msgbuf_domain;
};
static constexpr SpliceFallbackInfo SPLICE_FALLBACK_INFO[] =
{
{ "pipe-max-read-fallback", true },
{ "msgbuf-fsetpipe-ep", true },
{ "msgbuf-fsetpipe-any", true },
{ "msgbuf-pipe-create", true },
{ "reply-pipe-fsetpipe-ep", false },
{ "reply-pipe-fsetpipe-any", false },
{ "reply-pipe-ensure", false },
{ "reply-pipe-cap", false },
{ "reply-vmsplice-hdr", false },
{ "reply-fd-splice", false },
{ "receive-msgbuf-pipe", false },
{ "receive-splice-err", false },
{ "reply-splice-move-writev", false },
};
void
fuse_splice_fallback_log(splice_fallback_tag_t tag_)
{
static std::atomic<bool> seen[sizeof(SPLICE_FALLBACK_INFO) /
sizeof(SPLICE_FALLBACK_INFO[0])];
const SpliceFallbackInfo &info = SPLICE_FALLBACK_INFO[(size_t)tag_];
bool expected = false;
if(!seen[(size_t)tag_].compare_exchange_strong(expected,true))
return;
const char *prefix = (info.msgbuf_domain ?
"msgbuf pipe fallback" : "splice fallback");
const char *description = (info.msgbuf_domain ?
"splice receive will use a smaller/default pipe" :
"splice I/O will use the copy path for this mount");
SysLog::warning("{} [{}]: {}",prefix,info.name,description);
/* Explicit stderr echo (rather than syslog's LOG_PERROR, which
would apply to every log message in the process): keeps this
one class of message visible on a foreground (-f) run without
doubling unrelated log output under systemd/journald. */
fprintf(stderr,"mergerfs: %s [%s]: %s\n",prefix,info.name,description);
}
#include "mutex.hpp"
2026-02-20 22:27:55 -06:00
#include "objpool.hpp"
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-06 00:15:30 -04:00
#include <sys/ioctl.h>
#include <fcntl.h>
2022-11-29 17:24:09 -06:00
#include <unistd.h>
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-06 00:15:30 -04:00
#include <algorithm>
#include <cassert>
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-06 00:15:30 -04:00
#include <cerrno>
#include <charconv>
#include <climits>
#include <cstdio>
#include <cstdlib>
2022-11-29 17:24:09 -06:00
2026-02-20 22:27:55 -06:00
static u32 g_pagesize = 0;
static u64 g_bufsize = 0;
2022-11-29 17:24:09 -06:00
2026-03-17 19:42:34 -05:00
/* Extra pages: 1 for page-alignment headroom, 1 for
fuse_in_header+fuse_write_in alignment */
static constexpr u64 MSGBUF_OVERHEAD_PAGES = 2ULL;
2026-02-24 22:22:20 -06:00
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-06 00:15:30 -04:00
/* Guards g_cached_pipes (defined below, near its struct types.) Declared
here (ahead of _constructor(), which initializes it) rather than
next to g_cached_pipes itself. */
static mutex_t g_cached_pipes_mutex;
2026-02-20 22:27:55 -06:00
static
__attribute__((constructor))
void
_constructor()
{
long pagesize = sysconf(_SC_PAGESIZE);
if(pagesize <= 0)
fatal::abort("pagesize query failed - {}",strerror(errno));
2026-02-20 22:27:55 -06:00
g_pagesize = pagesize;
2022-11-29 17:24:09 -06:00
2026-02-20 22:27:55 -06:00
assert((sizeof(struct fuse_in_header) + sizeof(struct fuse_write_in))
< g_pagesize);
msgbuf_set_bufsize(FUSE_DEFAULT_MAX_MAX_PAGES);
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-06 00:15:30 -04:00
mutex_init(g_cached_pipes_mutex);
}
2026-02-20 22:27:55 -06:00
struct PageAlignedAllocator
{
2026-02-20 22:27:55 -06:00
void*
allocate(size_t size_, size_t align_)
{
void *buf = nullptr;
2026-02-20 22:27:55 -06:00
int rv = posix_memalign(&buf, align_, size_);
return (rv == 0) ? buf : nullptr;
2026-02-20 22:27:55 -06:00
}
2026-02-20 22:27:55 -06:00
void
deallocate(void *ptr_, size_t /*size_*/, size_t /*align_*/)
{
free(ptr_);
}
};
struct ShouldPoolMsgbuf
{
2026-02-20 22:27:55 -06:00
bool operator()(const fuse_msgbuf_t *msgbuf) const noexcept
{
return msgbuf->size == (g_bufsize - g_pagesize);
}
};
static ObjPool<fuse_msgbuf_t, PageAlignedAllocator, ShouldPoolMsgbuf> g_msgbuf_pool;
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-06 00:15:30 -04:00
static
void
_msgbuf_pipe_init(fuse_msgbuf_t *msgbuf_)
{
msgbuf_->pipefd[0] = -1;
msgbuf_->pipefd[1] = -1;
msgbuf_->pipe_used = 0;
msgbuf_->pipe_cap = 0;
}
static
2022-11-29 17:24:09 -06:00
void
_msgbuf_page_align(fuse_msgbuf_t *msgbuf_)
2022-11-29 17:24:09 -06:00
{
msgbuf_->mem = (char*)msgbuf_;
2026-02-20 22:27:55 -06:00
msgbuf_->mem += g_pagesize;
msgbuf_->size = (g_bufsize - g_pagesize);
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-06 00:15:30 -04:00
_msgbuf_pipe_init(msgbuf_);
}
static
void
_msgbuf_write_align(fuse_msgbuf_t *msgbuf_)
{
msgbuf_->mem = (char*)msgbuf_;
2026-02-20 22:27:55 -06:00
msgbuf_->mem += g_pagesize;
msgbuf_->mem -= sizeof(struct fuse_in_header);
msgbuf_->mem -= sizeof(struct fuse_write_in);
2026-02-20 22:27:55 -06:00
msgbuf_->size = (g_bufsize - g_pagesize);
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-06 00:15:30 -04:00
_msgbuf_pipe_init(msgbuf_);
}
/*
* The ObjPool value-initializes msgbufs on every alloc, which would
* clobber live pipe fds carried by a pooled buffer, so pipes are not
* stored across free/alloc cycles. Instead released pipes are stashed
* in a small process-global cache keyed by capacity so the next
* msgbuf_ensure_pipe() usually avoids pipe2(2) and
* fcntl(F_SETPIPE_SZ) entirely.
*
* Global rather than thread-local: with the opt-in process-thread
* pool, msgbuf_free() runs on worker threads while msgbuf_ensure_pipe()
* runs on read threads - a thread-local cache can never hand a pipe
* across that boundary, and every request would pay pipe2(2) +
* F_SETPIPE_SZ + close. The cache is guarded by g_cached_pipes_mutex
* (mutex_lockguard, same idiom as f.lock in fuse.cpp) - the ops it
* protects are O(1) and vastly cheaper than the syscalls they avoid.
*/
struct cached_pipe_t
{
int fd0{-1};
int fd1{-1};
u32 cap{0};
};
struct cached_pipes_t
{
static constexpr u32 MAX = 16;
u32 count{0};
cached_pipe_t entries[MAX];
};
static cached_pipes_t g_cached_pipes;
static
u32
_read_pipe_max_size(void)
{
int fd = ::open("/proc/sys/fs/pipe-max-size",O_RDONLY | O_CLOEXEC);
if(fd == -1)
{
fuse_splice_fallback_log(splice_fallback_tag_t::PIPE_MAX_READ_FALLBACK);
return 1048576;
}
char buf[32];
ssize_t rv = ::read(fd,buf,sizeof(buf));
::close(fd);
if(rv <= 0)
{
fuse_splice_fallback_log(splice_fallback_tag_t::PIPE_MAX_READ_FALLBACK);
return 1048576;
}
unsigned long v;
auto [end,ec] = std::from_chars(buf,buf + rv,v);
if(ec != std::errc() || v == 0)
{
fuse_splice_fallback_log(splice_fallback_tag_t::PIPE_MAX_READ_FALLBACK);
return 1048576;
}
/* Clamp rather than treat as unreadable: a legitimately huge
sysctl (e.g. an admin-set multi-GiB pipe-max-size) must not
be mistaken for "couldn't read it" and silently replaced by
the small 1MiB default below. */
return (v > UINT32_MAX) ? UINT32_MAX : (u32)v;
}
u32
fuse_effective_pipe_max()
{
static const u32 max = _read_pipe_max_size();
return max;
}
/* Shared by msgbuf_ensure_pipe (this file) and _reply_pipe_ensure
(fuse_lowlevel.cpp): try F_SETPIPE_SZ at cap_, retrying once at the
1MiB default on failure. tag_any_ is logged whenever the sized
request fails at all (even if the retry then succeeds); tag_ep_ is
logged only if every attempt fails. */
int
fuse_pipe_set_size(int fd_,
u32 cap_,
bool retry_unconditionally_,
splice_fallback_tag_t tag_any_,
splice_fallback_tag_t tag_ep_)
{
int err = 0;
/* F_SETPIPE_SZ takes a signed int; a cap of 2^31 or higher (reachable
when pipe-max-size and the negotiated fuse-msg-size both pass
~2GiB, e.g. large-page hosts) would sign-extend into a negative
fcntl() argument below. Clamp to INT_MAX rather than cast blindly. */
if(cap_ > (u32)INT_MAX)
cap_ = (u32)INT_MAX;
int rv = ::fcntl(fd_,F_SETPIPE_SZ,(int)cap_);
if(rv == -1)
{
/* Capture immediately: fuse_splice_fallback_log() below can
itself clobber errno before it's read again further down. */
err = -errno;
fuse_splice_fallback_log(tag_any_);
if(retry_unconditionally_ || err == -EPERM)
{
rv = ::fcntl(fd_,F_SETPIPE_SZ,1048576);
if(rv == -1)
err = -errno; /* re-capture: this is a different syscall */
}
if(rv <= 0)
fuse_splice_fallback_log(tag_ep_);
}
return (rv > 0) ? rv : err;
}
/* Shared failure-cleanup for msgbuf_ensure_pipe: close the fds when a
pipe was actually created (fd0_ != -1), reset the msgbuf's pipe
state, and return rv_ (the errno-or-fixed-code the caller wants to
propagate). */
static
int
_msgbuf_ensure_pipe_fail(fuse_msgbuf_t *msgbuf_,
int fd0_,
int fd1_,
int rv_)
{
if(fd0_ != -1)
{
::close(fd0_);
::close(fd1_);
}
msgbuf_->pipefd[0] = -1;
msgbuf_->pipefd[1] = -1;
msgbuf_->pipe_cap = 0;
return rv_;
}
int
msgbuf_ensure_pipe(fuse_msgbuf_t *msgbuf_)
{
#if !defined(__linux__)
/* F_SETPIPE_SZ (used below to size the pipe to fit a whole splice
receive) is Linux-only. Without it a splice receive can't be
reliably sized, so fail unconditionally here and let every
caller fall back to the portable read() receive path, exactly
as documented ("splice remains unavailable on platforms that
lack it; the copy path is the fallback everywhere"). */
(void)msgbuf_;
return -ENOTSUP;
#else
u32 cap = (u32)std::min((u64)g_bufsize,(u64)fuse_effective_pipe_max());
/* F_SETPIPE_SZ takes a signed int; a cap of 2^31 or higher (reachable
when pipe-max-size and the negotiated fuse-msg-size both pass
~2GiB, e.g. large-page hosts) would sign-extend into a negative
fcntl() argument below. Clamp to INT_MAX rather than cast blindly -
mirrors the identical clamp inside fuse_pipe_set_size, but the
cache lookup just below also needs the clamped value. */
if(cap > (u32)INT_MAX)
cap = (u32)INT_MAX;
if(msgbuf_->pipefd[0] != -1)
return 0;
/* try global cache first */
{
mutex_lockguard(g_cached_pipes_mutex);
for(u32 i = 0; i < g_cached_pipes.count; i++)
{
cached_pipe_t *cp = &g_cached_pipes.entries[i];
if(cp->cap >= cap)
{
msgbuf_->pipefd[0] = cp->fd0;
msgbuf_->pipefd[1] = cp->fd1;
msgbuf_->pipe_cap = cp->cap;
g_cached_pipes.entries[i] = g_cached_pipes.entries[g_cached_pipes.count - 1];
g_cached_pipes.count--;
/* mutex_lockguard's unlock is deferred to scope exit, which
this return still triggers (stack unwinding runs it
regardless of which nested block the return sits in). */
return 0;
}
}
}
int rv = ::pipe2(msgbuf_->pipefd,O_CLOEXEC);
if(rv == -1)
{
int err = -errno; /* capture before the log call: SysLog::warning
-> syslog()/LOG_PERROR can make its own
syscalls (connect/sendto/write) and clobber
errno first. */
fuse_splice_fallback_log(splice_fallback_tag_t::MSGBUF_PIPE_CREATE);
msgbuf_->pipefd[0] = -1;
msgbuf_->pipefd[1] = -1;
return err;
}
/* Only retry unconditionally on EPERM: unlike the reply pipe
(fuse_lowlevel.cpp), a non-EPERM failure here (e.g. EINVAL on
an already-too-large ask) should not mask itself with a 1MiB
retry that might still be wrong for this request. */
rv = fuse_pipe_set_size(msgbuf_->pipefd[0],cap,/*retry_unconditionally_=*/false,
splice_fallback_tag_t::MSGBUF_FSETPIPE_ANY,
splice_fallback_tag_t::MSGBUF_FSETPIPE_EP);
if(rv <= 0)
return _msgbuf_ensure_pipe_fail(msgbuf_,msgbuf_->pipefd[0],msgbuf_->pipefd[1],rv);
/* Record what F_SETPIPE_SZ actually granted, not what we asked
for: after an EPERM->1MiB retry the real capacity can be
smaller than the ask, and the process-global pipe cache must
not serve this pipe for a request it cannot hold (a short
receive splice would then fail the in->len check and kill the
worker thread). */
msgbuf_->pipe_cap = (u32)rv;
/* The EPERM->1MiB retry (or a cache hit sized for a smaller
g_bufsize than this msgbuf now needs) can grant a pipe smaller
than "cap" - what THIS request actually requires to hold one
whole message. Handing that pipe back as success would let the
caller splice from /dev/fuse into it, come up short
(splice_n < in->len), and return -EIO, which fuse_loop.cpp's
_retriable_receive_error does not treat as retriable: that
tears down the entire session over one thread's transient
pipe-sizing shortfall. Fail HERE instead, before any /dev/fuse
data has been touched, so the caller falls back to the plain
read() receive for this message like every other
msgbuf_ensure_pipe failure already does. */
if(msgbuf_->pipe_cap < cap)
{
fuse_splice_fallback_log(splice_fallback_tag_t::MSGBUF_FSETPIPE_EP);
return _msgbuf_ensure_pipe_fail(msgbuf_,msgbuf_->pipefd[0],msgbuf_->pipefd[1],-ENOSPC);
}
return 0;
#endif /* defined(__linux__) */
}
void
fuse_drain_pipe_fd(int fd_,
void *scratch_,
size_t scratch_size_)
{
if(fd_ == -1)
return;
for(;;)
{
int avail;
if(::ioctl(fd_,FIONREAD,&avail) == -1)
break;
if(avail <= 0)
break;
ssize_t rv = ::read(fd_,scratch_,std::min((size_t)avail,scratch_size_));
if(rv == -1)
{
if(errno == EINTR)
continue;
break;
}
if(rv == 0)
break;
}
}
void
msgbuf_drain_pipe(fuse_msgbuf_t *msgbuf_)
{
/* This is the cleanup path for failed spliced writes (ENOSPC etc.),
where the payload may have been fully or partially consumed
already. */
fuse_drain_pipe_fd(msgbuf_->pipefd[0],msgbuf_->mem,msgbuf_->size);
msgbuf_->pipe_used = 0;
}
static
void
_msgbuf_release_pipe(fuse_msgbuf_t *msgbuf_)
{
if(msgbuf_->pipefd[0] == -1)
return;
/* Cache bookkeeping happens under lock; the close() below (taken only
on cache overflow) deliberately happens AFTER the lock is released,
so the mutex is never held across a syscall. */
bool cached = false;
{
mutex_lockguard(g_cached_pipes_mutex);
if(g_cached_pipes.count < cached_pipes_t::MAX)
{
cached_pipe_t *cp = &g_cached_pipes.entries[g_cached_pipes.count++];
cp->fd0 = msgbuf_->pipefd[0];
cp->fd1 = msgbuf_->pipefd[1];
/* cache the capacity F_SETPIPE_SZ actually granted: every
success path through msgbuf_ensure_pipe (cache hit or fresh
fcntl) sets pipe_cap nonzero before pipefd[0] goes valid, so
there is no "no fcntl ever ran" case to fall back for here. */
cp->cap = msgbuf_->pipe_cap;
cached = true;
}
}
if(!cached)
{
::close(msgbuf_->pipefd[0]);
::close(msgbuf_->pipefd[1]);
}
msgbuf_->pipefd[0] = -1;
msgbuf_->pipefd[1] = -1;
}
/* Close and drop every pipe currently sitting in the process-global
cache. Used by msgbuf_clear() so the "gc" control-file command (the
admin-facing "shrink resource usage now" entry point) actually
reclaims the cache's fds/kernel pipe buffers instead of only
clearing the msgbuf pool - previously the cache was documented as
"reclaimed ... at process exit", but no such reclamation code
existed anywhere, so in practice it was only ever trimmed by the
count>=MAX overflow-close path inside _msgbuf_release_pipe. */
static
void
_cached_pipes_clear(void)
{
cached_pipes_t local;
/* Snapshot-and-clear under lock, close() (a syscall) after releasing
it - same discipline as _msgbuf_release_pipe above. */
{
mutex_lockguard(g_cached_pipes_mutex);
local = g_cached_pipes;
g_cached_pipes.count = 0;
}
for(u32 i = 0; i < local.count; i++)
{
::close(local.entries[i].fd0);
::close(local.entries[i].fd1);
}
2022-11-29 17:24:09 -06:00
}
typedef void (*msgbuf_setup_func_t)(fuse_msgbuf_t*);
static
2022-11-29 17:24:09 -06:00
fuse_msgbuf_t*
_msgbuf_alloc(msgbuf_setup_func_t setup_func_)
2022-11-29 17:24:09 -06:00
{
fuse_msgbuf_t *msgbuf;
2026-02-22 17:59:46 -06:00
msgbuf = g_msgbuf_pool.alloc_size(g_bufsize);
2026-02-20 22:27:55 -06:00
if(msgbuf == NULL)
return NULL;
2022-11-29 17:24:09 -06:00
setup_func_(msgbuf);
2022-11-29 17:24:09 -06:00
return msgbuf;
}
fuse_msgbuf_t*
msgbuf_alloc()
{
return _msgbuf_alloc(_msgbuf_write_align);
}
fuse_msgbuf_t*
msgbuf_alloc_page_aligned()
{
return _msgbuf_alloc(_msgbuf_page_align);
}
void
2026-02-20 22:27:55 -06:00
msgbuf_free(fuse_msgbuf_t *msgbuf_)
{
if(msgbuf_ == nullptr)
return;
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-06 00:15:30 -04:00
/* Drain whenever a pipe exists, not only when pipe_used says bytes
remain: receive-path error returns (short splice, header read
failure, in->len sanity) leave queued bytes with pipe_used == 0,
and releasing a dirty pipe into the global cache would corrupt
the next request staged into it. msgbuf_drain_pipe is a
FIONREAD-bounded no-op on a clean pipe. */
if(msgbuf_->pipefd[0] != -1)
msgbuf_drain_pipe(msgbuf_);
_msgbuf_release_pipe(msgbuf_);
2026-02-22 17:59:46 -06:00
g_msgbuf_pool.free_size(msgbuf_,g_bufsize);
}
2026-02-20 22:27:55 -06:00
u64
msgbuf_get_bufsize()
2022-11-29 17:24:09 -06:00
{
2026-02-20 22:27:55 -06:00
return g_bufsize;
}
2026-02-20 22:27:55 -06:00
u32
msgbuf_get_pagesize()
{
return g_pagesize;
}
2026-02-20 22:27:55 -06:00
void
msgbuf_set_bufsize(const u64 size_in_pages_)
{
u64 new_bufsize;
new_bufsize = ((size_in_pages_ + MSGBUF_OVERHEAD_PAGES) * g_pagesize);
if(new_bufsize != g_bufsize)
{
g_bufsize = new_bufsize;
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-06 00:15:30 -04:00
/* Pool nodes are keyed by alloc size; a stale-size node must
never be reused for the new bufsize, so drop the whole pool.
(Pooled msgbufs never hold pipes: msgbuf_free always runs
_msgbuf_release_pipe first, moving any pipe into the
process-global cache or closing it. Cached pipes are keyed
by granted capacity and remain usable across a bufsize
change - msgbuf_ensure_pipe's first-fit only accepts
cp->cap >= cap.) */
g_msgbuf_pool.clear();
}
}
u64
msgbuf_alloc_count()
{
2026-02-20 22:27:55 -06:00
return g_msgbuf_pool.size();
}
void
2026-02-20 22:27:55 -06:00
msgbuf_gc()
{
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-06 00:15:30 -04:00
/* gc() deallocates objects inside ObjPool; any pipe a msgbuf held
was already released to the process-global cache (or closed) by
msgbuf_free's _msgbuf_release_pipe before the node returned to
the pool. Nothing to reclaim here beyond delegating. The
process-global cache itself (up to 16 pipes / 32 fds) is left
alone by this lighter-weight periodic gc() - it survives across
threads by design - but IS reclaimed by _cached_pipes_clear() via
the heavier, explicitly-invoked msgbuf_clear(). */
2026-02-20 22:27:55 -06:00
g_msgbuf_pool.gc();
}
void
2026-02-20 22:27:55 -06:00
msgbuf_clear()
{
2026-02-20 22:27:55 -06:00
g_msgbuf_pool.clear();
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-06 00:15:30 -04:00
_cached_pipes_clear();
2022-11-29 17:24:09 -06:00
}