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>
|
|
|
|
|
|
|
|
|
|
|
2023-02-22 17:52:46 -05:00
|
|
|
#include "fuse_msgbuf.hpp"
|
2025-10-24 08:20:29 -05:00
|
|
|
|
2026-03-17 23:55:32 -05:00
|
|
|
#include "fatal.hpp"
|
2023-02-22 17:52:46 -05:00
|
|
|
#include "fuse.h"
|
2023-07-09 02:04:32 -05:00
|
|
|
#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>
|
2026-02-16 20:55:18 -06:00
|
|
|
#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>
|
2023-02-01 23:05:45 -05:00
|
|
|
#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 */
|
2026-03-17 23:55:32 -05:00
|
|
|
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);
|
2023-04-23 23:33:48 -04:00
|
|
|
|
2026-03-17 23:55:32 -05:00
|
|
|
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);
|
2023-07-09 02:04:32 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-20 22:27:55 -06:00
|
|
|
struct PageAlignedAllocator
|
2025-10-24 08:20:29 -05:00
|
|
|
{
|
2026-02-20 22:27:55 -06:00
|
|
|
void*
|
|
|
|
|
allocate(size_t size_, size_t align_)
|
|
|
|
|
{
|
2026-03-17 23:55:32 -05:00
|
|
|
void *buf = nullptr;
|
2026-02-20 22:27:55 -06:00
|
|
|
int rv = posix_memalign(&buf, align_, size_);
|
2026-03-17 23:55:32 -05:00
|
|
|
return (rv == 0) ? buf : nullptr;
|
2026-02-20 22:27:55 -06:00
|
|
|
}
|
2025-10-24 08:20:29 -05:00
|
|
|
|
2026-02-20 22:27:55 -06:00
|
|
|
void
|
|
|
|
|
deallocate(void *ptr_, size_t /*size_*/, size_t /*align_*/)
|
|
|
|
|
{
|
|
|
|
|
free(ptr_);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct ShouldPoolMsgbuf
|
2023-02-22 17:52:46 -05:00
|
|
|
{
|
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;
|
2023-02-22 17:52:46 -05: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
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-24 08:20:29 -05:00
|
|
|
static
|
2022-11-29 17:24:09 -06:00
|
|
|
void
|
2025-10-24 08:20:29 -05:00
|
|
|
_msgbuf_page_align(fuse_msgbuf_t *msgbuf_)
|
2022-11-29 17:24:09 -06:00
|
|
|
{
|
2023-07-09 02:04:32 -05: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_);
|
2023-07-09 02:04:32 -05:00
|
|
|
}
|
2023-04-23 23:33:48 -04:00
|
|
|
|
2025-10-24 08:20:29 -05:00
|
|
|
static
|
2023-07-09 02:04:32 -05:00
|
|
|
void
|
2025-10-24 08:20:29 -05:00
|
|
|
_msgbuf_write_align(fuse_msgbuf_t *msgbuf_)
|
2023-07-09 02:04:32 -05:00
|
|
|
{
|
|
|
|
|
msgbuf_->mem = (char*)msgbuf_;
|
2026-02-20 22:27:55 -06:00
|
|
|
msgbuf_->mem += g_pagesize;
|
2023-07-09 02:04:32 -05:00
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2023-07-09 02:04:32 -05:00
|
|
|
typedef void (*msgbuf_setup_func_t)(fuse_msgbuf_t*);
|
|
|
|
|
|
|
|
|
|
static
|
2022-11-29 17:24:09 -06:00
|
|
|
fuse_msgbuf_t*
|
2023-07-09 02:04:32 -05:00
|
|
|
_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
|
|
|
|
2023-07-09 02:04:32 -05:00
|
|
|
setup_func_(msgbuf);
|
|
|
|
|
|
2022-11-29 17:24:09 -06:00
|
|
|
return msgbuf;
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-09 02:04:32 -05:00
|
|
|
fuse_msgbuf_t*
|
|
|
|
|
msgbuf_alloc()
|
|
|
|
|
{
|
2025-10-24 08:20:29 -05:00
|
|
|
return _msgbuf_alloc(_msgbuf_write_align);
|
2023-07-09 02:04:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fuse_msgbuf_t*
|
|
|
|
|
msgbuf_alloc_page_aligned()
|
|
|
|
|
{
|
2025-10-24 08:20:29 -05:00
|
|
|
return _msgbuf_alloc(_msgbuf_page_align);
|
2023-07-09 02:04:32 -05:00
|
|
|
}
|
|
|
|
|
|
2023-04-23 23:33:48 -04:00
|
|
|
void
|
2026-02-20 22:27:55 -06:00
|
|
|
msgbuf_free(fuse_msgbuf_t *msgbuf_)
|
2023-04-23 23:33:48 -04:00
|
|
|
{
|
2026-03-17 23:55:32 -05:00
|
|
|
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);
|
2023-04-23 23:33:48 -04:00
|
|
|
}
|
|
|
|
|
|
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-16 20:55:18 -06:00
|
|
|
|
2026-02-20 22:27:55 -06:00
|
|
|
u32
|
|
|
|
|
msgbuf_get_pagesize()
|
|
|
|
|
{
|
|
|
|
|
return g_pagesize;
|
|
|
|
|
}
|
2026-02-16 20:55:18 -06:00
|
|
|
|
2026-02-20 22:27:55 -06:00
|
|
|
void
|
|
|
|
|
msgbuf_set_bufsize(const u64 size_in_pages_)
|
|
|
|
|
{
|
2026-03-09 23:42:03 -05:00
|
|
|
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.) */
|
2026-03-09 23:42:03 -05:00
|
|
|
g_msgbuf_pool.clear();
|
|
|
|
|
}
|
2023-04-23 23:33:48 -04:00
|
|
|
}
|
|
|
|
|
|
2026-02-16 20:55:18 -06:00
|
|
|
u64
|
2023-04-23 23:33:48 -04:00
|
|
|
msgbuf_alloc_count()
|
|
|
|
|
{
|
2026-02-20 22:27:55 -06:00
|
|
|
return g_msgbuf_pool.size();
|
2023-04-23 23:33:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void
|
2026-02-20 22:27:55 -06:00
|
|
|
msgbuf_gc()
|
2023-04-23 23:33:48 -04: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
|
|
|
/* 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();
|
2023-04-23 23:33:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void
|
2026-02-20 22:27:55 -06:00
|
|
|
msgbuf_clear()
|
2023-04-23 23:33:48 -04:00
|
|
|
{
|
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
|
|
|
}
|