kernel/mem/page_cache.rs
1//! Global Page Cache Manager
2//!
3//! Provides a unified page cache for all filesystem operations (read/write/mmap).
4//! This cache is shared across all file objects pointing to the same file,
5//! ensuring consistency and memory efficiency.
6//!
7//! # Phase 0 Implementation
8//!
9//! Current implementation includes:
10//! - Page allocation and caching with CacheId-based indexing
11//! - Pin-based protection against eviction during active use
12//! - Dirty page tracking for write operations
13//! - Object-level locking for mmap support
14//! - Flush operations for writeback
15//!
16//! Not yet implemented:
17//! - Page eviction (LRU or similar policy)
18//! - LOADING state coordination for concurrent access
19//! - Integration with filesystem read/write operations (Phase 1)
20//! - Demand paging for mmap (Phase 2)
21//!
22//! # Usage
23//!
24//! ```rust,no_run
25//! use crate::mem::page_cache::PageCacheManager;
26//!
27//! let paddr = PageCacheManager::global().get_or_create_pinned(cache_id, page_index, |paddr| {
28//! // Load page content from disk to paddr
29//! Ok(())
30//! })?;
31//! // Use the page...
32//! PageCacheManager::global().unpin(cache_id, page_index);
33//! ```
34
35use alloc::collections::BTreeMap;
36use core::sync::atomic::{AtomicUsize, Ordering};
37use spin::RwLock;
38
39use crate::fs::vfs_v2::cache::CacheId;
40use crate::mem::page::allocate_boxed_pages;
41
42/// Page index within a file (0, 1, 2, ...)
43pub type PageIndex = u64;
44
45/// Physical address of a page
46pub type PhysicalAddress = usize;
47
48/// Entry in the page cache representing a single cached page
49pub struct PageCacheEntry {
50 /// Physical address of the cached page
51 paddr: PhysicalAddress,
52 /// Pin count - number of active short-term accesses
53 /// Pages with pin_count > 0 cannot be evicted
54 pin_count: AtomicUsize,
55 /// Dirty flag - true if page has been modified and needs writeback
56 is_dirty: AtomicUsize, // Using AtomicUsize as AtomicBool
57}
58
59impl PageCacheEntry {
60 /// Create a new page cache entry
61 fn new(paddr: PhysicalAddress) -> Self {
62 Self {
63 paddr,
64 pin_count: AtomicUsize::new(0),
65 is_dirty: AtomicUsize::new(0),
66 }
67 }
68
69 /// Get the physical address
70 #[inline]
71 pub fn paddr(&self) -> PhysicalAddress {
72 self.paddr
73 }
74
75 /// Increment pin count
76 #[inline]
77 fn pin(&self) {
78 self.pin_count.fetch_add(1, Ordering::Relaxed);
79 }
80
81 /// Decrement pin count
82 #[inline]
83 fn unpin(&self) {
84 self.pin_count.fetch_sub(1, Ordering::Relaxed);
85 }
86
87 /// Get current pin count
88 #[inline]
89 fn pin_count(&self) -> usize {
90 self.pin_count.load(Ordering::Relaxed)
91 }
92
93 /// Mark page as dirty
94 #[inline]
95 fn mark_dirty(&self) {
96 self.is_dirty.store(1, Ordering::Relaxed);
97 }
98
99 /// Check if page is dirty
100 #[inline]
101 fn is_dirty(&self) -> bool {
102 self.is_dirty.load(Ordering::Relaxed) != 0
103 }
104}
105
106/// Global page cache manager
107///
108/// Manages all cached pages for filesystem operations. Uses (CacheId, PageIndex)
109/// as the key to uniquely identify pages across the entire system.
110pub struct PageCacheManager {
111 /// Map from (CacheId, PageIndex) to cached page entry
112 entries: RwLock<BTreeMap<(CacheId, PageIndex), PageCacheEntry>>,
113 /// Object-level lock counts for eviction prevention
114 /// Maps CacheId to lock count (>0 means object is unevictable)
115 object_locks: RwLock<BTreeMap<CacheId, usize>>,
116}
117
118impl PageCacheManager {
119 /// Create a new empty page cache manager
120 pub const fn new() -> Self {
121 Self {
122 entries: RwLock::new(BTreeMap::new()),
123 object_locks: RwLock::new(BTreeMap::new()),
124 }
125 }
126
127 /// Global singleton accessor
128 #[inline]
129 pub fn global() -> &'static PageCacheManager {
130 &GLOBAL_PAGE_CACHE
131 }
132
133 /// Get a page, pinning it to prevent eviction during access
134 ///
135 /// If the page is not in cache, calls the `loader` callback to load it.
136 /// Returns the physical address of the page with pin_count incremented.
137 ///
138 /// # Arguments
139 /// * `id` - Cache identifier (filesystem + file)
140 /// * `index` - Page index within the file
141 /// * `loader` - Callback to load the page if not cached. Receives the allocated
142 /// physical address and should fill it with page content.
143 ///
144 /// # Returns
145 /// Physical address of the pinned page
146 pub fn get_or_create_pinned<F>(
147 &self,
148 id: CacheId,
149 index: PageIndex,
150 loader: F,
151 ) -> Result<PhysicalAddress, &'static str>
152 where
153 F: FnOnce(PhysicalAddress) -> Result<(), &'static str>,
154 {
155 let key = (id, index);
156
157 // Fast path: page already cached (read lock)
158 if let Some(entry) = self.entries.read().get(&key) {
159 entry.pin();
160 return Ok(entry.paddr());
161 }
162
163 // Slow path: allocate new page and load content (may race; acceptable)
164 let mut boxed_pages = allocate_boxed_pages(1);
165 let page_ptr = boxed_pages.as_mut_ptr();
166 let paddr = page_ptr as PhysicalAddress;
167
168 // Call loader to fill the page with content
169 loader(paddr)?;
170
171 // Acquire write lock and insert if still missing
172 let mut map = self.entries.write();
173 if let Some(existing) = map.get(&key) {
174 // Someone else inserted meanwhile; reuse it and drop our allocation
175 existing.pin();
176 return Ok(existing.paddr());
177 }
178
179 // Create cache entry with pin_count = 1 and insert
180 let entry = PageCacheEntry::new(paddr);
181 entry.pin();
182 map.insert(key, entry);
183
184 // Leak the box to prevent deallocation - we manage it manually now
185 core::mem::forget(boxed_pages);
186
187 Ok(paddr)
188 }
189
190 /// Try to get a pinned page without triggering I/O
191 ///
192 /// Returns Some(paddr) if the page is already cached, None otherwise.
193 /// If successful, increments pin_count.
194 pub fn try_get_pinned(&self, id: CacheId, index: PageIndex) -> Option<PhysicalAddress> {
195 let key = (id, index);
196 self.entries.read().get(&key).map(|entry| {
197 entry.pin();
198 entry.paddr()
199 })
200 }
201
202 /// Unpin a page, allowing it to be evicted
203 ///
204 /// Decrements the pin count. When pin_count reaches 0, the page
205 /// becomes eligible for eviction (if not locked).
206 pub fn unpin(&self, id: CacheId, index: PageIndex) {
207 if let Some(entry) = self.entries.read().get(&(id, index)) {
208 entry.unpin();
209 }
210 }
211
212 /// Mark a page as dirty (modified)
213 ///
214 /// Dirty pages will be written back to storage during flush or eviction.
215 pub fn mark_dirty(&self, id: CacheId, index: PageIndex) {
216 if let Some(entry) = self.entries.read().get(&(id, index)) {
217 entry.mark_dirty();
218 }
219 }
220
221 /// Set object-level lock (prevents eviction of all pages for this object)
222 ///
223 /// Used during mmap to keep all mapped pages resident.
224 /// Phase 0: Simple implementation - lock entire object during mmap.
225 pub fn set_object_locked(&self, id: CacheId, locked: bool) {
226 if locked {
227 *self.object_locks.write().entry(id).or_insert(0) += 1;
228 } else if let Some(count) = self.object_locks.write().get_mut(&id) {
229 *count = count.saturating_sub(1);
230 if *count == 0 {
231 self.object_locks.write().remove(&id);
232 }
233 }
234 }
235
236 /// Check if an object is locked (unevictable)
237 fn is_object_locked(&self, id: CacheId) -> bool {
238 self.object_locks
239 .read()
240 .get(&id)
241 .map_or(false, |&count| count > 0)
242 }
243
244 /// Flush dirty pages for a specific cache object
245 ///
246 /// Writes all dirty pages back to storage using the provided writer callback.
247 /// Only flushes pages with pin_count == 0 to avoid writing pages being modified.
248 ///
249 /// # Arguments
250 /// * `id` - Cache identifier
251 /// * `writer` - Callback to write a page. Receives (page_index, paddr)
252 pub fn flush<F>(&self, id: CacheId, mut writer: F) -> Result<(), &'static str>
253 where
254 F: FnMut(PageIndex, PhysicalAddress) -> Result<(), &'static str>,
255 {
256 // Collect targets under read lock to minimize lock contention
257 let mut targets: alloc::vec::Vec<(PageIndex, PhysicalAddress)> = alloc::vec::Vec::new();
258 {
259 let map = self.entries.read();
260 for (&(cache_id, page_index), entry) in map.iter() {
261 if cache_id == id && entry.is_dirty() && entry.pin_count() == 0 {
262 targets.push((page_index, entry.paddr()));
263 }
264 }
265 }
266
267 // Perform writes without holding the map lock; then clear dirty flags
268 for (page_index, paddr) in targets.into_iter() {
269 writer(page_index, paddr)?;
270 if let Some(entry) = self.entries.read().get(&(id, page_index)) {
271 entry.is_dirty.store(0, Ordering::SeqCst);
272 }
273 }
274 Ok(())
275 }
276
277 /// Get or load a page and return an RAII guard that unpins on drop.
278 #[inline]
279 pub fn pin_or_load<F>(
280 &self,
281 id: CacheId,
282 index: PageIndex,
283 loader: F,
284 ) -> Result<PinnedPage, &'static str>
285 where
286 F: FnOnce(PhysicalAddress) -> Result<(), &'static str>,
287 {
288 let paddr = self.get_or_create_pinned(id, index, loader)?;
289 Ok(PinnedPage { id, index, paddr })
290 }
291
292 /// Try to pin an already cached page and return an RAII guard.
293 #[inline]
294 pub fn try_pin(&self, id: CacheId, index: PageIndex) -> Option<PinnedPage> {
295 self.try_get_pinned(id, index)
296 .map(|paddr| PinnedPage { id, index, paddr })
297 }
298
299 /// Invalidate (drop) all cached pages belonging to the given CacheId.
300 ///
301 /// This is used when a file is removed (unlink) so that subsequent
302 /// lookups / recreations do not observe stale cached content that
303 /// belonged to the old incarnation of the file.
304 pub fn invalidate(&self, id: CacheId) {
305 // Collect keys first under read lock to minimize time with write lock
306 let mut to_remove: alloc::vec::Vec<(CacheId, PageIndex)> = alloc::vec::Vec::new();
307 {
308 let map = self.entries.read();
309 for (&(cache_id, page_index), _entry) in map.iter() {
310 if cache_id == id {
311 to_remove.push((cache_id, page_index));
312 }
313 }
314 }
315 if to_remove.is_empty() {
316 return;
317 }
318 let mut map = self.entries.write();
319 for key in to_remove.into_iter() {
320 map.remove(&key);
321 }
322 }
323}
324
325/// RAII guard for a pinned page.
326///
327/// Unpins the page on Drop. Provides helpers to access the page and mark it dirty.
328pub struct PinnedPage {
329 id: CacheId,
330 index: PageIndex,
331 paddr: PhysicalAddress,
332}
333
334impl PinnedPage {
335 /// Physical address of the pinned page
336 #[inline]
337 pub fn paddr(&self) -> PhysicalAddress {
338 self.paddr
339 }
340
341 /// Cache identifier
342 #[inline]
343 pub fn id(&self) -> CacheId {
344 self.id
345 }
346
347 /// Page index within the object
348 #[inline]
349 pub fn index(&self) -> PageIndex {
350 self.index
351 }
352
353 /// Mark this page dirty
354 #[inline]
355 pub fn mark_dirty(&self) {
356 PageCacheManager::global().mark_dirty(self.id, self.index);
357 }
358}
359
360impl Drop for PinnedPage {
361 fn drop(&mut self) {
362 PageCacheManager::global().unpin(self.id, self.index);
363 }
364}
365
366/// Global page cache instance
367///
368pub static GLOBAL_PAGE_CACHE: PageCacheManager = PageCacheManager::new();
369
370// Note: Callers should access the cache via `PageCacheManager::global()`.
371
372// TODO (Phase 2): Replace single global structure with sharded structure:
373// - Hash (CacheId, PageIndex) -> shard index (e.g. 16 or 32 shards)
374// - Each shard: Mutex/PageCacheShard { entries }
375// - object_locks separated or distributed
376// Instance methods remain the stable API; callers use PageCacheManager::global().