mirror of
https://github.com/trapexit/mergerfs.git
synced 2026-10-26 19:01:56 -04:00
splice
This commit is contained in:
parent
515b0549ef
commit
54166afe08
15 changed files with 1984 additions and 65 deletions
313
src/fuse_read_buf.cpp
Normal file
313
src/fuse_read_buf.cpp
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
/*
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2026, 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.
|
||||
*/
|
||||
|
||||
#include "fuse_read_buf.hpp"
|
||||
|
||||
#include "fileinfo.hpp"
|
||||
#include "fs_fstat.hpp"
|
||||
#include "fs_getfl.hpp"
|
||||
#include "fs_statx.hpp"
|
||||
#include "ioprio.hpp"
|
||||
#include "state.hpp"
|
||||
|
||||
#include "fuse.h"
|
||||
|
||||
#include "boost/unordered/concurrent_flat_map.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <fcntl.h>
|
||||
#include <shared_mutex>
|
||||
|
||||
#if defined(__linux__)
|
||||
#include <sys/sysmacros.h>
|
||||
#include <sys/vfs.h>
|
||||
#endif
|
||||
|
||||
|
||||
/* The branch file's type, size and device. A statx asking only for the
|
||||
first two lets filesystems skip work a full fstat implies (NFS
|
||||
flushes the file's dirty pages before reporting c/mtime; the device
|
||||
is always reported); fstat covers builds and filesystems without
|
||||
statx. */
|
||||
static
|
||||
int
|
||||
_type_and_size(const int fd_,
|
||||
mode_t *mode_,
|
||||
s64 *size_,
|
||||
dev_t *dev_)
|
||||
{
|
||||
int rv;
|
||||
struct stat st;
|
||||
|
||||
#ifdef MERGERFS_SUPPORTED_STATX
|
||||
struct fuse_statx stx;
|
||||
const unsigned int want = (STATX_TYPE|STATX_SIZE);
|
||||
|
||||
rv = fs::statx(fd_,"",AT_EMPTY_PATH,want,&stx);
|
||||
if((rv == 0) && ((stx.mask & want) == want))
|
||||
{
|
||||
*mode_ = stx.mode;
|
||||
*size_ = (s64)stx.size;
|
||||
*dev_ = makedev(stx.dev_major,stx.dev_minor);
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
rv = fs::fstat(fd_,&st);
|
||||
if(rv < 0)
|
||||
return rv;
|
||||
|
||||
*mode_ = st.st_mode;
|
||||
*size_ = st.st_size;
|
||||
*dev_ = st.st_dev;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Devices of branch filesystems _splice_coherent refused by type. The
|
||||
type is a property of the superblock, not of the open file, and on
|
||||
the network and FUSE filesystems it refuses fstatfs is a round trip
|
||||
to their server, which the first large read of every opened file
|
||||
would otherwise pay. Only refusals are remembered, so a device
|
||||
number reused by another filesystem after an unmount can cost
|
||||
splicing but never coherence. The value is unused. */
|
||||
static boost::concurrent_flat_map<dev_t,bool> g_refused_devs;
|
||||
|
||||
/* Whether splicing from the branch fd returns what pread would, and is
|
||||
worth doing. A filesystem that cannot be identified is refused too.
|
||||
Not on FUSE, SMB/CIFS, NFS, Ceph, AFS, overlayfs or 9p
|
||||
branches: their splice_read serves the branch inode's page cache
|
||||
(filemap_splice_read) while their read path bypasses it or first
|
||||
revalidates it (FUSE direct_io and auto_inval_data, SMB without a
|
||||
read lease, NFS/Ceph/AFS/9p attribute/data caching, overlayfs's
|
||||
copy-up), so a spliced reply could return stale data where the copy
|
||||
path returns current data. Nor on an fd opened O_DIRECT, regardless
|
||||
of filesystem: the kernel serves that splice by direct I/O into
|
||||
freshly allocated pipe pages (as pread does, so nothing is stale),
|
||||
which saves no copy over the copy path while adding syscalls, and a
|
||||
misaligned request's EINVAL would look like a filesystem without
|
||||
splice_read. A refusal by filesystem type is remembered for dev_ (the
|
||||
fd's device, 0 if unknown) so later handles on that filesystem skip
|
||||
the fstatfs. */
|
||||
static
|
||||
bool
|
||||
_splice_coherent(const int fd_,
|
||||
const dev_t dev_)
|
||||
{
|
||||
#if defined(__linux__)
|
||||
constexpr u32 MAGIC_FUSE = 0x65735546; // fuse, fuseblk, virtiofs
|
||||
constexpr u32 MAGIC_CIFS = 0xFF534D42;
|
||||
constexpr u32 MAGIC_SMB2 = 0xFE534D42;
|
||||
constexpr u32 MAGIC_NFS = 0x00006969;
|
||||
constexpr u32 MAGIC_CEPH = 0x00C36400;
|
||||
constexpr u32 MAGIC_AFS = 0x5346414F; // OpenAFS (/afs)
|
||||
constexpr u32 MAGIC_KAFS = 0x6B414653; // in-kernel AFS client
|
||||
constexpr u32 MAGIC_OVERLAYFS = 0x794C7630;
|
||||
constexpr u32 MAGIC_V9FS = 0x01021997;
|
||||
struct statfs st;
|
||||
int rv;
|
||||
|
||||
if(::fstatfs(fd_,&st) < 0)
|
||||
return false;
|
||||
|
||||
switch((u32)st.f_type)
|
||||
{
|
||||
case MAGIC_FUSE:
|
||||
case MAGIC_CIFS:
|
||||
case MAGIC_SMB2:
|
||||
case MAGIC_NFS:
|
||||
case MAGIC_CEPH:
|
||||
case MAGIC_AFS:
|
||||
case MAGIC_KAFS:
|
||||
case MAGIC_OVERLAYFS:
|
||||
case MAGIC_V9FS:
|
||||
if(dev_ != 0)
|
||||
g_refused_devs.emplace(dev_,true);
|
||||
return false;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
rv = fs::getfl(fd_);
|
||||
if((rv >= 0) && (rv & O_DIRECT))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
#else
|
||||
(void)fd_;
|
||||
(void)dev_;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Whether [offset, offset+size) lies inside [0, bound), computed
|
||||
without overflow: offset+size itself can exceed INT64_MAX for reads
|
||||
near the top of a huge sparse file. */
|
||||
static
|
||||
inline
|
||||
bool
|
||||
_window_inside(const off_t offset_,
|
||||
const size_t size_,
|
||||
const s64 bound_)
|
||||
{
|
||||
return ((offset_ >= 0) &&
|
||||
(offset_ < bound_) &&
|
||||
((u64)size_ <= (u64)(bound_ - offset_)));
|
||||
}
|
||||
|
||||
/* Describe a large read as a region of the branch file so libfuse can
|
||||
splice it into /dev/fuse without the data crossing userspace.
|
||||
Returning a positive value declines, and libfuse calls FUSE::read.
|
||||
|
||||
The reply header, whose length is the region size, is staged before
|
||||
any data, so only a read lying entirely within the file's reported
|
||||
size is offered. A read crossing it declines rather than being
|
||||
trimmed: some branches report a size that lags the data
|
||||
(attribute-caching network or FUSE filesystems), and a trimmed reply
|
||||
would end short where pread returns the rest. This also declines
|
||||
non-regular files and files reporting size 0 (procfs) or 4096
|
||||
(sysfs) while returning live content, which a splice past the
|
||||
reported size would generate twice or block on. A file that shrinks
|
||||
between the size check and the splice falls back to FUSE::read.
|
||||
|
||||
The size check is skipped when the window ends inside
|
||||
fi->size_floor, a size already observed on this handle; a stale floor
|
||||
only costs a fallback, whose short FUSE::read lowers it.
|
||||
|
||||
fi->mutex is held shared so moveonenospc's dup2 cannot rebind fd
|
||||
while libfuse stages from it (see FileRegion::lock). It is never
|
||||
waited for: a move holds it for a whole file copy, so the read
|
||||
declines instead and the copy path serves it from the old fd. */
|
||||
int
|
||||
FUSE::read_buf(const fuse_req_ctx_t *ctx_,
|
||||
const fuse_file_info_t *ffi_,
|
||||
FileRegion *region_,
|
||||
const size_t size_,
|
||||
const off_t offset_)
|
||||
{
|
||||
FileInfo *fi;
|
||||
FileInfo::Splice splice;
|
||||
s64 floor;
|
||||
dev_t dev = 0;
|
||||
|
||||
fi = state.get_fi(ctx_,ffi_->fh);
|
||||
if(not fi)
|
||||
return -EBADF;
|
||||
|
||||
if(fi->splice_state.load(std::memory_order_relaxed) == FileInfo::Splice::no)
|
||||
return 1;
|
||||
|
||||
std::shared_lock<std::shared_mutex> lock(fi->mutex,std::try_to_lock);
|
||||
if(not lock.owns_lock())
|
||||
return 1;
|
||||
|
||||
/* Checked after the lock is held so a mover that started waiting
|
||||
before try_to_lock succeeded is not joined by this splice. */
|
||||
if(fi->exclusive_waiters.load(std::memory_order_relaxed) > 0)
|
||||
return 1;
|
||||
|
||||
/* Read again under the lock: a move that completed before try_to_lock
|
||||
rebound fd and reset this to unknown (fd_rebound), and the
|
||||
once-per-binding check below must see that. */
|
||||
splice = fi->splice_state.load(std::memory_order_relaxed);
|
||||
if(splice == FileInfo::Splice::no)
|
||||
return 1;
|
||||
|
||||
floor = fi->size_floor.load(std::memory_order_relaxed);
|
||||
if(not ::_window_inside(offset_,size_,floor))
|
||||
{
|
||||
mode_t mode;
|
||||
s64 fsize;
|
||||
|
||||
if((::_type_and_size(fi->fd,&mode,&fsize,&dev) < 0) ||
|
||||
!S_ISREG(mode))
|
||||
return 1;
|
||||
|
||||
/* On a filesystem already refused by type, settle this handle
|
||||
now, even for a read that would decline below anyway (every
|
||||
read of a file smaller than the request does), so its later
|
||||
reads skip this statx. */
|
||||
if((splice == FileInfo::Splice::unknown) && g_refused_devs.contains(dev))
|
||||
{
|
||||
fi->splice_state.compare_exchange_strong(splice,
|
||||
FileInfo::Splice::no,
|
||||
std::memory_order_relaxed);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Only replace the floor this check started from: a short read
|
||||
may have lowered it meanwhile, and that is newer than fsize. */
|
||||
if(fsize > 0)
|
||||
fi->size_floor.compare_exchange_strong(floor,fsize,
|
||||
std::memory_order_relaxed);
|
||||
if(not ::_window_inside(offset_,size_,fsize))
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Once per fd binding, for a read that could otherwise splice, and
|
||||
under the lock so the answer is about the file fd refers to. A
|
||||
concurrent rejection (read_buf_rejected) wins over this check. */
|
||||
if(splice == FileInfo::Splice::unknown)
|
||||
{
|
||||
/* A filesystem that cannot be identified is not offered either,
|
||||
and that is settled too: retrying would repeat the fstatfs,
|
||||
possibly a server round trip, on every later large read. */
|
||||
const FileInfo::Splice want = (::_splice_coherent(fi->fd,dev) ?
|
||||
FileInfo::Splice::yes :
|
||||
FileInfo::Splice::no);
|
||||
|
||||
/* On failure splice holds the concurrent decision, which wins. */
|
||||
if(fi->splice_state.compare_exchange_strong(splice,want,
|
||||
std::memory_order_relaxed))
|
||||
splice = want;
|
||||
if(splice != FileInfo::Splice::yes)
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ioprio is thread-persistent; without SetFrom the branch-file I/O
|
||||
performed downstream of this handler (the kernel-side splice in
|
||||
the reply path runs on this same worker thread) would inherit
|
||||
whatever priority a previous request left behind. */
|
||||
ioprio::SetFrom iop(ctx_->pid);
|
||||
|
||||
region_->fd = fi->fd;
|
||||
region_->offset = offset_;
|
||||
region_->size = size_;
|
||||
region_->lock = std::move(lock);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The branch fd refused splice (EINVAL on its first splice: its
|
||||
filesystem has no splice_read). That will not change for this fd, so
|
||||
stop offering it and let every later read go straight to FUSE::read.
|
||||
O_DIRECT fds never get here: _splice_coherent refuses them. libfuse
|
||||
calls this while the region's lock is still held, so fd is still the
|
||||
one that refused. */
|
||||
void
|
||||
FUSE::read_buf_rejected(const fuse_req_ctx_t *ctx_,
|
||||
const fuse_file_info_t *ffi_)
|
||||
{
|
||||
FileInfo *fi;
|
||||
|
||||
fi = state.get_fi(ctx_,ffi_->fh);
|
||||
if(not fi)
|
||||
return;
|
||||
|
||||
fi->splice_state.store(FileInfo::Splice::no,std::memory_order_relaxed);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue