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().