Skip to content

Commit f5883d5

Browse files
authored
perf(virtiofs): eliminate readdir per-entry lookups (#2125)
* perf(virtiofs): eliminate readdir per-entry lookups Preserve filesystem-provided inode, type, and opaque cookie data through FUSE, VFS, mount wrappers, and overlayfs so directory scans no longer fall back to lookup plus getattr for every entry. Serialize per-open directory snapshots with seek state, implement READDIRPLUS_AUTO behavior, bound the positive lookup cache with lazy LRU eviction, and retain overlay inode identity and whiteout semantics. Extend the virtiofs benchmark with deterministic readdir datasets and request-count assertions, and add dunitest coverage for typed records, cookie resume, shared-fd concurrency, AUTO request sequencing, and overlay lower-layer scans. Signed-off-by: longjin <longjin@dragonos.org> * fix(fuse): harden readdir compatibility Preserve opaque FUSE directory cookies and raw name bytes through the VFS getdents path. Keep per-open snapshots stable across zero-cookie records and undersized userspace buffers, detect daemon offset cycles without rejecting valid records, and retry READDIR after READDIRPLUS returns ENOSYS. Guarantee RELEASEDIR after every opened-directory request or parser failure, balance non-UTF-8 READDIRPLUS lookup references with FORGET, and forward byte-oriented lookup through mount wrappers and overlay whiteout checks. Restore bounded positive-cache expiry cleanup without scanning or perturbing live LRU entries. Correct the local test daemon's root-child enumeration and populate READDIRPLUS validity fields so the tests exercise real Linux-compatible cache behavior. Validated with make fmt, make kernel, FuseCore 5/5, and FuseExtended 70/70 in a DragonOS QEMU guest. Signed-off-by: longjin <longjin@dragonos.org> * fix(vfs): resume rebuilt directory snapshots Honor a nonzero saved directory cookie when getdents rebuilds a snapshot after rewind. Use one cookie-to-index path for both lseek against an existing snapshot and deferred positioning after a new snapshot is populated. Keep zero-cookie streams distinct from an explicit rewind, and preserve the selected entry across an undersized getdents buffer. Extend the FUSE typed-directory regression with rewind, saved-cookie seek, EINVAL retry, and resume assertions. Validated with make fmt, make kernel, FuseCore 5/5, and FuseExtended 70/70 in a DragonOS QEMU guest. Signed-off-by: longjin <longjin@dragonos.org> * fix(vfs): preserve directory cursor semantics Treat SEEK_CUR with a zero delta as a serialized position query so zero and repeated opaque cookies cannot rewind a live directory snapshot. Reject cookies outside the signed getdents64 and lseek range before emitting a record or advancing file state. Make native typed directory records an explicit optional capability. FUSE and overlay keep their typed fast path, while legacy filesystems cache names and resolve metadata only as the caller buffer advances; overlay materializes legacy metadata only when a full layer merge requires it. Balance READDIRPLUS lookup references when a complete name is followed by truncated alignment padding without changing Linux short-record behavior. Extend coverage for zero and duplicate cookies, SEEK_CUR queries, signed-cookie bounds, and truncated lookup accounting. Validated with make fmt, make kernel, FuseCore 5/5, FuseExtended 70/70, and DevtmpfsSemantics 7/7 in a DragonOS QEMU guest. Signed-off-by: longjin <longjin@dragonos.org> * fix(virtiofs): report readdir pre-scan failures Ensure readdir_scan always terminates its transcript with a machine-readable result when pre-scan quiescence times out or the statistics baseline is unavailable. Preserve the first failure while closing the dataset directory and report ETIMEDOUT or EIO explicitly. Extend the host transcript regression suite to exercise both failure paths and verify their single-result contract. Signed-off-by: longjin <longjin@dragonos.org> * fix(fuse): synchronize shared readdir test workers Replace volatile start, stop, and readiness flags in the shared-directory readdir regression with C++ atomics. Use release stores and acquire loads so worker startup and shutdown have defined happens-before relationships on optimized and weakly ordered targets. Keep worker result fields non-atomic because they are consumed only after pthread_join, which provides the required completion synchronization. Signed-off-by: longjin <longjin@dragonos.org> --------- Signed-off-by: longjin <longjin@dragonos.org>
1 parent fcfc8eb commit f5883d5

13 files changed

Lines changed: 2075 additions & 377 deletions

File tree

kernel/src/filesystem/fuse/conn.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1362,6 +1362,13 @@ impl FuseConn {
13621362
!g.no_readdirplus && (g.init.flags & FUSE_DO_READDIRPLUS) != 0
13631363
}
13641364

1365+
pub fn readdirplus_auto(&self) -> bool {
1366+
let g = self.inner.lock();
1367+
!g.no_readdirplus
1368+
&& (g.init.flags & (FUSE_DO_READDIRPLUS | FUSE_READDIRPLUS_AUTO))
1369+
== (FUSE_DO_READDIRPLUS | FUSE_READDIRPLUS_AUTO)
1370+
}
1371+
13651372
pub fn disable_readdirplus(&self) {
13661373
let mut g = self.inner.lock();
13671374
g.no_readdirplus = true;

kernel/src/filesystem/fuse/inode.rs

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ use alloc::{
88
sync::{Arc, Weak},
99
vec::Vec,
1010
};
11-
use core::mem::size_of;
1211
use core::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering};
12+
use core::{mem::size_of, num::NonZeroUsize};
13+
use lru::LruCache;
1314

1415
use system_error::SystemError;
1516

@@ -287,7 +288,11 @@ pub struct FuseNode {
287288
cached_metadata: Mutex<Option<Metadata>>,
288289
page_cache: Mutex<Option<Arc<PageCache>>>,
289290
writeback_handles: Mutex<Vec<Arc<FuseWritebackHandle>>>,
290-
lookup_cache: Mutex<BTreeMap<String, FuseLookupCacheEntry>>,
291+
lookup_cache: Mutex<LruCache<String, FuseLookupCacheEntry>>,
292+
/// Linux FUSE_I_INIT_RDPLUS equivalent for the first cache use of a child.
293+
readdirplus_init: AtomicBool,
294+
/// Linux FUSE_I_ADVISE_RDPLUS equivalent for the next directory batch.
295+
readdirplus_advised: AtomicBool,
291296
/// Serializes dirty-page admission against operations which must drain and
292297
/// invalidate the page cache (truncate and direct I/O).
293298
writeback_barrier: RwSem<()>,
@@ -330,6 +335,7 @@ struct FuseLookupCacheEntry {
330335
impl FuseNode {
331336
const FUSE_DIRENT_ALIGN: usize = 8;
332337
const LOOKUP_CACHE_MAX_ENTRIES: usize = 1024;
338+
const LOOKUP_CACHE_PRUNE_BUDGET: usize = 64;
333339
const READDIR_BUFFER_SIZE: usize = 64 * 1024;
334340
const XATTR_SIZE_MAX: usize = 65536;
335341
const XATTR_LIST_MAX: usize = 65536;
@@ -370,7 +376,11 @@ impl FuseNode {
370376
cached_metadata: Mutex::new(cached),
371377
page_cache: Mutex::new(None),
372378
writeback_handles: Mutex::new(Vec::new()),
373-
lookup_cache: Mutex::new(BTreeMap::new()),
379+
lookup_cache: Mutex::new(LruCache::new(
380+
NonZeroUsize::new(Self::LOOKUP_CACHE_MAX_ENTRIES).unwrap(),
381+
)),
382+
readdirplus_init: AtomicBool::new(false),
383+
readdirplus_advised: AtomicBool::new(false),
374384
writeback_barrier: RwSem::new(()),
375385
dax_reclaim_serial: Mutex::new(()),
376386
dax_mappings: RwSem::new(DaxMappingTree::default()),
@@ -1174,10 +1184,6 @@ impl FuseNode {
11741184
*self.name.lock() = Some(name.to_string());
11751185
}
11761186

1177-
pub(crate) fn has_dname(&self, name: &str) -> bool {
1178-
self.name.lock().as_deref() == Some(name)
1179-
}
1180-
11811187
pub(crate) fn clear_dname_if(&self, name: &str) {
11821188
let mut dname = self.name.lock();
11831189
if dname.as_deref() == Some(name) {
@@ -1374,21 +1380,21 @@ impl FuseNode {
13741380
self.fs.upgrade()
13751381
}
13761382

1377-
pub(crate) fn parent_fuse_nodeid(&self) -> u64 {
1378-
*self.parent_nodeid.lock()
1379-
}
1380-
13811383
fn request_name(&self, opcode: u32, nodeid: u64, name: &str) -> Result<FuseReply, SystemError> {
1382-
self.check_not_stale()?;
1383-
let payload = Self::pack_name_payload(name);
1384-
self.conn().request(opcode, nodeid, &payload)
1384+
self.request_name_bytes(opcode, nodeid, name.as_bytes())
13851385
}
13861386

1387-
fn pack_name_payload(name: &str) -> Vec<u8> {
1387+
fn request_name_bytes(
1388+
&self,
1389+
opcode: u32,
1390+
nodeid: u64,
1391+
name: &[u8],
1392+
) -> Result<FuseReply, SystemError> {
1393+
self.check_not_stale()?;
13881394
let mut payload = Vec::with_capacity(name.len() + 1);
1389-
payload.extend_from_slice(name.as_bytes());
1395+
payload.extend_from_slice(name);
13901396
payload.push(0);
1391-
payload
1397+
self.conn().request(opcode, nodeid, &payload)
13921398
}
13931399

13941400
fn pack_struct_and_name_payload<T: Copy>(inarg: &T, name: &str) -> Vec<u8> {

0 commit comments

Comments
 (0)