From 39d6ed1230abec7a81b6a6025ac61a639a2da1f0 Mon Sep 17 00:00:00 2001 From: Cong Wang Date: Sat, 7 Feb 2026 18:57:06 -0800 Subject: [PATCH] Add optional FUSE passthrough support for near-native I/O Signed-off-by: Cong Wang --- Cargo.toml | 2 +- bench/branchfs_bench.py | 9 ++- bench/quick_bench.sh | 18 +++-- src/daemon.rs | 42 ++++++++--- src/fs.rs | 149 +++++++++++++++++++++++++++++++++++----- src/main.rs | 11 +++ tests/test_helper.sh | 6 +- 7 files changed, 201 insertions(+), 36 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 91b198c..83fc1fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ keywords = ["fuse", "filesystem", "branching", "copy-on-write"] categories = ["filesystem"] [dependencies] -fuser = "0.16" +fuser = { version = "0.16", features = ["abi-7-40"] } clap = { version = "4", features = ["derive"] } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/bench/branchfs_bench.py b/bench/branchfs_bench.py index 7078bbc..6ac4bca 100755 --- a/bench/branchfs_bench.py +++ b/bench/branchfs_bench.py @@ -53,10 +53,15 @@ def _start_daemon(self, base_dir: Path, storage: Path, mountpoint: Path, log_fil env = os.environ.copy() env["RUST_LOG"] = "debug" + cmd = [self.branchfs, "mount", "--base", str(base_dir), + "--storage", str(storage)] + if os.geteuid() == 0: + cmd.append("--passthrough") + cmd.append(str(mountpoint)) + with open(log_file, "w") as f: proc = subprocess.Popen( - [self.branchfs, "mount", "--base", str(base_dir), - "--storage", str(storage), str(mountpoint)], + cmd, env=env, stdout=f, stderr=subprocess.STDOUT diff --git a/bench/quick_bench.sh b/bench/quick_bench.sh index fcc9fc2..afd31df 100755 --- a/bench/quick_bench.sh +++ b/bench/quick_bench.sh @@ -22,8 +22,16 @@ fi TMPDIR=$(mktemp -d) trap "rm -rf $TMPDIR" EXIT +PASSTHROUGH_FLAG="" +if [ "$(id -u)" = "0" ]; then + PASSTHROUGH_FLAG="--passthrough" +fi + echo "Using branchfs: $BRANCHFS" echo "Temp dir: $TMPDIR" +if [ -n "$PASSTHROUGH_FLAG" ]; then + echo "Passthrough: enabled (root)" +fi # Helper to measure time in microseconds using high-res timer time_us() { @@ -65,7 +73,7 @@ for num_files in 100 1000 10000; do done # Mount - $BRANCHFS mount --base "$BASE" --storage "$STORAGE" "$MNT" + $BRANCHFS mount --base "$BASE" --storage "$STORAGE" $PASSTHROUGH_FLAG "$MNT" sleep 0.2 # Measure branch creation (average of 5) @@ -100,7 +108,7 @@ for mod_kb in 1 10 100 1000; do STORAGE="$TMPDIR/commit_storage_$mod_kb" mkdir -p "$MNT" "$STORAGE" - $BRANCHFS mount --base "$BASE" --storage "$STORAGE" "$MNT" + $BRANCHFS mount --base "$BASE" --storage "$STORAGE" $PASSTHROUGH_FLAG "$MNT" sleep 0.1 $BRANCHFS create "bench" "$MNT" --storage "$STORAGE" @@ -128,7 +136,7 @@ for mod_kb in 1 100 1000; do STORAGE="$TMPDIR/abort_storage_$mod_kb" mkdir -p "$MNT" "$STORAGE" - $BRANCHFS mount --base "$BASE" --storage "$STORAGE" "$MNT" + $BRANCHFS mount --base "$BASE" --storage "$STORAGE" $PASSTHROUGH_FLAG "$MNT" sleep 0.1 $BRANCHFS create "bench" "$MNT" --storage "$STORAGE" @@ -158,7 +166,7 @@ mkdir -p "$MNT" "$STORAGE" # Create test file dd if=/dev/urandom of="$BASE/testfile.bin" bs=1M count=1 2>/dev/null -$BRANCHFS mount --base "$BASE" --storage "$STORAGE" "$MNT" +$BRANCHFS mount --base "$BASE" --storage "$STORAGE" $PASSTHROUGH_FLAG "$MNT" sleep 0.1 $BRANCHFS create "bench" "$MNT" --storage "$STORAGE" @@ -186,7 +194,7 @@ MNT="$TMPDIR/switch_mnt" STORAGE="$TMPDIR/switch_storage" mkdir -p "$MNT" "$STORAGE" -$BRANCHFS mount --base "$BASE" --storage "$STORAGE" "$MNT" +$BRANCHFS mount --base "$BASE" --storage "$STORAGE" $PASSTHROUGH_FLAG "$MNT" sleep 0.1 # Create multiple branches diff --git a/src/daemon.rs b/src/daemon.rs index c254377..8c45171 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -19,11 +19,26 @@ use crate::fs::BranchFs; #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "cmd", rename_all = "snake_case")] pub enum Request { - Mount { branch: String, mountpoint: String }, - Unmount { mountpoint: String }, - Create { name: String, parent: String }, - NotifySwitch { mountpoint: String, branch: String }, - GetMountBranch { mountpoint: String }, + Mount { + branch: String, + mountpoint: String, + #[serde(default)] + passthrough: bool, + }, + Unmount { + mountpoint: String, + }, + Create { + name: String, + parent: String, + }, + NotifySwitch { + mountpoint: String, + branch: String, + }, + GetMountBranch { + mountpoint: String, + }, List, Shutdown, } @@ -124,8 +139,13 @@ impl Daemon { &self.socket_path } - pub fn spawn_mount(&self, branch_name: &str, mountpoint: &Path) -> Result<()> { - let fs = BranchFs::new(self.manager.clone(), branch_name.to_string()); + pub fn spawn_mount( + &self, + branch_name: &str, + mountpoint: &Path, + passthrough: bool, + ) -> Result<()> { + let fs = BranchFs::new(self.manager.clone(), branch_name.to_string(), passthrough); let options = vec![ MountOption::FSName("branchfs".to_string()), MountOption::DefaultPermissions, @@ -284,12 +304,16 @@ impl Daemon { fn handle_request(&self, request: Request) -> Response { match request { - Request::Mount { branch, mountpoint } => { + Request::Mount { + branch, + mountpoint, + passthrough, + } => { let path = PathBuf::from(&mountpoint); if let Err(e) = fs::create_dir_all(&path) { return Response::error(&format!("Failed to create mountpoint: {}", e)); } - match self.spawn_mount(&branch, &path) { + match self.spawn_mount(&branch, &path, passthrough) { Ok(()) => Response::success(), Err(e) => Response::error(&format!("{}", e)), } diff --git a/src/fs.rs b/src/fs.rs index 6dae6d9..7b90ca8 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -8,8 +8,8 @@ use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use fuser::{ - FileType, Filesystem, ReplyAttr, ReplyData, ReplyDirectory, ReplyEmpty, ReplyEntry, ReplyIoctl, - ReplyOpen, ReplyWrite, Request, TimeOrNow, + BackingId, FileType, Filesystem, ReplyAttr, ReplyData, ReplyDirectory, ReplyEmpty, ReplyEntry, + ReplyIoctl, ReplyOpen, ReplyWrite, Request, TimeOrNow, }; use parking_lot::RwLock; @@ -129,10 +129,16 @@ pub struct BranchFs { /// Cached write fd — avoids re-open on consecutive writes to the same /// delta file (after COW). write_cache: WriteFileCache, + /// Whether FUSE passthrough mode is enabled (--passthrough flag). + passthrough_enabled: bool, + /// Monotonically increasing file handle counter for passthrough opens. + next_fh: AtomicU64, + /// BackingId objects kept alive until release() — one per passthrough open(). + backing_ids: HashMap, } impl BranchFs { - pub fn new(manager: Arc, branch_name: String) -> Self { + pub fn new(manager: Arc, branch_name: String, passthrough: bool) -> Self { let current_epoch = manager.get_epoch(); Self { manager, @@ -147,6 +153,9 @@ impl BranchFs { gid: AtomicU32::new(nix::unistd::getgid().as_raw()), open_cache: OpenFileCache::new(), write_cache: WriteFileCache::new(), + passthrough_enabled: passthrough, + next_fh: AtomicU64::new(1), + backing_ids: HashMap::new(), } } @@ -212,6 +221,64 @@ impl BranchFs { } } + /// Attempt to open a file with FUSE passthrough. Falls back to non-passthrough on failure. + fn try_open_passthrough( + &mut self, + _ino: u64, + flags: i32, + branch: &str, + rel_path: &str, + resolved: &std::path::Path, + reply: ReplyOpen, + ) { + let is_writable = (flags & libc::O_ACCMODE) != libc::O_RDONLY; + + // For writable opens, do eager COW — the kernel will write directly to + // the backing file, bypassing our write() callback. + let backing_path = if is_writable { + match self.ensure_cow_for_branch(branch, rel_path) { + Ok(p) => p, + Err(_) => { + // Fallback to non-passthrough + reply.opened(0, 0); + return; + } + } + } else { + resolved.to_path_buf() + }; + + // Open the backing file + let open_result = if is_writable { + std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(&backing_path) + } else { + File::open(&backing_path) + }; + let file = match open_result { + Ok(f) => f, + Err(_) => { + reply.opened(0, 0); + return; + } + }; + + // Register the fd with the kernel + let backing_id = match reply.open_backing(&file) { + Ok(id) => id, + Err(_) => { + reply.opened(0, 0); + return; + } + }; + + let fh = self.next_fh.fetch_add(1, Ordering::Relaxed); + reply.opened_passthrough(fh, 0, &backing_id); + self.backing_ids.insert(fh, backing_id); + } + /// Classify an inode number. Returns None for root and CTL_INO (handled separately). fn classify_ino(&self, ino: u64) -> Option { if ino == ROOT_INO { @@ -230,11 +297,7 @@ impl BranchFs { } impl Filesystem for BranchFs { - fn init( - &mut self, - req: &Request, - _config: &mut fuser::KernelConfig, - ) -> Result<(), libc::c_int> { + fn init(&mut self, req: &Request, config: &mut fuser::KernelConfig) -> Result<(), libc::c_int> { // The init request may come from the kernel (uid=0) rather than the // mounting user, so only override the process-derived defaults when // the request carries a real (non-root) uid. @@ -243,6 +306,24 @@ impl Filesystem for BranchFs { self.gid.store(req.gid(), Ordering::Relaxed); } + if self.passthrough_enabled { + if let Err(e) = config.add_capabilities(fuser::consts::FUSE_PASSTHROUGH) { + log::warn!( + "Kernel does not support FUSE_PASSTHROUGH (unsupported bits: {:#x}), disabling", + e + ); + self.passthrough_enabled = false; + } else if let Err(e) = config.set_max_stack_depth(2) { + log::warn!( + "Failed to set max_stack_depth (max: {}), disabling passthrough", + e + ); + self.passthrough_enabled = false; + } else { + log::info!("FUSE passthrough enabled"); + } + } + Ok(()) } @@ -1037,7 +1118,7 @@ impl Filesystem for BranchFs { self.unlink(_req, parent, name, reply); } - fn open(&mut self, _req: &Request, ino: u64, _flags: i32, reply: ReplyOpen) { + fn open(&mut self, _req: &Request, ino: u64, flags: i32, reply: ReplyOpen) { // Control file is always openable (no epoch check) if ino == CTL_INO { reply.opened(0, 0); @@ -1070,11 +1151,19 @@ impl Filesystem for BranchFs { reply.error(libc::ENOENT); return; } - if self.resolve_for_branch(&branch, &rel_path).is_some() { - self.manager.register_opened_inode(&branch, ino); - reply.opened(0, 0); + let resolved = match self.resolve_for_branch(&branch, &rel_path) { + Some(p) => p, + None => { + reply.error(libc::ENOENT); + return; + } + }; + self.manager.register_opened_inode(&branch, ino); + + if self.passthrough_enabled { + self.try_open_passthrough(ino, flags, &branch, &rel_path, &resolved, reply); } else { - reply.error(libc::ENOENT); + reply.opened(0, 0); } } _ => { @@ -1083,17 +1172,41 @@ impl Filesystem for BranchFs { reply.error(libc::ESTALE); return; } - if self.resolve(&path).is_some() { - self.manager - .register_opened_inode(&self.get_branch_name(), ino); - reply.opened(0, 0); + let resolved = match self.resolve(&path) { + Some(p) => p, + None => { + reply.error(libc::ENOENT); + return; + } + }; + let branch_name = self.get_branch_name(); + self.manager.register_opened_inode(&branch_name, ino); + + if self.passthrough_enabled { + self.try_open_passthrough(ino, flags, &branch_name, &path, &resolved, reply); } else { - reply.error(libc::ENOENT); + reply.opened(0, 0); } } } } + fn release( + &mut self, + _req: &Request<'_>, + _ino: u64, + fh: u64, + _flags: i32, + _lock_owner: Option, + _flush: bool, + reply: ReplyEmpty, + ) { + if fh != 0 { + self.backing_ids.remove(&fh); + } + reply.ok(); + } + fn setattr( &mut self, _req: &Request, diff --git a/src/main.rs b/src/main.rs index ab7ab68..3de8467 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,6 +27,10 @@ enum Commands { #[arg(long, default_value = "/var/lib/branchfs")] storage: PathBuf, + /// Enable FUSE passthrough for near-native I/O performance (requires root) + #[arg(long)] + passthrough: bool, + /// Mount point mountpoint: PathBuf, }, @@ -144,8 +148,14 @@ fn main() -> Result<()> { Commands::Mount { base, storage, + passthrough, mountpoint, } => { + if passthrough && nix::unistd::geteuid().as_raw() != 0 { + eprintln!("Error: --passthrough requires root (CAP_SYS_ADMIN)"); + process::exit(1); + } + std::fs::create_dir_all(&storage)?; let storage = storage.canonicalize()?; @@ -166,6 +176,7 @@ fn main() -> Result<()> { &Request::Mount { branch: "main".to_string(), mountpoint: mountpoint.to_string_lossy().to_string(), + passthrough, }, )?; diff --git a/tests/test_helper.sh b/tests/test_helper.sh index 49e44ec..83537cd 100755 --- a/tests/test_helper.sh +++ b/tests/test_helper.sh @@ -89,7 +89,11 @@ trap cleanup EXIT # Mount the filesystem do_mount() { - "$BRANCHFS" mount --base "$TEST_BASE" --storage "$TEST_STORAGE" "$TEST_MNT" + local extra_args=() + if [[ "$(id -u)" == "0" ]]; then + extra_args+=(--passthrough) + fi + "$BRANCHFS" mount --base "$TEST_BASE" --storage "$TEST_STORAGE" "${extra_args[@]}" "$TEST_MNT" sleep 0.5 # Give FUSE time to initialize }