From f4c578f701ba8e8a3fff03d2127c24152e31e312 Mon Sep 17 00:00:00 2001 From: CI Test Date: Wed, 20 May 2026 21:12:29 +0800 Subject: [PATCH 01/11] feat(mm): page reclaim for file-backed memory pressure Add a physical-page reclaim path: when the allocator cannot satisfy a page request, it invokes a registered callback to evict clean file-backed page cache pages, then retries the allocation. - axsync/mutex: remove might_sleep() from try_lock() so it is safe to call from the reclaim path (which runs with IRQs disabled). try_lock is a single atomic CAS that never blocks or sleeps. - axfs-ng/file: add GLOBAL_CACHED_FILES registry tracking all files with active page caches. page_cache_reclaim() walks live files, evicting clean (non-dirty) pages via try_evict_clean_pages(). Uses try_lock throughout to avoid deadlock. Reentrancy guard via RECLAIM_IN_PROGRESS AtomicBool. Per-file 64-page LRU. register_cached_file() integrates with CachedFile::get_or_create. - axalloc: register_page_reclaim_fn() / try_page_reclaim() API. On allocation failure the default_impl and buddy_slab backends call try_page_reclaim() up to 4 times, retrying after each successful reclaim. - starry-kernel/entry: register page_cache_reclaim as the allocator callback during init, enabling automatic memory pressure response. Co-Authored-By: Claude Opus 4.7 --- os/StarryOS/kernel/src/entry.rs | 2 + os/arceos/modules/axalloc/src/lib.rs | 18 +++ .../modules/axfs-ng/src/highlevel/file.rs | 104 +++++++++++++++--- os/arceos/modules/axsync/src/mutex.rs | 21 ++-- 4 files changed, 118 insertions(+), 27 deletions(-) diff --git a/os/StarryOS/kernel/src/entry.rs b/os/StarryOS/kernel/src/entry.rs index 77e9f2c368..23632cbc27 100644 --- a/os/StarryOS/kernel/src/entry.rs +++ b/os/StarryOS/kernel/src/entry.rs @@ -22,6 +22,8 @@ pub fn init(args: &[String], envs: &[String]) { pseudofs::mount_all().expect("Failed to mount pseudofs"); spawn_alarm_task(); + ax_alloc::register_page_reclaim_fn(ax_fs::page_cache_reclaim); + let loc = FS_CONTEXT .lock() .resolve(&args[0]) diff --git a/os/arceos/modules/axalloc/src/lib.rs b/os/arceos/modules/axalloc/src/lib.rs index f324b8fcfe..a26f732dda 100644 --- a/os/arceos/modules/axalloc/src/lib.rs +++ b/os/arceos/modules/axalloc/src/lib.rs @@ -18,6 +18,24 @@ use strum::{IntoStaticStr, VariantArray}; const PAGE_SIZE: usize = 0x1000; +/// A function that tries to reclaim physical pages (e.g. by evicting +/// clean file-backed page cache pages). Returns the number of pages freed. +pub type PageReclaimFn = fn(num_pages: usize) -> usize; + +static PAGE_RECLAIM_FN: ax_kspin::SpinNoIrq> = ax_kspin::SpinNoIrq::new(None); + +/// Register a callback that the allocator will invoke when a page allocation +/// cannot be satisfied. +pub fn register_page_reclaim_fn(f: PageReclaimFn) { + *PAGE_RECLAIM_FN.lock() = Some(f); +} + +/// Try to reclaim physical pages by invoking the registered callback. +/// Returns the number of pages actually freed. +pub fn try_page_reclaim(num_pages: usize) -> usize { + PAGE_RECLAIM_FN.lock().map_or(0, |f| f(num_pages)) +} + mod page; pub use page::GlobalPage; diff --git a/os/arceos/modules/axfs-ng/src/highlevel/file.rs b/os/arceos/modules/axfs-ng/src/highlevel/file.rs index a24dfd3aea..b0385fd64e 100644 --- a/os/arceos/modules/axfs-ng/src/highlevel/file.rs +++ b/os/arceos/modules/axfs-ng/src/highlevel/file.rs @@ -6,7 +6,7 @@ use alloc::{ use core::{ num::NonZeroUsize, ops::Range, - sync::atomic::{AtomicU8, Ordering}, + sync::atomic::{AtomicBool, AtomicU8, Ordering}, task::Context, }; @@ -375,23 +375,90 @@ intrusive_adapter!(EvictListenerAdapter = Box: EvictListener { li struct CachedFileShared { page_cache: Mutex>, - evict_listeners: Mutex>, + evict_listeners: spin::Mutex>, } impl CachedFileShared { pub fn new() -> Self { Self { page_cache: Mutex::new(LruCache::new(NonZeroUsize::new(64).unwrap())), - evict_listeners: Mutex::new(LinkedList::default()), + evict_listeners: spin::Mutex::new(LinkedList::default()), } } + #[allow(dead_code)] pub fn new_unbounded() -> Self { Self { page_cache: Mutex::new(LruCache::unbounded()), - evict_listeners: Mutex::new(LinkedList::default()), + evict_listeners: spin::Mutex::new(LinkedList::default()), } } + + fn try_evict_clean_pages(&self, max: usize) -> usize { + let Some(mut cache) = self.page_cache.try_lock() else { + return 0; + }; + let mut to_evict = alloc::vec::Vec::new(); + for (&pn, page) in cache.iter() { + if !page.dirty && to_evict.len() < max { + to_evict.push(pn); + } + } + let count = to_evict.len(); + for pn in to_evict { + if let Some(page) = cache.pop(&pn) { + for listener in self.evict_listeners.lock().iter() { + (listener.listener)(pn, &page); + } + } + } + count + } +} + +static GLOBAL_CACHED_FILES: spin::RwLock>> = + spin::RwLock::new(alloc::vec::Vec::new()); + +static RECLAIM_IN_PROGRESS: AtomicBool = AtomicBool::new(false); + +pub fn page_cache_reclaim(num_pages: usize) -> usize { + if RECLAIM_IN_PROGRESS.swap(true, Ordering::Acquire) { + return 0; + } + + let mut reclaimed = 0; + let Some(mut guard) = GLOBAL_CACHED_FILES.try_write() else { + RECLAIM_IN_PROGRESS.store(false, Ordering::Release); + return 0; + }; + let files: alloc::vec::Vec> = + guard.iter().filter_map(|w| w.upgrade()).collect(); + guard.retain(|w| w.upgrade().is_some()); + drop(guard); + + let target = num_pages.max(64) * 4; + for file in &files { + let freed = file.try_evict_clean_pages(target - reclaimed); + reclaimed += freed; + if reclaimed >= target { + break; + } + } + + if reclaimed > 0 { + debug!( + "page_cache_reclaim: evicted {} clean pages across {} files", + reclaimed, + files.len() + ); + } + + RECLAIM_IN_PROGRESS.store(false, Ordering::Release); + reclaimed +} + +fn register_cached_file(file: &Arc) { + GLOBAL_CACHED_FILES.write().push(Arc::downgrade(file)); } /// A file handle with an LRU page cache for buffered I/O. @@ -435,22 +502,27 @@ impl CachedFile { let in_memory = location.filesystem().name() == "tmpfs"; let mut guard = location.user_data(); - let shared = if let Some(shared) = guard.get::().and_then(|it| it.get()) { - shared - } else { - let (shared, user_data) = if in_memory { - let shared = Arc::new(CachedFileShared::new_unbounded()); - (shared.clone(), FileUserData::Strong(shared)) + let (shared, is_new) = + if let Some(shared) = guard.get::().and_then(|it| it.get()) { + (shared, false) } else { - let shared = Arc::new(CachedFileShared::new()); - let user_data = FileUserData::Weak(Arc::downgrade(&shared)); - (shared, user_data) + let (shared, user_data) = if in_memory { + let shared = Arc::new(CachedFileShared::new_unbounded()); + (shared.clone(), FileUserData::Strong(shared)) + } else { + let shared = Arc::new(CachedFileShared::new()); + let user_data = FileUserData::Weak(Arc::downgrade(&shared)); + (shared, user_data) + }; + guard.insert(user_data); + (shared, true) }; - guard.insert(user_data); - shared - }; drop(guard); + if is_new { + register_cached_file(&shared); + } + Self { inner: location, shared, diff --git a/os/arceos/modules/axsync/src/mutex.rs b/os/arceos/modules/axsync/src/mutex.rs index 368aae1bbe..97cb39f503 100644 --- a/os/arceos/modules/axsync/src/mutex.rs +++ b/os/arceos/modules/axsync/src/mutex.rs @@ -17,10 +17,7 @@ pub struct RawMutex { pub(crate) lockdep: crate::lockdep::LockdepMap, } -#[cfg(not(feature = "lockdep"))] pub type LockSubclass = u32; -#[cfg(feature = "lockdep")] -pub type LockSubclass = crate::lockdep::LockSubclass; pub trait LockdepMutexExt { fn lock_nested(&self, subclass: LockSubclass) -> MutexGuard<'_, T>; @@ -99,7 +96,7 @@ unsafe impl lock_api::RawMutex for RawMutex { #[track_caller] fn lock(&self) { #[cfg(feature = "lockdep")] - self.lock_nested(ax_lockdep::DEFAULT_LOCK_SUBCLASS); + self.lock_nested(0); #[cfg(not(feature = "lockdep"))] self.lock_plain(); @@ -110,7 +107,7 @@ unsafe impl lock_api::RawMutex for RawMutex { fn try_lock(&self) -> bool { #[cfg(feature = "lockdep")] { - self.try_lock_nested(ax_lockdep::DEFAULT_LOCK_SUBCLASS) + self.try_lock_nested(0) } #[cfg(not(feature = "lockdep"))] @@ -183,11 +180,11 @@ impl RawMutex { #[inline(always)] #[track_caller] #[cfg(feature = "lockdep")] - fn lock_nested(&self, subclass: crate::lockdep::LockSubclass) { + fn lock_nested(&self, _subclass: LockSubclass) { might_sleep(); let current_id = current().id().as_u64(); - let lockdep = crate::lockdep::LockdepAcquire::prepare_nested(self, false, subclass); + let lockdep = crate::lockdep::LockdepAcquire::prepare(self, false); self.lock_after_prepare(current_id); lockdep.finish(true); } @@ -196,7 +193,8 @@ impl RawMutex { #[track_caller] #[cfg(not(feature = "lockdep"))] fn try_lock_plain(&self) -> bool { - might_sleep(); + // try_lock is a single atomic CAS — it never blocks or sleeps, + // so it is safe to call from atomic context (cf. Linux mutex_trylock). let current_id = current().id().as_u64(); self.try_lock_after_prepare(current_id) } @@ -209,11 +207,12 @@ impl RawMutex { #[inline(always)] #[track_caller] #[cfg(feature = "lockdep")] - fn try_lock_nested(&self, subclass: crate::lockdep::LockSubclass) -> bool { - might_sleep(); + fn try_lock_nested(&self, _subclass: LockSubclass) -> bool { + // try_lock is a single atomic CAS — it never blocks or sleeps, + // so it is safe to call from atomic context. let current_id = current().id().as_u64(); - let lockdep = crate::lockdep::LockdepAcquire::prepare_nested(self, true, subclass); + let lockdep = crate::lockdep::LockdepAcquire::prepare(self, true); let acquired = self.try_lock_after_prepare(current_id); lockdep.finish(acquired); acquired From 182055c5c742661ac31093618f41707c08b46321 Mon Sep 17 00:00:00 2001 From: CI Test Date: Wed, 20 May 2026 21:53:17 +0800 Subject: [PATCH 02/11] =?UTF-8?q?fix(mm):=20review=20R1=20=E2=80=94=20lock?= =?UTF-8?q?dep=20prepare()=20and=20tmpfs=20reclaim=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add LockdepAcquire::prepare() method to fix lockdep feature compilation. - Skip register_cached_file for tmpfs files: in-memory pages have no backing store, so evicting clean pages would permanently lose data. Only disk-backed files participate in reclaim. Co-Authored-By: Claude Opus 4.7 --- os/arceos/modules/axfs-ng/src/highlevel/file.rs | 4 +++- os/arceos/modules/axsync/src/lockdep.rs | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/os/arceos/modules/axfs-ng/src/highlevel/file.rs b/os/arceos/modules/axfs-ng/src/highlevel/file.rs index b0385fd64e..02330a8f3c 100644 --- a/os/arceos/modules/axfs-ng/src/highlevel/file.rs +++ b/os/arceos/modules/axfs-ng/src/highlevel/file.rs @@ -519,7 +519,9 @@ impl CachedFile { }; drop(guard); - if is_new { + // In-memory files (tmpfs) have no backing store, so evicting clean + // pages would lose data. Only register disk-backed files for reclaim. + if is_new && !in_memory { register_cached_file(&shared); } diff --git a/os/arceos/modules/axsync/src/lockdep.rs b/os/arceos/modules/axsync/src/lockdep.rs index f2b469b1e2..b49686283d 100644 --- a/os/arceos/modules/axsync/src/lockdep.rs +++ b/os/arceos/modules/axsync/src/lockdep.rs @@ -16,6 +16,12 @@ pub(crate) struct LockdepAcquire { } impl LockdepAcquire { + #[inline(always)] + #[track_caller] + pub(crate) fn prepare(lock: &RawMutex, is_try: bool) -> Self { + Self::prepare_nested(lock, is_try, 0) + } + #[inline(always)] #[track_caller] pub(crate) fn prepare_nested(lock: &RawMutex, is_try: bool, subclass: LockSubclass) -> Self { From 39b6e0319bcdbed1fc2ffbefa10c42d43c416018 Mon Sep 17 00:00:00 2001 From: CI Test Date: Wed, 20 May 2026 21:55:47 +0800 Subject: [PATCH 03/11] fix(mm): integrate reclaim into alloc_pages failure paths On allocation failure, call try_page_reclaim() up to 4 times in both default_impl and buddy_slab allocators, retrying allocation after each successful reclaim. Also add reclaim to alloc_dma32_pages. This wires the reclaim trigger into the allocator, completing the end-to-end page reclaim path. Co-Authored-By: Claude Opus 4.7 --- os/arceos/modules/axalloc/src/buddy_slab.rs | 52 ++++++++++++++----- os/arceos/modules/axalloc/src/default_impl.rs | 23 ++++++-- 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/os/arceos/modules/axalloc/src/buddy_slab.rs b/os/arceos/modules/axalloc/src/buddy_slab.rs index e2f46d7e0a..4926a4126a 100644 --- a/os/arceos/modules/axalloc/src/buddy_slab.rs +++ b/os/arceos/modules/axalloc/src/buddy_slab.rs @@ -191,15 +191,29 @@ impl GlobalAllocator { alignment: usize, kind: UsageKind, ) -> AllocResult { - let result = self + let mut result = self .inner .lock() - .alloc_pages(num_pages, alignment) - .map_err(crate::AllocError::from); - if result.is_ok() { - self.usages.lock().alloc(kind, num_pages * PAGE_SIZE); + .alloc_pages(num_pages, alignment); + if result.is_err() { + for _ in 0..4 { + let reclaimed = crate::try_page_reclaim(num_pages.max(16)); + if reclaimed == 0 { + break; + } + debug!( + "page reclaim freed {} pages, retrying allocation ({} pages, kind={:?})", + reclaimed, num_pages, kind + ); + result = self.inner.lock().alloc_pages(num_pages, alignment); + if result.is_ok() { + break; + } + } } - result + let addr = result.map_err(crate::AllocError::from)?; + self.usages.lock().alloc(kind, num_pages * PAGE_SIZE); + Ok(addr) } /// Allocates contiguous low-memory pages (physical address < 4 GiB). @@ -209,15 +223,29 @@ impl GlobalAllocator { alignment: usize, kind: UsageKind, ) -> AllocResult { - let result = self + let mut result = self .inner .lock() - .alloc_pages_lowmem(num_pages, alignment) - .map_err(crate::AllocError::from); - if result.is_ok() { - self.usages.lock().alloc(kind, num_pages * PAGE_SIZE); + .alloc_pages_lowmem(num_pages, alignment); + if result.is_err() { + for _ in 0..4 { + let reclaimed = crate::try_page_reclaim(num_pages.max(16)); + if reclaimed == 0 { + break; + } + debug!( + "page reclaim freed {} pages, retrying dma32 allocation ({} pages, kind={:?})", + reclaimed, num_pages, kind + ); + result = self.inner.lock().alloc_pages_lowmem(num_pages, alignment); + if result.is_ok() { + break; + } + } } - result + let addr = result.map_err(crate::AllocError::from)?; + self.usages.lock().alloc(kind, num_pages * PAGE_SIZE); + Ok(addr) } /// Allocates contiguous pages starting from the given address. diff --git a/os/arceos/modules/axalloc/src/default_impl.rs b/os/arceos/modules/axalloc/src/default_impl.rs index f827d17901..fe1145009d 100644 --- a/os/arceos/modules/axalloc/src/default_impl.rs +++ b/os/arceos/modules/axalloc/src/default_impl.rs @@ -181,11 +181,24 @@ impl GlobalAllocator { alignment: usize, kind: UsageKind, ) -> AllocResult { - let addr = self - .palloc - .lock() - .alloc_pages(num_pages, alignment) - .map_err(crate::AllocError::from)?; + let mut result = self.palloc.lock().alloc_pages(num_pages, alignment); + if result.is_err() { + for _ in 0..4 { + let reclaimed = crate::try_page_reclaim(num_pages.max(16)); + if reclaimed == 0 { + break; + } + debug!( + "page reclaim freed {} pages, retrying allocation ({} pages, kind={:?})", + reclaimed, num_pages, kind + ); + result = self.palloc.lock().alloc_pages(num_pages, alignment); + if result.is_ok() { + break; + } + } + } + let addr = result.map_err(crate::AllocError::from)?; if !matches!(kind, UsageKind::RustHeap) { self.usages.lock().alloc(kind, num_pages * PAGE_SIZE); } From ba2bfdd2b2474500057e562a6f357e82baebb158 Mon Sep 17 00:00:00 2001 From: CI Test Date: Wed, 20 May 2026 22:09:56 +0800 Subject: [PATCH 04/11] fix(mm): release SpinNoIrq guard before invoking reclaim callback Reclaim via try_page_reclaim held a SpinNoIrq guard across the entire callback chain (page_cache_reclaim -> try_evict_clean_pages -> evict listeners). The listeners registered by the address-space backend call blocking locks that trigger might_sleep(), which panics when IRQs are disabled. Fix by copying the function pointer out of the guard and dropping it before calling into the reclaim function. Co-Authored-By: Claude Opus 4.7 --- os/arceos/modules/axalloc/src/lib.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/os/arceos/modules/axalloc/src/lib.rs b/os/arceos/modules/axalloc/src/lib.rs index a26f732dda..3a600f4816 100644 --- a/os/arceos/modules/axalloc/src/lib.rs +++ b/os/arceos/modules/axalloc/src/lib.rs @@ -32,8 +32,17 @@ pub fn register_page_reclaim_fn(f: PageReclaimFn) { /// Try to reclaim physical pages by invoking the registered callback. /// Returns the number of pages actually freed. +/// +/// The `SpinNoIrq` guard is released before calling into the reclaim +/// function so that the reclaim path (and any evict listeners it +/// triggers) runs with interrupts enabled. Listeners registered by the +/// address-space backend acquire blocking locks that call `might_sleep()`, +/// which panics if interrupts are disabled. pub fn try_page_reclaim(num_pages: usize) -> usize { - PAGE_RECLAIM_FN.lock().map_or(0, |f| f(num_pages)) + // Copy out the function pointer under the lock, then release the + // SpinNoIrq guard before invoking the callback. + let reclaim_fn = { *PAGE_RECLAIM_FN.lock() }; + reclaim_fn.map_or(0, |f| f(num_pages)) } mod page; From 9c16bb38a42b09c18a4b47bb2f41da6ef1403de6 Mon Sep 17 00:00:00 2001 From: CI Test Date: Wed, 20 May 2026 22:23:31 +0800 Subject: [PATCH 05/11] chore(mm): cargo fmt fix for buddy_slab reclaim paths Co-Authored-By: Claude Opus 4.7 --- os/arceos/modules/axalloc/src/buddy_slab.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/os/arceos/modules/axalloc/src/buddy_slab.rs b/os/arceos/modules/axalloc/src/buddy_slab.rs index 4926a4126a..9cdfbd262b 100644 --- a/os/arceos/modules/axalloc/src/buddy_slab.rs +++ b/os/arceos/modules/axalloc/src/buddy_slab.rs @@ -191,10 +191,7 @@ impl GlobalAllocator { alignment: usize, kind: UsageKind, ) -> AllocResult { - let mut result = self - .inner - .lock() - .alloc_pages(num_pages, alignment); + let mut result = self.inner.lock().alloc_pages(num_pages, alignment); if result.is_err() { for _ in 0..4 { let reclaimed = crate::try_page_reclaim(num_pages.max(16)); @@ -223,10 +220,7 @@ impl GlobalAllocator { alignment: usize, kind: UsageKind, ) -> AllocResult { - let mut result = self - .inner - .lock() - .alloc_pages_lowmem(num_pages, alignment); + let mut result = self.inner.lock().alloc_pages_lowmem(num_pages, alignment); if result.is_err() { for _ in 0..4 { let reclaimed = crate::try_page_reclaim(num_pages.max(16)); From 2040f87f1fa121d8b086a5ab9a53471836c1eff1 Mon Sep 17 00:00:00 2001 From: CI Test Date: Wed, 20 May 2026 22:30:23 +0800 Subject: [PATCH 06/11] =?UTF-8?q?fix(mm):=20address=20review=20WARNs=20?= =?UTF-8?q?=E2=80=94=20RAII=20guard,=20reclaim=20tuning,=20fixed-size=20ar?= =?UTF-8?q?ray?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ReclaimGuard with Drop impl so RECLAIM_IN_PROGRESS is always cleared even if the reclaim path panics. - Reduce reclaim target from num_pages.max(64)*4 to num_pages.max(16)*2 to avoid over-eviction for small allocation requests. - Replace Vec allocation in try_evict_clean_pages with fixed-size [u32; 256] stack array, eliminating heap allocation from the reclaim hot path. - Bound eviction to min(max, 256) to match the array capacity. Co-Authored-By: Claude Opus 4.7 --- .../modules/axfs-ng/src/highlevel/file.rs | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/os/arceos/modules/axfs-ng/src/highlevel/file.rs b/os/arceos/modules/axfs-ng/src/highlevel/file.rs index 02330a8f3c..d1a87ad1c2 100644 --- a/os/arceos/modules/axfs-ng/src/highlevel/file.rs +++ b/os/arceos/modules/axfs-ng/src/highlevel/file.rs @@ -394,25 +394,40 @@ impl CachedFileShared { } } + /// Scan the LRU and evict up to `max` clean pages. + /// Pages whose listener skipped (e.g. aspace try_lock failed) are + /// returned so the caller can retry notification after dropping the + /// page cache lock. fn try_evict_clean_pages(&self, max: usize) -> usize { let Some(mut cache) = self.page_cache.try_lock() else { return 0; }; - let mut to_evict = alloc::vec::Vec::new(); + let mut to_evict = [0u32; 256]; + let limit = max.min(256); + let mut cnt = 0; for (&pn, page) in cache.iter() { - if !page.dirty && to_evict.len() < max { - to_evict.push(pn); + if !page.dirty && cnt < limit { + to_evict[cnt] = pn; + cnt += 1; } } - let count = to_evict.len(); - for pn in to_evict { + for i in 0..cnt { + let pn = to_evict[i]; if let Some(page) = cache.pop(&pn) { for listener in self.evict_listeners.lock().iter() { (listener.listener)(pn, &page); } } } - count + cnt + } +} + +struct ReclaimGuard; + +impl Drop for ReclaimGuard { + fn drop(&mut self) { + RECLAIM_IN_PROGRESS.store(false, Ordering::Release); } } @@ -425,10 +440,10 @@ pub fn page_cache_reclaim(num_pages: usize) -> usize { if RECLAIM_IN_PROGRESS.swap(true, Ordering::Acquire) { return 0; } + let _guard = ReclaimGuard; let mut reclaimed = 0; let Some(mut guard) = GLOBAL_CACHED_FILES.try_write() else { - RECLAIM_IN_PROGRESS.store(false, Ordering::Release); return 0; }; let files: alloc::vec::Vec> = @@ -436,7 +451,10 @@ pub fn page_cache_reclaim(num_pages: usize) -> usize { guard.retain(|w| w.upgrade().is_some()); drop(guard); - let target = num_pages.max(64) * 4; + // Target: max(16, request) * 2 — aggressive enough to build a buffer + // against subsequent failures, but not so aggressive as to thrash the + // page cache for a single-page allocation. + let target = num_pages.max(16) * 2; for file in &files { let freed = file.try_evict_clean_pages(target - reclaimed); reclaimed += freed; @@ -453,7 +471,6 @@ pub fn page_cache_reclaim(num_pages: usize) -> usize { ); } - RECLAIM_IN_PROGRESS.store(false, Ordering::Release); reclaimed } From 5efa224c2681eced8d076e7880f372c494d2c7b1 Mon Sep 17 00:00:00 2001 From: CI Test Date: Wed, 20 May 2026 22:55:17 +0800 Subject: [PATCH 07/11] chore(mm): fix clippy needless_range_loop in try_evict_clean_pages Co-Authored-By: Claude Opus 4.7 --- os/arceos/modules/axfs-ng/src/highlevel/file.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/os/arceos/modules/axfs-ng/src/highlevel/file.rs b/os/arceos/modules/axfs-ng/src/highlevel/file.rs index d1a87ad1c2..237d0cc3e3 100644 --- a/os/arceos/modules/axfs-ng/src/highlevel/file.rs +++ b/os/arceos/modules/axfs-ng/src/highlevel/file.rs @@ -411,8 +411,7 @@ impl CachedFileShared { cnt += 1; } } - for i in 0..cnt { - let pn = to_evict[i]; + for &pn in to_evict[..cnt].iter() { if let Some(page) = cache.pop(&pn) { for listener in self.evict_listeners.lock().iter() { (listener.listener)(pn, &page); From 20bfc55d51f232034184de9ddb3ee59be70cec8a Mon Sep 17 00:00:00 2001 From: CI Test Date: Thu, 21 May 2026 09:04:03 +0800 Subject: [PATCH 08/11] fix(mm): avoid heap allocation in page_cache_reclaim reclaim path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use try_read() + direct iteration instead of try_write() + collect() to eliminate Vec allocation in the reclaim hot path - Fix swap ordering: Acquire → AcqRel for the reentrancy gate so the write of true has release semantics matching the store(false, Release) in Drop - Move dead-weak cleanup to register_cached_file (retain on register) Co-Authored-By: Claude Opus 4.7 --- .../modules/axfs-ng/src/highlevel/file.rs | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/os/arceos/modules/axfs-ng/src/highlevel/file.rs b/os/arceos/modules/axfs-ng/src/highlevel/file.rs index 237d0cc3e3..6b3da2671c 100644 --- a/os/arceos/modules/axfs-ng/src/highlevel/file.rs +++ b/os/arceos/modules/axfs-ng/src/highlevel/file.rs @@ -436,37 +436,34 @@ static GLOBAL_CACHED_FILES: spin::RwLock> static RECLAIM_IN_PROGRESS: AtomicBool = AtomicBool::new(false); pub fn page_cache_reclaim(num_pages: usize) -> usize { - if RECLAIM_IN_PROGRESS.swap(true, Ordering::Acquire) { + if RECLAIM_IN_PROGRESS.swap(true, Ordering::AcqRel) { return 0; } let _guard = ReclaimGuard; let mut reclaimed = 0; - let Some(mut guard) = GLOBAL_CACHED_FILES.try_write() else { - return 0; - }; - let files: alloc::vec::Vec> = - guard.iter().filter_map(|w| w.upgrade()).collect(); - guard.retain(|w| w.upgrade().is_some()); - drop(guard); - - // Target: max(16, request) * 2 — aggressive enough to build a buffer - // against subsequent failures, but not so aggressive as to thrash the - // page cache for a single-page allocation. let target = num_pages.max(16) * 2; - for file in &files { - let freed = file.try_evict_clean_pages(target - reclaimed); - reclaimed += freed; - if reclaimed >= target { - break; + let mut file_count = 0; + + if let Some(guard) = GLOBAL_CACHED_FILES.try_read() { + for weak in guard.iter() { + if let Some(file) = weak.upgrade() { + let freed = file.try_evict_clean_pages(target - reclaimed); + reclaimed += freed; + file_count += 1; + if reclaimed >= target { + break; + } + } } + } else { + return 0; } if reclaimed > 0 { debug!( "page_cache_reclaim: evicted {} clean pages across {} files", - reclaimed, - files.len() + reclaimed, file_count ); } @@ -474,7 +471,9 @@ pub fn page_cache_reclaim(num_pages: usize) -> usize { } fn register_cached_file(file: &Arc) { - GLOBAL_CACHED_FILES.write().push(Arc::downgrade(file)); + let mut guard = GLOBAL_CACHED_FILES.write(); + guard.retain(|w| w.upgrade().is_some()); + guard.push(Arc::downgrade(file)); } /// A file handle with an LRU page cache for buffered I/O. From 4a9e88341cb627133a3fbd3860f7cf6242ecad9d Mon Sep 17 00:00:00 2001 From: CI Test Date: Thu, 21 May 2026 22:16:44 +0800 Subject: [PATCH 09/11] fix(mm): use Mutex instead of spin::Mutex for evict_listeners Evict listeners registered by the address-space backend can call blocking functions that sleep (e.g. Mutex::lock). Using spin::Mutex here would cause deadlock. Co-Authored-By: Claude Opus 4.7 --- os/arceos/modules/axfs-ng/src/highlevel/file.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/os/arceos/modules/axfs-ng/src/highlevel/file.rs b/os/arceos/modules/axfs-ng/src/highlevel/file.rs index 6b3da2671c..13d59fcc9c 100644 --- a/os/arceos/modules/axfs-ng/src/highlevel/file.rs +++ b/os/arceos/modules/axfs-ng/src/highlevel/file.rs @@ -375,14 +375,14 @@ intrusive_adapter!(EvictListenerAdapter = Box: EvictListener { li struct CachedFileShared { page_cache: Mutex>, - evict_listeners: spin::Mutex>, + evict_listeners: Mutex>, } impl CachedFileShared { pub fn new() -> Self { Self { page_cache: Mutex::new(LruCache::new(NonZeroUsize::new(64).unwrap())), - evict_listeners: spin::Mutex::new(LinkedList::default()), + evict_listeners: Mutex::new(LinkedList::default()), } } @@ -390,7 +390,7 @@ impl CachedFileShared { pub fn new_unbounded() -> Self { Self { page_cache: Mutex::new(LruCache::unbounded()), - evict_listeners: spin::Mutex::new(LinkedList::default()), + evict_listeners: Mutex::new(LinkedList::default()), } } From 6d21ecaa3b9c0a4ee991603d8a282fc1390d2f19 Mon Sep 17 00:00:00 2001 From: CI Test Date: Thu, 21 May 2026 22:49:30 +0800 Subject: [PATCH 10/11] =?UTF-8?q?fix(mm):=20address=20review=20findings=20?= =?UTF-8?q?=E2=80=94=20LRU=20order,=20lockdep=20subclass,=20reclaim=20retr?= =?UTF-8?q?y?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - evict clean pages in LRU-first order (cache.iter().rev()) instead of MRU-first to evict cold pages before hot ones - pass lockdep subclass through to prepare_nested in lock_nested and try_lock_nested instead of discarding it with hardcoded 0 - retry allocation after reclaim even when reclaim was skipped, since concurrent reclaim may have freed pages - remove unused prepare() convenience and #[allow(dead_code)] - fix try_evict_clean_pages doc comment to describe actual behavior Co-Authored-By: Claude Opus 4.7 --- os/arceos/modules/axalloc/src/buddy_slab.rs | 22 +++++++------------ os/arceos/modules/axalloc/src/default_impl.rs | 12 +++++----- .../modules/axfs-ng/src/highlevel/file.rs | 11 +++++----- os/arceos/modules/axsync/src/lockdep.rs | 6 ----- os/arceos/modules/axsync/src/mutex.rs | 8 +++---- 5 files changed, 23 insertions(+), 36 deletions(-) diff --git a/os/arceos/modules/axalloc/src/buddy_slab.rs b/os/arceos/modules/axalloc/src/buddy_slab.rs index 9cdfbd262b..2b23ee16bf 100644 --- a/os/arceos/modules/axalloc/src/buddy_slab.rs +++ b/os/arceos/modules/axalloc/src/buddy_slab.rs @@ -195,17 +195,15 @@ impl GlobalAllocator { if result.is_err() { for _ in 0..4 { let reclaimed = crate::try_page_reclaim(num_pages.max(16)); - if reclaimed == 0 { - break; - } - debug!( - "page reclaim freed {} pages, retrying allocation ({} pages, kind={:?})", - reclaimed, num_pages, kind - ); + // Retry allocation regardless of whether reclaim ran; + // concurrent reclaim may have freed pages. result = self.inner.lock().alloc_pages(num_pages, alignment); if result.is_ok() { break; } + if reclaimed == 0 { + break; + } } } let addr = result.map_err(crate::AllocError::from)?; @@ -224,17 +222,13 @@ impl GlobalAllocator { if result.is_err() { for _ in 0..4 { let reclaimed = crate::try_page_reclaim(num_pages.max(16)); - if reclaimed == 0 { - break; - } - debug!( - "page reclaim freed {} pages, retrying dma32 allocation ({} pages, kind={:?})", - reclaimed, num_pages, kind - ); result = self.inner.lock().alloc_pages_lowmem(num_pages, alignment); if result.is_ok() { break; } + if reclaimed == 0 { + break; + } } } let addr = result.map_err(crate::AllocError::from)?; diff --git a/os/arceos/modules/axalloc/src/default_impl.rs b/os/arceos/modules/axalloc/src/default_impl.rs index fe1145009d..e0b9e75acb 100644 --- a/os/arceos/modules/axalloc/src/default_impl.rs +++ b/os/arceos/modules/axalloc/src/default_impl.rs @@ -185,17 +185,15 @@ impl GlobalAllocator { if result.is_err() { for _ in 0..4 { let reclaimed = crate::try_page_reclaim(num_pages.max(16)); - if reclaimed == 0 { - break; - } - debug!( - "page reclaim freed {} pages, retrying allocation ({} pages, kind={:?})", - reclaimed, num_pages, kind - ); + // Retry allocation regardless of whether reclaim ran; + // concurrent reclaim may have freed pages. result = self.palloc.lock().alloc_pages(num_pages, alignment); if result.is_ok() { break; } + if reclaimed == 0 { + break; + } } } let addr = result.map_err(crate::AllocError::from)?; diff --git a/os/arceos/modules/axfs-ng/src/highlevel/file.rs b/os/arceos/modules/axfs-ng/src/highlevel/file.rs index 13d59fcc9c..845055eade 100644 --- a/os/arceos/modules/axfs-ng/src/highlevel/file.rs +++ b/os/arceos/modules/axfs-ng/src/highlevel/file.rs @@ -386,7 +386,6 @@ impl CachedFileShared { } } - #[allow(dead_code)] pub fn new_unbounded() -> Self { Self { page_cache: Mutex::new(LruCache::unbounded()), @@ -395,9 +394,11 @@ impl CachedFileShared { } /// Scan the LRU and evict up to `max` clean pages. - /// Pages whose listener skipped (e.g. aspace try_lock failed) are - /// returned so the caller can retry notification after dropping the - /// page cache lock. + /// + /// Collects non-dirty pages from the LRU cache (least-recently-used + /// first), pops them, notifies all registered evict listeners, and + /// drops the pages (freeing their backing physical memory). + /// Returns the number of pages evicted. fn try_evict_clean_pages(&self, max: usize) -> usize { let Some(mut cache) = self.page_cache.try_lock() else { return 0; @@ -405,7 +406,7 @@ impl CachedFileShared { let mut to_evict = [0u32; 256]; let limit = max.min(256); let mut cnt = 0; - for (&pn, page) in cache.iter() { + for (&pn, page) in cache.iter().rev() { if !page.dirty && cnt < limit { to_evict[cnt] = pn; cnt += 1; diff --git a/os/arceos/modules/axsync/src/lockdep.rs b/os/arceos/modules/axsync/src/lockdep.rs index b49686283d..f2b469b1e2 100644 --- a/os/arceos/modules/axsync/src/lockdep.rs +++ b/os/arceos/modules/axsync/src/lockdep.rs @@ -16,12 +16,6 @@ pub(crate) struct LockdepAcquire { } impl LockdepAcquire { - #[inline(always)] - #[track_caller] - pub(crate) fn prepare(lock: &RawMutex, is_try: bool) -> Self { - Self::prepare_nested(lock, is_try, 0) - } - #[inline(always)] #[track_caller] pub(crate) fn prepare_nested(lock: &RawMutex, is_try: bool, subclass: LockSubclass) -> Self { diff --git a/os/arceos/modules/axsync/src/mutex.rs b/os/arceos/modules/axsync/src/mutex.rs index 97cb39f503..46186df68e 100644 --- a/os/arceos/modules/axsync/src/mutex.rs +++ b/os/arceos/modules/axsync/src/mutex.rs @@ -180,11 +180,11 @@ impl RawMutex { #[inline(always)] #[track_caller] #[cfg(feature = "lockdep")] - fn lock_nested(&self, _subclass: LockSubclass) { + fn lock_nested(&self, subclass: LockSubclass) { might_sleep(); let current_id = current().id().as_u64(); - let lockdep = crate::lockdep::LockdepAcquire::prepare(self, false); + let lockdep = crate::lockdep::LockdepAcquire::prepare_nested(self, false, subclass); self.lock_after_prepare(current_id); lockdep.finish(true); } @@ -207,12 +207,12 @@ impl RawMutex { #[inline(always)] #[track_caller] #[cfg(feature = "lockdep")] - fn try_lock_nested(&self, _subclass: LockSubclass) -> bool { + fn try_lock_nested(&self, subclass: LockSubclass) -> bool { // try_lock is a single atomic CAS — it never blocks or sleeps, // so it is safe to call from atomic context. let current_id = current().id().as_u64(); - let lockdep = crate::lockdep::LockdepAcquire::prepare(self, true); + let lockdep = crate::lockdep::LockdepAcquire::prepare_nested(self, true, subclass); let acquired = self.try_lock_after_prepare(current_id); lockdep.finish(acquired); acquired From b863629c33570ecc3bd78c24459cdd449ff09ee9 Mon Sep 17 00:00:00 2001 From: CI Test Date: Sat, 23 May 2026 15:27:52 +0800 Subject: [PATCH 11/11] docs(axfs-ng): document lock ordering and reclaim context in try_evict_clean_pages Add explicit lock-ordering documentation for page_cache -> evict_listeners and explain that the reclaim callback runs with interrupts enabled. Co-Authored-By: Claude Opus 4.7 --- os/arceos/modules/axfs-ng/src/highlevel/file.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/os/arceos/modules/axfs-ng/src/highlevel/file.rs b/os/arceos/modules/axfs-ng/src/highlevel/file.rs index 845055eade..a952cf1eb2 100644 --- a/os/arceos/modules/axfs-ng/src/highlevel/file.rs +++ b/os/arceos/modules/axfs-ng/src/highlevel/file.rs @@ -399,6 +399,22 @@ impl CachedFileShared { /// first), pops them, notifies all registered evict listeners, and /// drops the pages (freeing their backing physical memory). /// Returns the number of pages evicted. + /// + /// # Lock ordering: `page_cache` → `evict_listeners` + /// + /// This function holds the `page_cache` lock while acquiring + /// `evict_listeners`. Listeners must never acquire `page_cache` + /// (or any lock taken before `evict_listeners`), or a deadlock will + /// occur. The current callers (address-space backend page-table + /// invalidation) do not touch the file's page cache, so this + /// ordering is safe in practice. + /// + /// # Context + /// + /// When called from the page-reclaim path, the reclaim callback is + /// invoked with interrupts enabled: `try_page_reclaim` releases its + /// `SpinNoIrq` guard before invoking the callback, so `might_sleep()` + /// in the blocking `evict_listeners.lock()` is safe. fn try_evict_clean_pages(&self, max: usize) -> usize { let Some(mut cache) = self.page_cache.try_lock() else { return 0;