blob: c205506db2d7287abea9ea5ee3e0594852594b39 [file]
// Copyright 2021 The Fuchsia Authors
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT
mod page_cache_ffi;
use crate::arch_rs::{CpuAlignMarker, MAX_CACHE_LINE, curr_cpu_num};
use crate::counters::define_kcounter;
use crate::kernel::percpu::PerCpu;
use crate::kernel::thread::{AutoPreemptDisabler, current_memory_allocation_state_is_enabled};
use crate::vm::page::VmPageDoublyLinkedList;
use crate::vm::page_state::VmPageState;
use crate::vm::page_state::bindings::vm_page_state;
use crate::vm::pmm::{self, ALLOC_FLAG_CAN_WAIT};
use core::ffi::c_int;
use core::ops::Deref;
use core::pin::Pin;
use ksync::{KCell, KMutex, RawMutex};
use page_cache_ffi::{cpp_global_prng_draw, rand_r};
use pin_init::{PinInit, pin_data, pin_init, pinned_drop, stack_pin_init};
use zx_status::Status;
const TRACE_ENABLED: bool = false;
const RAND_MAX: c_int = 0x7fff_ffff;
define_kcounter!(PAGE_CACHE_HIT_PAGES, "cache.page.hit", Sum);
define_kcounter!(PAGE_CACHE_MISS_PAGES, "cache.page.missed", Sum);
define_kcounter!(PAGE_CACHE_REFILL_PAGES, "cache.page.refilled", Sum);
define_kcounter!(PAGE_CACHE_RETURN_PAGES, "cache.page.returned", Sum);
define_kcounter!(PAGE_CACHE_FREE_PAGES, "cache.page.freed", Sum);
fn count_hit_pages(page_count: usize) {
PAGE_CACHE_HIT_PAGES.add(page_count as i64);
}
fn count_miss_pages(page_count: usize) {
PAGE_CACHE_MISS_PAGES.add(page_count as i64);
}
fn count_refill_pages(page_count: usize) {
PAGE_CACHE_REFILL_PAGES.add(page_count as i64);
}
fn count_return_pages(page_count: usize) {
PAGE_CACHE_RETURN_PAGES.add(page_count as i64);
}
fn count_free_pages(page_count: usize) {
PAGE_CACHE_FREE_PAGES.add(page_count as i64);
}
/// An RAII wrapper around `VmPageDoublyLinkedList` that automatically frees remaining pages
/// back to the PMM upon destruction.
#[repr(transparent)]
#[pin_data(PinnedDrop)]
pub struct PageList {
#[pin]
pages: VmPageDoublyLinkedList,
}
// SAFETY: `PageList` owns the `VmPage` nodes linked within it and can be safely
// transferred across threads.
unsafe impl Send for PageList {}
impl PageList {
/// Creates an in-place initializer for a new, empty `PageList`.
pub fn new() -> impl PinInit<Self, core::convert::Infallible> {
pin_init!(Self {
pages <- VmPageDoublyLinkedList::new(),
})
}
/// Returns a shared reference to the underlying `VmPageDoublyLinkedList`.
pub fn list(&self) -> &VmPageDoublyLinkedList {
&self.pages
}
/// Returns a pinned mutable reference to the underlying `VmPageDoublyLinkedList`.
pub fn list_mut(self: Pin<&mut Self>) -> Pin<&mut VmPageDoublyLinkedList> {
self.project().pages
}
}
impl Deref for PageList {
type Target = VmPageDoublyLinkedList;
fn deref(&self) -> &Self::Target {
&self.pages
}
}
#[pinned_drop]
impl PinnedDrop for PageList {
fn drop(self: Pin<&mut Self>) {
if !self.pages.is_empty() {
// SAFETY: Pages in `self.pages` are valid allocated PMM pages that have not
// already been freed.
unsafe {
pmm::free_list(self.project().pages);
}
}
}
}
/// The number of pages remaining in the per-CPU cache after an allocation request.
pub type AvailablePages = usize;
// `#[repr(C)]` preserves field order so the fast-path fields (`cache_lock`, `available_pages`, and
// `free_list`, 48 bytes total) stay together in the first 64-byte cache line, while slow-path
// `fill_lock` and `random_should_wait_state` sit in the second cache line.
#[ksync::guarded]
#[repr(C)]
struct CpuCache {
_align: CpuAlignMarker,
#[mutex]
cache_lock: KMutex<RawMutex>,
#[guarded_by(cache_lock)]
available_pages: usize,
#[pin]
#[guarded_by(cache_lock)]
free_list: PageList,
#[mutex]
fill_lock: KMutex<RawMutex>,
#[guarded_by(cache_lock)]
random_should_wait_state: Option<usize>,
}
zr::static_assert!(core::mem::align_of::<CpuCache>() == MAX_CACHE_LINE);
zr::static_assert!(core::mem::offset_of!(CpuCache, fill_lock) <= MAX_CACHE_LINE);
impl CpuCache {
fn init() -> impl PinInit<Self, core::convert::Infallible> {
pin_init!(Self {
_align: CpuAlignMarker,
cache_lock <- KMutex::init(),
available_pages: 0.into(),
free_list <- KCell::pin_init(PageList::new()),
fill_lock <- KMutex::init(),
random_should_wait_state: None.into(),
})
}
}
/// `PageCache` provides a front end to the PMM that reserves a given number of
/// pages in per-CPU caches to reduce contention on the PMM.
///
/// TODO(https://fxbug.dev/42147478): Add support for KASAN.
/// TODO(https://fxbug.dev/42147480): Flush page caches when CPUs go offline.
#[repr(C)]
#[derive(Default)]
pub struct PageCache {
reserve_pages: usize,
per_cpu_caches: Option<fbl::Array<CpuCache>>,
}
zr::static_assert!(core::mem::size_of::<PageCache>() == 24);
zr::static_assert!(core::mem::align_of::<PageCache>() == 8);
zr::static_assert!(core::mem::offset_of!(PageCache, reserve_pages) == 0);
zr::static_assert!(core::mem::offset_of!(PageCache, per_cpu_caches) == 8);
// SAFETY: `PageCache` internally synchronizes all mutable per-CPU cache access
// via `CpuCache`'s `fill_lock` and `cache_lock`.
unsafe impl Send for PageCache {}
unsafe impl Sync for PageCache {}
impl PageCache {
/// Creates a new page cache with the given number of reserve pages per CPU.
/// Filling the per-CPU page caches is deferred until the first allocation
/// request.
pub fn new(reserve_pages: usize) -> Result<Self, Status> {
let cpu_count = PerCpu::processor_count();
debug_assert!(cpu_count != 0);
let mut entries = fbl::Array::<CpuCache>::try_new_uninit_slice(cpu_count)
.map_err(|_| Status::NO_MEMORY)?;
for item in entries.iter_mut() {
let init = CpuCache::init();
// SAFETY: `item.as_mut_ptr()` is a valid, writable pointer to uninitialized
// memory in `entries`, and `entries` remains pinned on the heap for the
// lifetime of `PageCache`.
unsafe {
let _ = init.__pinned_init(item.as_mut_ptr());
}
}
// SAFETY: All elements in `entries` were initialized in the loop above.
let entries = unsafe { entries.assume_init() };
debug_assert!((entries.as_ptr() as usize) & (MAX_CACHE_LINE - 1) == 0);
Ok(Self { reserve_pages, per_cpu_caches: Some(entries) })
}
/// Returns the number of reserve pages per CPU configured for this cache.
pub fn reserve_pages(&self) -> usize {
self.reserve_pages
}
/// Seeds the per-CPU PRNG state used to randomly return `Status::SHOULD_WAIT`
/// on waitable allocations.
pub fn seed_random_should_wait(&self) {
let per_cpu_caches = self.per_cpu_caches.as_deref().unwrap_or(&[]);
assert!(!per_cpu_caches.is_empty());
for entry in per_cpu_caches.iter() {
ksync::lock!(let mut guard = entry.lock_cache_lock());
let mut seed: usize = 0;
// SAFETY: `seed` is a valid stack variable of `size_of::<usize>()` bytes.
unsafe {
cpp_global_prng_draw((&raw mut seed).cast::<u8>(), core::mem::size_of::<usize>());
}
*guard.as_mut().fields_mut().random_should_wait_state = Some(seed);
}
}
/// Allocates `count` pages from the page cache into `out_pages`.
pub fn alloc(
&self,
count: usize,
out_pages: Pin<&mut PageList>,
) -> Result<AvailablePages, Status> {
self.allocate(count, 0, out_pages)
}
/// Allocates the given number of pages from the page cache into `out_pages`.
/// Falls back to the PMM if the cache is insufficient to fulfill the request.
pub fn allocate(
&self,
page_count: usize,
alloc_flags: u32,
out_pages: Pin<&mut PageList>,
) -> Result<AvailablePages, Status> {
let _trace = crate::ktrace_rs::begin_scope_cond!(
TRACE_ENABLED,
"kernel:sched",
"PageCache::Allocate",
"page_count" => page_count as u64,
"alloc_flags" => alloc_flags
);
debug_assert!(current_memory_allocation_state_is_enabled());
let per_cpu_caches = self.per_cpu_caches.as_deref().unwrap_or(&[]);
debug_assert!(!per_cpu_caches.is_empty());
let _preempt_disable = AutoPreemptDisabler::new();
let current_cpu = curr_cpu_num() as usize;
self.allocate_entry(&per_cpu_caches[current_cpu], page_count, alloc_flags, out_pages)
}
/// Frees pages back to the page cache. Excess pages are returned to the PMM.
pub fn free(&self, pages: Pin<&mut PageList>) {
let mut page_list = pages.list_mut();
let _trace =
crate::ktrace_rs::begin_scope_cond!(TRACE_ENABLED, "kernel:sched", "PageCache::Free");
let per_cpu_caches = self.per_cpu_caches.as_deref().unwrap_or(&[]);
debug_assert!(!per_cpu_caches.is_empty());
if !page_list.is_empty() {
{
// Note that _preempt_disable is destroyed before excess pages in
// page_list are freed to the PMM, intentionally resulting in excess
// pages being freed outside of this local preemption disablement.
let _preempt_disable = AutoPreemptDisabler::new();
let current_cpu = curr_cpu_num() as usize;
self.free_entry(&per_cpu_caches[current_cpu], page_list.as_mut());
}
if !page_list.is_empty() {
// SAFETY: Remaining pages in `page_list` are valid allocated PMM pages
// that did not fit in the per-CPU cache.
unsafe { pmm::free_list(page_list) };
}
}
}
/// Attempts to allocate the given number of pages from the CPU cache. If the
/// cache is insufficient for the request, falls back to the PMM to fulfill the
/// request and refill the cache. The requested number of pages may be zero, in
/// which case only the cache is filled.
fn allocate_entry(
&self,
entry: &CpuCache,
requested_pages: usize,
alloc_flags: u32,
out_pages: Pin<&mut PageList>,
) -> Result<AvailablePages, Status> {
if requested_pages > 0 {
ksync::lock!(let mut guard = entry.lock_cache_lock());
let fields = guard.as_mut().fields_mut();
// If the allocation is waitable, check if user wants random should waits,
// and if so randomly error 10% of allocation requests.
if (alloc_flags & ALLOC_FLAG_CAN_WAIT) != 0
&& let Some(seed) = fields.random_should_wait_state.as_mut()
// SAFETY: `seed` is a valid mutable pointer to the per-CPU PRNG state
// guarded by `cache_lock`.
&& unsafe { rand_r(seed) } < (RAND_MAX / 10)
{
return Err(Status::SHOULD_WAIT);
}
if requested_pages <= *fields.available_pages {
return Ok(Self::allocate_cache_pages(guard.as_mut(), requested_pages, out_pages));
}
}
self.allocate_pages_and_fill_cache(entry, requested_pages, alloc_flags, out_pages)
}
/// Allocates the given number of pages from the given CPU cache.
fn allocate_cache_pages(
mut guard: Pin<&mut CpuCacheCacheLockGuard<'_>>,
requested_pages: usize,
out_pages: Pin<&mut PageList>,
) -> AvailablePages {
let mut fields = guard.as_mut().fields_mut();
debug_assert!(requested_pages <= *fields.available_pages);
debug_assert!(requested_pages > 0);
*fields.available_pages -= requested_pages;
let mut out_list = out_pages.list_mut();
let mut free_list = fields.free_list.as_mut().list_mut();
for _ in 0..requested_pages {
let page = free_list.as_mut().pop_back().expect("free_list underflow");
// SAFETY: We have conceptual ownership of `page` after popping it from `free_list`.
unsafe {
page.as_ref().set_state(VmPageState(vm_page_state::ALLOC));
out_list.as_mut().push_front_raw(page);
}
}
count_hit_pages(requested_pages);
*fields.available_pages
}
/// Returns the given list of pages to the given CPU cache, leaving excess
/// pages in `page_list` to be returned to the PMM by the caller.
fn free_entry(&self, entry: &CpuCache, page_list: Pin<&mut VmPageDoublyLinkedList>) {
ksync::lock!(let mut guard = entry.lock_cache_lock());
let mut fields = guard.as_mut().fields_mut();
let mut free_count = 0;
let mut return_count = 0;
stack_pin_init!(let return_list = VmPageDoublyLinkedList::new());
// Filter out non-borrowed pages to return to the free list. Pages remaining
// in page_list are freed to the PMM outside the lock.
let mut cursor = page_list.cursor_front_mut();
while let Some(page) = cursor.get_mut() {
debug_assert!(!page.is_loaned());
if *fields.available_pages < self.reserve_pages {
// SAFETY: We have conceptual ownership of `page` in `page_list`.
unsafe { page.set_state(VmPageState(vm_page_state::CACHE)) };
let erased = cursor.erase().expect("cursor had current element");
// SAFETY: `erased` was just removed from `page_list` and is not in any list.
unsafe { return_list.as_mut().push_back_raw(erased) };
*fields.available_pages += 1;
return_count += 1;
} else {
free_count += 1;
cursor.move_next();
}
}
// Return the selected pages to the free list.
let free_list = fields.free_list.as_mut().list_mut();
free_list.cursor_front_mut().splice(return_list);
count_return_pages(return_count);
count_free_pages(free_count);
}
fn allocate_pages_and_fill_cache(
&self,
entry: &CpuCache,
requested_pages: usize,
alloc_flags: u32,
out_pages: Pin<&mut PageList>,
) -> Result<AvailablePages, Status> {
let _trace = crate::ktrace_rs::begin_scope_cond!(
TRACE_ENABLED,
"kernel:sched",
"PageCache::AllocatePagesAndFillCache",
"requested_pages" => requested_pages as u64,
"alloc_flags" => alloc_flags
);
// Serialize cache fill + allocate operations on this cache. Contention
// means another thread tried to allocate from the PMM and blocked on the
// PMM lock. There's no benefit to following the owning thread into the PMM
// allocator; by the time this lock is released the cache may have enough
// pages to satisfy this request without falling back to the PMM again.
ksync::lock!(entry.lock_fill_lock());
// Acquire the cache lock after the fill lock. If this thread blocked on the
// previous lock, there is a chance this thread is now running on a
// different CPU. However, there's also a good chance the cache is now
// sufficient to fulfill the request without falling back to the PMM.
ksync::lock!(let mut cache_guard = entry.lock_cache_lock());
// Re-validate the request after acquiring the locks. Another thread may
// have filled the cache sufficiently already.
if requested_pages > *cache_guard.available_pages() {
let refill_pages = self.reserve_pages - *cache_guard.available_pages();
let total_pages = requested_pages + refill_pages;
count_refill_pages(refill_pages);
count_miss_pages(requested_pages);
stack_pin_init!(let page_list = VmPageDoublyLinkedList::new());
// Release the cache lock while calling into the PMM to permit other
// threads to access the cache if this thread blocks.
let status = cache_guard.as_mut().call_unlocked(|| {
let mut pages = total_pages;
let mut status = pmm::alloc_pages(pages, alloc_flags, page_list.as_mut());
// If the alloc_flags contained ALLOC_FLAG_CAN_WAIT then we could get
// Status::SHOULD_WAIT as a return code. In this case, as per the
// documentation of pmm::alloc_pages, we must fall back to attempting to
// allocate single pages.
if status == Err(Status::SHOULD_WAIT) {
status = Ok(());
while pages > 0 {
match pmm::alloc_page(alloc_flags) {
Ok((page, _pa)) => {
// SAFETY: `page` was just allocated from the PMM and is not in
// any list.
unsafe { page_list.as_mut().push_back_raw(page.as_non_null()) };
pages -= 1;
}
Err(err) => {
status = Err(err);
break;
}
}
}
}
status
});
if let Err(err) = status {
// Even if allocating all the pages failed, we may have received a few.
// If so, put them into the cache and then see if we have enough for the
// actual requested allocation. Since this is an uncommon edge case do
// not try and be too optimal with the implementation.
let mut fields = cache_guard.as_mut().fields_mut();
let mut free_list = fields.free_list.as_mut().list_mut();
while let Some(page) = page_list.as_mut().pop_front() {
// SAFETY: We have conceptual ownership of `page` and it is not in any list.
unsafe {
page.as_ref().set_state(VmPageState(vm_page_state::CACHE));
free_list.as_mut().push_back_raw(page);
}
*fields.available_pages += 1;
}
if requested_pages <= *fields.available_pages {
return Ok(Self::allocate_cache_pages(
cache_guard.as_mut(),
requested_pages,
out_pages,
));
}
return Err(err);
}
let out_list = out_pages.list_mut();
// Set the page state of the refill pages and find the end of the refill list.
if refill_pages > 0 {
let mut cursor = page_list.as_mut().cursor_front_mut();
for i in 0..refill_pages {
let page = cursor.get_mut().expect("refill_pages > page_list len");
// SAFETY: We have conceptual ownership of `page` in `page_list`.
unsafe { page.set_state(VmPageState(vm_page_state::CACHE)) };
if i + 1 < refill_pages {
cursor.move_next();
}
}
// Separate the return list from the refill list and add the latter to the
// free list.
let mut out_cursor = out_list.cursor_back_mut();
cursor.split_after(&mut out_cursor);
} else {
out_list.splice(page_list.as_mut());
}
let mut fields = cache_guard.as_mut().fields_mut();
let free_list = fields.free_list.as_mut().list_mut();
free_list.cursor_front_mut().splice(page_list);
*fields.available_pages += refill_pages;
} else if requested_pages > 0 {
return Ok(Self::allocate_cache_pages(
cache_guard.as_mut(),
requested_pages,
out_pages,
));
}
Ok(*cache_guard.available_pages())
}
}
/// In-tree kernel unit tests for `page_cache`.
#[cfg(ktest)]
#[unittest::suite(name = "page_cache_rust")]
mod tests {
use super::{PageCache, PageList};
use crate::arch_rs::curr_cpu_num;
use crate::kernel::thread::{AutoPreemptDisabler, ThreadPtr};
use crate::kernel::types::cpu_num_to_mask;
use crate::vm::pmm::ALLOC_FLAG_CAN_WAIT;
use pin_init::stack_pin_init;
use unittest::{expect_eq, expect_false, unwrap_ok};
use zx_status::Status;
/// Tests page cache allocation, refill, free, and random `SHOULD_WAIT` behavior.
#[test]
fn page_cache_tests() {
const RESERVE_PAGES: usize = 8;
let page_cache = unwrap_ok!(PageCache::new(RESERVE_PAGES));
expect_eq!(RESERVE_PAGES, page_cache.reserve_pages());
page_cache.seed_random_should_wait();
// Stay on one CPU during the following tests to verify numeric properties of
// a single per-CPU cache. Accounting for CPU migration during the tests would
// make them overly complicated for little value.
// SAFETY: Kernel unit tests run after threading is initialized.
let current_thread = unsafe { ThreadPtr::current() };
// SAFETY: `current_thread` is valid for the duration of this test.
let original_affinity_mask = unsafe { current_thread.get_cpu_affinity() };
let _restore_affinity = zr::defer(|| {
// SAFETY: `current_thread` is valid for the duration of this test.
unsafe {
current_thread.set_cpu_affinity(original_affinity_mask);
}
});
{
let _preempt_disable = AutoPreemptDisabler::new();
let current_cpu = curr_cpu_num();
// SAFETY: `current_thread` is valid.
unsafe {
current_thread.set_cpu_affinity(cpu_num_to_mask(current_cpu));
}
}
// An allocation from an empty or insufficient page cache fills the cache AND
// returns the pages requested.
{
let page_count = RESERVE_PAGES / 2;
stack_pin_init!(let page_list = PageList::new());
let available_pages = unwrap_ok!(page_cache.alloc(page_count, page_list.as_mut()));
expect_eq!(page_count, page_list.iter().count());
expect_eq!(RESERVE_PAGES, available_pages);
}
// An allocation from a sufficient page cache does not fill the cache AND
// reduces the number of pages available.
{
let page_count = RESERVE_PAGES / 2;
stack_pin_init!(let page_list = PageList::new());
let available_pages = unwrap_ok!(page_cache.alloc(page_count, page_list.as_mut()));
expect_eq!(page_count, page_list.iter().count());
expect_eq!(page_count, available_pages);
}
// An allocation that is too large for the page cache fills the cache AND
// returns the pages requested.
{
let page_count = RESERVE_PAGES * 2;
stack_pin_init!(let page_list = PageList::new());
let available_pages = unwrap_ok!(page_cache.alloc(page_count, page_list.as_mut()));
expect_eq!(page_count, page_list.iter().count());
expect_eq!(RESERVE_PAGES, available_pages);
}
// Exercise basic free.
{
let page_count = RESERVE_PAGES / 2;
stack_pin_init!(let page_list = PageList::new());
let available_pages = unwrap_ok!(page_cache.alloc(page_count, page_list.as_mut()));
expect_eq!(page_count, page_list.iter().count());
expect_eq!(RESERVE_PAGES - page_count, available_pages);
page_cache.free(page_list.as_mut());
expect_eq!(0, page_list.iter().count());
stack_pin_init!(let null_list = PageList::new());
let null_available = unwrap_ok!(page_cache.alloc(0, null_list.as_mut()));
expect_eq!(RESERVE_PAGES, null_available);
}
// Exercise intermixing small and oversized allocations and frees.
{
let large_page_count = RESERVE_PAGES * 2;
stack_pin_init!(let large_list = PageList::new());
let large_available =
unwrap_ok!(page_cache.alloc(large_page_count, large_list.as_mut()));
expect_eq!(large_page_count, large_list.iter().count());
expect_eq!(RESERVE_PAGES, large_available);
let page_count = 1;
stack_pin_init!(let page_list = PageList::new());
let available_pages = unwrap_ok!(page_cache.alloc(page_count, page_list.as_mut()));
expect_eq!(page_count, page_list.iter().count());
expect_eq!(RESERVE_PAGES - page_count, available_pages);
page_cache.free(large_list.as_mut());
expect_eq!(0, large_list.iter().count());
stack_pin_init!(let null_list = PageList::new());
let null_available = unwrap_ok!(page_cache.alloc(0, null_list.as_mut()));
expect_eq!(RESERVE_PAGES, null_available);
expect_eq!(0, null_list.iter().count());
page_cache.free(page_list.as_mut());
expect_eq!(0, page_list.iter().count());
stack_pin_init!(let null_list2 = PageList::new());
let null_available2 = unwrap_ok!(page_cache.alloc(0, null_list2.as_mut()));
expect_eq!(RESERVE_PAGES, null_available2);
expect_eq!(0, null_list2.iter().count());
}
// Verify that random should wait will work by repeatedly allocating and freeing a
// page until we get should wait.
{
// Allocate a page without SHOULD_WAIT to ensure that the cache has items.
stack_pin_init!(let page_list = PageList::new());
let _ = unwrap_ok!(page_cache.alloc(1, page_list.as_mut()));
// Now repeatedly free and allocate this page until we get should wait.
let status = loop {
page_cache.free(page_list.as_mut());
if let Err(err) = page_cache.allocate(1, ALLOC_FLAG_CAN_WAIT, page_list.as_mut()) {
break err;
}
};
expect_eq!(status.into_raw(), Status::SHOULD_WAIT.into_raw());
}
}
/// Tests that PageList drop automatically frees unconsumed pages back to PMM.
#[test]
fn test_page_list_drop_frees_pages() {
let cache = unwrap_ok!(PageCache::new(4));
{
stack_pin_init!(let pages = PageList::new());
let _available_pages = unwrap_ok!(cache.alloc(2, pages.as_mut()));
expect_false!(pages.is_empty());
// Dropping `pages` without calling `cache.free`:
// `PageList::drop` must safely return pages to the PMM without leaking.
}
}
}