Files
asterinas/kernel/aster-nix/src/fs/utils/page_cache.rs

280 lines
8.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SPDX-License-Identifier: MPL-2.0
use core::ops::Range;
use aster_block::bio::{BioStatus, BioWaiter};
use aster_frame::vm::{VmAllocOptions, VmFrame};
use aster_rights::Full;
use lru::LruCache;
use crate::{
prelude::*,
vm::vmo::{get_page_idx_range, Pager, Vmo, VmoFlags, VmoOptions},
};
pub struct PageCache {
pages: Vmo<Full>,
manager: Arc<PageCacheManager>,
}
impl PageCache {
/// Creates an empty size page cache associated with a new backend.
pub fn new(backend: Weak<dyn PageCacheBackend>) -> Result<Self> {
let manager = Arc::new(PageCacheManager::new(backend));
let pages = VmoOptions::<Full>::new(0)
.flags(VmoFlags::RESIZABLE)
.pager(manager.clone())
.alloc()?;
Ok(Self { pages, manager })
}
/// Creates a page cache associated with an existing backend.
///
/// The `capacity` is the initial cache size required by the backend.
/// This size usually corresponds to the size of the backend.
pub fn with_capacity(capacity: usize, backend: Weak<dyn PageCacheBackend>) -> Result<Self> {
let manager = Arc::new(PageCacheManager::new(backend));
let pages = VmoOptions::<Full>::new(capacity)
.flags(VmoFlags::RESIZABLE)
.pager(manager.clone())
.alloc()?;
Ok(Self { pages, manager })
}
/// Returns the Vmo object.
// TODO: The capability is too highrestrict it to eliminate the possibility of misuse.
// For example, the `resize` api should be forbidded.
pub fn pages(&self) -> Vmo<Full> {
self.pages.dup()
}
/// Evict the data within a specified range from the page cache and persist
/// them to the backend.
pub fn evict_range(&self, range: Range<usize>) -> Result<()> {
self.manager.evict_range(range)
}
/// Evict the data within a specified range from the page cache without persisting
/// them to the backend.
pub fn discard_range(&self, range: Range<usize>) {
self.manager.discard_range(range)
}
/// Returns the backend.
pub fn backend(&self) -> Arc<dyn PageCacheBackend> {
self.manager.backend()
}
}
impl Drop for PageCache {
fn drop(&mut self) {
// TODO:
// The default destruction procedure exhibits slow performance.
// In contrast, resizing the `VMO` to zero greatly accelerates the process.
// We need to find out the underlying cause of this discrepancy.
let _ = self.pages.resize(0);
}
}
impl Debug for PageCache {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.debug_struct("PageCache")
.field("size", &self.pages.size())
.field("mamager", &self.manager)
.finish()
}
}
struct PageCacheManager {
pages: Mutex<LruCache<usize, Page>>,
backend: Weak<dyn PageCacheBackend>,
}
impl PageCacheManager {
pub fn new(backend: Weak<dyn PageCacheBackend>) -> Self {
Self {
pages: Mutex::new(LruCache::unbounded()),
backend,
}
}
pub fn backend(&self) -> Arc<dyn PageCacheBackend> {
self.backend.upgrade().unwrap()
}
// Discard pages without writing them back to disk.
pub fn discard_range(&self, range: Range<usize>) {
let page_idx_range = get_page_idx_range(&range);
for idx in page_idx_range {
self.pages.lock().pop(&idx);
}
}
pub fn evict_range(&self, range: Range<usize>) -> Result<()> {
let page_idx_range = get_page_idx_range(&range);
//TODO: When there are many pages, we should submit them in batches of folios rather than all at once.
let mut indices_and_waiters: Vec<(usize, BioWaiter)> = Vec::new();
for idx in page_idx_range {
if let Some(page) = self.pages.lock().get_mut(&idx) {
if let PageState::Dirty = page.state() {
let backend = self.backend();
if idx < backend.npages() {
indices_and_waiters.push((idx, backend.write_page(idx, page.frame())?));
}
}
}
}
for (idx, waiter) in indices_and_waiters.iter() {
if matches!(waiter.wait(), Some(BioStatus::Complete)) {
if let Some(page) = self.pages.lock().get_mut(idx) {
page.set_state(PageState::UpToDate)
}
} else {
// TODO: We may need an error handler here.
return_errno!(Errno::EIO)
}
}
Ok(())
}
}
impl Debug for PageCacheManager {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.debug_struct("PageCacheManager")
.field("pages", &self.pages.lock())
.finish()
}
}
impl Pager for PageCacheManager {
fn commit_page(&self, idx: usize) -> Result<VmFrame> {
if let Some(page) = self.pages.lock().get(&idx) {
return Ok(page.frame.clone());
}
//Multiple threads may commit the same page, but the result is ok.
let backend = self.backend();
let page = if idx < backend.npages() {
let mut page = Page::alloc()?;
backend.read_page_sync(idx, page.frame())?;
page.set_state(PageState::UpToDate);
page
} else {
Page::alloc_zero()?
};
let frame = page.frame().clone();
self.pages.lock().put(idx, page);
Ok(frame)
}
fn update_page(&self, idx: usize) -> Result<()> {
let mut pages = self.pages.lock();
if let Some(page) = pages.get_mut(&idx) {
page.set_state(PageState::Dirty);
} else {
warn!("The page {} is not in page cache", idx);
}
Ok(())
}
fn decommit_page(&self, idx: usize) -> Result<()> {
let page_result = self.pages.lock().pop(&idx);
if let Some(page) = page_result {
if let PageState::Dirty = page.state() {
let Some(backend) = self.backend.upgrade() else {
return Ok(());
};
if idx < backend.npages() {
backend.write_page_sync(idx, page.frame())?;
}
}
}
Ok(())
}
}
#[derive(Debug)]
struct Page {
frame: VmFrame,
state: PageState,
}
impl Page {
pub fn alloc() -> Result<Self> {
let frame = VmAllocOptions::new(1).uninit(true).alloc_single()?;
Ok(Self {
frame,
state: PageState::Uninit,
})
}
pub fn alloc_zero() -> Result<Self> {
let frame = VmAllocOptions::new(1).alloc_single()?;
Ok(Self {
frame,
state: PageState::Dirty,
})
}
pub fn frame(&self) -> &VmFrame {
&self.frame
}
pub fn state(&self) -> &PageState {
&self.state
}
pub fn set_state(&mut self, new_state: PageState) {
self.state = new_state;
}
}
#[derive(Debug)]
enum PageState {
/// `Uninit` indicates a new allocated page which content has not been initialized.
/// The page is available to write, not available to read.
Uninit,
/// `UpToDate` indicates a page which content is consistent with corresponding disk content.
/// The page is available to read and write.
UpToDate,
/// `Dirty` indicates a page which content has been updated and not written back to underlying disk.
/// The page is available to read and write.
Dirty,
}
/// This trait represents the backend for the page cache.
pub trait PageCacheBackend: Sync + Send {
/// Reads a page from the backend asynchronously.
fn read_page(&self, idx: usize, frame: &VmFrame) -> Result<BioWaiter>;
/// Writes a page to the backend asynchronously.
fn write_page(&self, idx: usize, frame: &VmFrame) -> Result<BioWaiter>;
/// Returns the number of pages in the backend.
fn npages(&self) -> usize;
}
impl dyn PageCacheBackend {
/// Reads a page from the backend synchronously.
fn read_page_sync(&self, idx: usize, frame: &VmFrame) -> Result<()> {
let waiter = self.read_page(idx, frame)?;
match waiter.wait() {
Some(BioStatus::Complete) => Ok(()),
_ => return_errno!(Errno::EIO),
}
}
/// Writes a page to the backend synchronously.
fn write_page_sync(&self, idx: usize, frame: &VmFrame) -> Result<()> {
let waiter = self.write_page(idx, frame)?;
match waiter.wait() {
Some(BioStatus::Complete) => Ok(()),
_ => return_errno!(Errno::EIO),
}
}
}