feat(kernel): kernel-owned memory model
- FrameTable: per-frame owner/use, replaces PagePool + ProcessMemory - Quota per process, page tables charged to owner - Regions for shared mem + device MMIO, torn down w/ owner - Platform: create/destroy AS, map/unmap, zero_frame; drop translate_va - RequestPages/ReleasePages/MapPage(move) checked vs kernel records - user window 0x1000..32MiB, private pts by construction - boot loader + device setup go through kernel api - kernel built in place, boot allocator bounded - isolation tests, randomized invariant test, FrameTable kani proofs - fixes: inflated release, unmapped release, dst va unchecked, heap overflow, unbounded pt alloc, unzeroed frames, double free
This commit is contained in:
@@ -8,7 +8,10 @@ use types::{
|
||||
pub enum KernelError {
|
||||
InvalidSlot(CapSlot),
|
||||
RevokedCapability(CapSlot),
|
||||
PermissionDenied { slot: CapSlot, needed: Permission },
|
||||
PermissionDenied {
|
||||
slot: CapSlot,
|
||||
needed: Permission,
|
||||
},
|
||||
NoFreeSlot,
|
||||
InvalidProcess(ProcessId),
|
||||
WouldBlock,
|
||||
@@ -23,4 +26,9 @@ pub enum KernelError {
|
||||
EndpointFull,
|
||||
ProcessCreateDenied,
|
||||
IrqAlreadyOwned(IrqId),
|
||||
/// Address outside the user window, unaligned, or not owned by the caller.
|
||||
InvalidAddress,
|
||||
/// The target virtual range already holds a mapping.
|
||||
AddressInUse,
|
||||
InvalidRegion,
|
||||
}
|
||||
|
||||
217
crates/domain/src/memory/frame/mod.rs
Normal file
217
crates/domain/src/memory/frame/mod.rs
Normal file
@@ -0,0 +1,217 @@
|
||||
//! Physical frame ownership. The kernel's single source of truth for what
|
||||
//! every frame of the user pool is used for and who is charged for it.
|
||||
|
||||
use types::{
|
||||
address::{PhysicalAddress, VirtualAddress},
|
||||
id::{ProcessId, RegionId},
|
||||
mapping::PagePerms,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests.rs"]
|
||||
mod tests;
|
||||
|
||||
#[cfg(kani)]
|
||||
#[path = "proofs.rs"]
|
||||
mod proofs;
|
||||
|
||||
pub const PAGE_SIZE: usize = 4096;
|
||||
|
||||
/// Frames the kernel can track. The board's user pool must fit.
|
||||
pub const MAX_FRAMES: usize = 4096;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FrameUse {
|
||||
/// Mapped exactly once, at `page` in `owner`'s address space.
|
||||
User {
|
||||
owner: ProcessId,
|
||||
page: u32,
|
||||
perms: PagePerms,
|
||||
},
|
||||
/// Holds part of `owner`'s page tables.
|
||||
PageTable { owner: ProcessId },
|
||||
/// Backs a shared memory region.
|
||||
Region { region: RegionId },
|
||||
}
|
||||
|
||||
impl FrameUse {
|
||||
/// A user mapping at `va`, which must be page aligned.
|
||||
pub fn user(owner: ProcessId, va: VirtualAddress, perms: PagePerms) -> Option<Self> {
|
||||
if !va.raw().is_multiple_of(PAGE_SIZE) {
|
||||
return None;
|
||||
}
|
||||
let page = u32::try_from(va.raw() / PAGE_SIZE).ok()?;
|
||||
Some(Self::User { owner, page, perms })
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FrameTable<const N: usize = MAX_FRAMES> {
|
||||
base: PhysicalAddress,
|
||||
total: usize,
|
||||
frames: [Option<FrameUse>; N],
|
||||
free: usize,
|
||||
}
|
||||
|
||||
impl<const N: usize> FrameTable<N> {
|
||||
pub fn new(base: PhysicalAddress, total: usize) -> Self {
|
||||
let total = total.min(N);
|
||||
Self {
|
||||
base,
|
||||
total,
|
||||
frames: [None; N],
|
||||
free: total,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn base(&self) -> PhysicalAddress {
|
||||
self.base
|
||||
}
|
||||
|
||||
pub fn capacity(&self) -> usize {
|
||||
self.total
|
||||
}
|
||||
|
||||
pub fn free_count(&self) -> usize {
|
||||
self.free
|
||||
}
|
||||
|
||||
fn index_of(&self, pa: PhysicalAddress) -> Option<usize> {
|
||||
let offset = pa.raw().checked_sub(self.base.raw())?;
|
||||
if !offset.is_multiple_of(PAGE_SIZE) {
|
||||
return None;
|
||||
}
|
||||
let idx = offset / PAGE_SIZE;
|
||||
(idx < self.total).then_some(idx)
|
||||
}
|
||||
|
||||
fn address_of(&self, idx: usize) -> PhysicalAddress {
|
||||
self.base.offset(idx)
|
||||
}
|
||||
|
||||
pub fn get(&self, pa: PhysicalAddress) -> Option<&FrameUse> {
|
||||
self.frames[self.index_of(pa)?].as_ref()
|
||||
}
|
||||
|
||||
pub fn alloc(&mut self, usage: FrameUse) -> Option<PhysicalAddress> {
|
||||
let idx = self.frames[..self.total].iter().position(Option::is_none)?;
|
||||
self.frames[idx] = Some(usage);
|
||||
self.free -= 1;
|
||||
Some(self.address_of(idx))
|
||||
}
|
||||
|
||||
/// Allocates `count` physically adjacent frames; frame `i` gets `usage(i)`.
|
||||
pub fn alloc_contiguous(
|
||||
&mut self,
|
||||
count: usize,
|
||||
usage: impl Fn(usize) -> FrameUse,
|
||||
) -> Option<PhysicalAddress> {
|
||||
if count == 0 {
|
||||
return None;
|
||||
}
|
||||
let mut run = 0;
|
||||
let mut start = None;
|
||||
for idx in 0..self.total {
|
||||
if self.frames[idx].is_some() {
|
||||
run = 0;
|
||||
continue;
|
||||
}
|
||||
run += 1;
|
||||
if run == count {
|
||||
start = Some(idx + 1 - count);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let start = start?;
|
||||
for i in 0..count {
|
||||
self.frames[start + i] = Some(usage(i));
|
||||
}
|
||||
self.free -= count;
|
||||
Some(self.address_of(start))
|
||||
}
|
||||
|
||||
/// Frees an allocated frame and reports what it was used for.
|
||||
pub fn free(&mut self, pa: PhysicalAddress) -> Option<FrameUse> {
|
||||
let idx = self.index_of(pa)?;
|
||||
let usage = self.frames[idx].take()?;
|
||||
self.free += 1;
|
||||
Some(usage)
|
||||
}
|
||||
|
||||
/// Changes the use of an allocated frame (e.g. moving it to another
|
||||
/// process). Fails on free frames.
|
||||
pub fn retag(&mut self, pa: PhysicalAddress, usage: FrameUse) -> bool {
|
||||
match self.index_of(pa).and_then(|i| self.frames[i].as_mut()) {
|
||||
Some(slot) => {
|
||||
*slot = usage;
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The frame mapped at `va` in `owner`, if `owner` holds it as a user page.
|
||||
pub fn find_user(
|
||||
&self,
|
||||
owner: ProcessId,
|
||||
va: VirtualAddress,
|
||||
) -> Option<(PhysicalAddress, PagePerms)> {
|
||||
if !va.raw().is_multiple_of(PAGE_SIZE) {
|
||||
return None;
|
||||
}
|
||||
let page = u32::try_from(va.raw() / PAGE_SIZE).ok()?;
|
||||
self.frames[..self.total]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(idx, f)| match f {
|
||||
Some(FrameUse::User {
|
||||
owner: o,
|
||||
page: p,
|
||||
perms,
|
||||
}) if *o == owner && *p == page => Some((self.address_of(idx), *perms)),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Every allocated frame and its use.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (PhysicalAddress, &FrameUse)> + '_ {
|
||||
self.frames[..self.total]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(idx, f)| f.as_ref().map(|u| (self.address_of(idx), u)))
|
||||
}
|
||||
|
||||
/// User pages of `owner` as (frame, virtual address) pairs.
|
||||
pub fn user_frames_of(
|
||||
&self,
|
||||
owner: ProcessId,
|
||||
) -> impl Iterator<Item = (PhysicalAddress, VirtualAddress)> + '_ {
|
||||
self.frames[..self.total]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(move |(idx, f)| match f {
|
||||
Some(FrameUse::User { owner: o, page, .. }) if *o == owner => Some((
|
||||
self.address_of(idx),
|
||||
VirtualAddress::from_raw(*page as usize * PAGE_SIZE),
|
||||
)),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Frees every user and page-table frame charged to `owner`; returns
|
||||
/// how many were freed.
|
||||
pub fn free_owned_by(&mut self, owner: ProcessId) -> usize {
|
||||
let mut freed = 0;
|
||||
for slot in &mut self.frames[..self.total] {
|
||||
let owned = matches!(
|
||||
slot,
|
||||
Some(FrameUse::User { owner: o, .. } | FrameUse::PageTable { owner: o }) if *o == owner
|
||||
);
|
||||
if owned {
|
||||
*slot = None;
|
||||
freed += 1;
|
||||
}
|
||||
}
|
||||
self.free += freed;
|
||||
freed
|
||||
}
|
||||
}
|
||||
195
crates/domain/src/memory/frame/proofs.rs
Normal file
195
crates/domain/src/memory/frame/proofs.rs
Normal file
@@ -0,0 +1,195 @@
|
||||
//! Bounded proofs over every reachable state of a small frame table.
|
||||
//! Each harness starts from an arbitrary *well-formed* table (free count
|
||||
//! matches the number of empty slots) and checks one operation.
|
||||
|
||||
use super::*;
|
||||
use types::address::{PhysicalAddress, VirtualAddress};
|
||||
use types::id::{ProcessId, RegionId};
|
||||
use types::mapping::PagePerms;
|
||||
|
||||
const N: usize = 4;
|
||||
const BASE: usize = 0x8400_0000;
|
||||
|
||||
fn any_perms() -> PagePerms {
|
||||
match kani::any::<u8>() % 3 {
|
||||
0 => PagePerms::ReadOnly,
|
||||
1 => PagePerms::ReadWrite,
|
||||
_ => PagePerms::ReadExecute,
|
||||
}
|
||||
}
|
||||
|
||||
fn any_use() -> FrameUse {
|
||||
// Small id ranges keep owner collisions likely, which is where the
|
||||
// interesting cases are.
|
||||
let owner = ProcessId::from_raw(kani::any::<u16>() % 3);
|
||||
match kani::any::<u8>() % 3 {
|
||||
0 => FrameUse::User {
|
||||
owner,
|
||||
page: kani::any::<u32>() % 4,
|
||||
perms: any_perms(),
|
||||
},
|
||||
1 => FrameUse::PageTable { owner },
|
||||
_ => FrameUse::Region {
|
||||
region: RegionId::from_raw(kani::any::<u16>() % 2),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn any_slot() -> Option<FrameUse> {
|
||||
if kani::any() { Some(any_use()) } else { None }
|
||||
}
|
||||
|
||||
fn any_table() -> FrameTable<N> {
|
||||
let frames = [any_slot(), any_slot(), any_slot(), any_slot()];
|
||||
let free = frames.iter().filter(|f| f.is_none()).count();
|
||||
FrameTable {
|
||||
base: PhysicalAddress::from_raw(BASE),
|
||||
total: N,
|
||||
frames,
|
||||
free,
|
||||
}
|
||||
}
|
||||
|
||||
fn well_formed(t: &FrameTable<N>) -> bool {
|
||||
t.free == t.frames[..t.total].iter().filter(|f| f.is_none()).count()
|
||||
}
|
||||
|
||||
fn any_pa() -> PhysicalAddress {
|
||||
// Anything from below the pool to past its end, aligned or not.
|
||||
PhysicalAddress::from_raw(BASE - 4096 + kani::any::<usize>() % ((N + 2) * 4096))
|
||||
}
|
||||
|
||||
#[kani::proof]
|
||||
#[kani::unwind(6)]
|
||||
fn alloc_only_hands_out_free_frames() {
|
||||
let mut t = any_table();
|
||||
let before = t.frames;
|
||||
let usage = any_use();
|
||||
match t.alloc(usage) {
|
||||
Some(pa) => {
|
||||
let idx = (pa.raw() - BASE) / PAGE_SIZE;
|
||||
assert!(idx < N);
|
||||
assert!(before[idx].is_none());
|
||||
assert!(t.frames[idx] == Some(usage));
|
||||
for i in 0..N {
|
||||
if i != idx {
|
||||
assert!(t.frames[i] == before[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
assert!(before.iter().all(Option::is_some));
|
||||
assert!(t.frames == before);
|
||||
}
|
||||
}
|
||||
assert!(well_formed(&t));
|
||||
}
|
||||
|
||||
#[kani::proof]
|
||||
#[kani::unwind(6)]
|
||||
fn free_releases_exactly_one_allocated_frame_or_nothing() {
|
||||
let mut t = any_table();
|
||||
let before = t.frames;
|
||||
let pa = any_pa();
|
||||
match t.free(pa) {
|
||||
Some(usage) => {
|
||||
let idx = (pa.raw() - BASE) / PAGE_SIZE;
|
||||
assert!(pa.raw() % PAGE_SIZE == 0);
|
||||
assert!(before[idx] == Some(usage));
|
||||
assert!(t.frames[idx].is_none());
|
||||
for i in 0..N {
|
||||
if i != idx {
|
||||
assert!(t.frames[i] == before[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => assert!(t.frames == before),
|
||||
}
|
||||
assert!(well_formed(&t));
|
||||
}
|
||||
|
||||
#[kani::proof]
|
||||
#[kani::unwind(6)]
|
||||
fn retag_never_allocates() {
|
||||
let mut t = any_table();
|
||||
let before = t.frames;
|
||||
let pa = any_pa();
|
||||
let usage = any_use();
|
||||
let changed = t.retag(pa, usage);
|
||||
for i in 0..N {
|
||||
if before[i].is_none() {
|
||||
assert!(t.frames[i].is_none());
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
assert!(t.frames == before);
|
||||
}
|
||||
assert!(well_formed(&t));
|
||||
}
|
||||
|
||||
#[kani::proof]
|
||||
#[kani::unwind(6)]
|
||||
fn free_owned_by_spares_everyone_else() {
|
||||
let mut t = any_table();
|
||||
let before = t.frames;
|
||||
let owner = ProcessId::from_raw(kani::any::<u16>() % 3);
|
||||
t.free_owned_by(owner);
|
||||
for i in 0..N {
|
||||
let was_owned = matches!(
|
||||
before[i],
|
||||
Some(FrameUse::User { owner: o, .. } | FrameUse::PageTable { owner: o }) if o == owner
|
||||
);
|
||||
if was_owned {
|
||||
assert!(t.frames[i].is_none());
|
||||
} else {
|
||||
assert!(t.frames[i] == before[i]);
|
||||
}
|
||||
}
|
||||
assert!(well_formed(&t));
|
||||
}
|
||||
|
||||
#[kani::proof]
|
||||
#[kani::unwind(6)]
|
||||
fn find_user_only_reports_the_owners_mapping() {
|
||||
let t = any_table();
|
||||
let owner = ProcessId::from_raw(kani::any::<u16>() % 3);
|
||||
let va = VirtualAddress::from_raw(kani::any::<usize>() % (5 * 4096));
|
||||
if let Some((pa, perms)) = t.find_user(owner, va) {
|
||||
let idx = (pa.raw() - BASE) / PAGE_SIZE;
|
||||
assert!(idx < N);
|
||||
assert!(va.raw() % PAGE_SIZE == 0);
|
||||
assert!(
|
||||
t.frames[idx]
|
||||
== Some(FrameUse::User {
|
||||
owner,
|
||||
page: (va.raw() / PAGE_SIZE) as u32,
|
||||
perms,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[kani::proof]
|
||||
#[kani::unwind(6)]
|
||||
fn alloc_contiguous_only_claims_a_free_run() {
|
||||
let mut t = any_table();
|
||||
let before = t.frames;
|
||||
let count = kani::any::<usize>() % (N + 2);
|
||||
let region = RegionId::from_raw(1);
|
||||
match t.alloc_contiguous(count, |_| FrameUse::Region { region }) {
|
||||
Some(pa) => {
|
||||
let start = (pa.raw() - BASE) / PAGE_SIZE;
|
||||
assert!(count > 0 && start + count <= N);
|
||||
for i in 0..N {
|
||||
if i >= start && i < start + count {
|
||||
assert!(before[i].is_none());
|
||||
assert!(t.frames[i] == Some(FrameUse::Region { region }));
|
||||
} else {
|
||||
assert!(t.frames[i] == before[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => assert!(t.frames == before),
|
||||
}
|
||||
assert!(well_formed(&t));
|
||||
}
|
||||
188
crates/domain/src/memory/frame/tests.rs
Normal file
188
crates/domain/src/memory/frame/tests.rs
Normal file
@@ -0,0 +1,188 @@
|
||||
use super::*;
|
||||
use types::address::{PhysicalAddress, VirtualAddress};
|
||||
use types::id::{ProcessId, RegionId};
|
||||
use types::mapping::PagePerms;
|
||||
|
||||
const BASE: usize = 0x8400_0000;
|
||||
|
||||
fn table() -> FrameTable<8> {
|
||||
FrameTable::new(PhysicalAddress::from_raw(BASE), 8)
|
||||
}
|
||||
|
||||
fn pid(n: u16) -> ProcessId {
|
||||
ProcessId::from_raw(n)
|
||||
}
|
||||
|
||||
fn va(addr: usize) -> VirtualAddress {
|
||||
VirtualAddress::from_raw(addr)
|
||||
}
|
||||
|
||||
fn user(owner: u16, addr: usize) -> FrameUse {
|
||||
FrameUse::user(pid(owner), va(addr), PagePerms::ReadWrite).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_table_has_every_frame_free() {
|
||||
let t = table();
|
||||
assert_eq!(t.free_count(), 8);
|
||||
assert_eq!(t.capacity(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn total_is_clamped_to_capacity() {
|
||||
let t: FrameTable<4> = FrameTable::new(PhysicalAddress::from_raw(BASE), 100);
|
||||
assert_eq!(t.free_count(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_use_requires_page_aligned_va() {
|
||||
assert!(FrameUse::user(pid(0), va(0x1234), PagePerms::ReadOnly).is_none());
|
||||
assert!(FrameUse::user(pid(0), va(0x1000), PagePerms::ReadOnly).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alloc_hands_out_distinct_frames_inside_the_pool() {
|
||||
let mut t = table();
|
||||
let a = t.alloc(user(0, 0x1000)).unwrap();
|
||||
let b = t.alloc(user(0, 0x2000)).unwrap();
|
||||
assert_ne!(a, b);
|
||||
for pa in [a, b] {
|
||||
assert!(pa.raw() >= BASE && pa.raw() < BASE + 8 * 4096);
|
||||
assert_eq!(pa.raw() % 4096, 0);
|
||||
}
|
||||
assert_eq!(t.free_count(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alloc_fails_when_exhausted() {
|
||||
let mut t = table();
|
||||
for i in 0..8 {
|
||||
t.alloc(user(0, (i + 1) * 0x1000)).unwrap();
|
||||
}
|
||||
assert!(t.alloc(user(0, 0x9000)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn free_returns_the_frame_and_its_use() {
|
||||
let mut t = table();
|
||||
let pa = t.alloc(user(3, 0x5000)).unwrap();
|
||||
assert_eq!(t.free(pa), Some(user(3, 0x5000)));
|
||||
assert_eq!(t.free_count(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn freeing_a_free_frame_is_rejected() {
|
||||
let mut t = table();
|
||||
let pa = t.alloc(user(0, 0x1000)).unwrap();
|
||||
t.free(pa).unwrap();
|
||||
assert_eq!(t.free(pa), None);
|
||||
assert_eq!(t.free_count(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn freeing_outside_the_pool_or_unaligned_is_rejected() {
|
||||
let mut t = table();
|
||||
t.alloc(user(0, 0x1000)).unwrap();
|
||||
assert_eq!(t.free(PhysicalAddress::from_raw(BASE - 4096)), None);
|
||||
assert_eq!(t.free(PhysicalAddress::from_raw(BASE + 8 * 4096)), None);
|
||||
assert_eq!(t.free(PhysicalAddress::from_raw(BASE + 1)), None);
|
||||
assert_eq!(t.free_count(), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_user_matches_owner_and_va() {
|
||||
let mut t = table();
|
||||
let pa = t.alloc(user(1, 0x3000)).unwrap();
|
||||
assert_eq!(
|
||||
t.find_user(pid(1), va(0x3000)),
|
||||
Some((pa, PagePerms::ReadWrite))
|
||||
);
|
||||
assert_eq!(t.find_user(pid(2), va(0x3000)), None);
|
||||
assert_eq!(t.find_user(pid(1), va(0x4000)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retag_moves_a_frame_to_a_new_use() {
|
||||
let mut t = table();
|
||||
let pa = t.alloc(user(1, 0x3000)).unwrap();
|
||||
assert!(t.retag(pa, user(2, 0x7000)));
|
||||
assert_eq!(t.find_user(pid(1), va(0x3000)), None);
|
||||
assert_eq!(t.find_user(pid(2), va(0x7000)).map(|(p, _)| p), Some(pa));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retag_of_a_free_frame_is_rejected() {
|
||||
let mut t = table();
|
||||
let pa = t.alloc(user(1, 0x3000)).unwrap();
|
||||
t.free(pa);
|
||||
assert!(!t.retag(pa, user(2, 0x7000)));
|
||||
assert_eq!(t.free_count(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alloc_contiguous_returns_adjacent_frames() {
|
||||
let mut t = table();
|
||||
let base = t
|
||||
.alloc_contiguous(3, |_| FrameUse::Region {
|
||||
region: RegionId::from_raw(0),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(t.free_count(), 5);
|
||||
for i in 0..3 {
|
||||
assert_eq!(
|
||||
t.get(base.offset(i)),
|
||||
Some(&FrameUse::Region {
|
||||
region: RegionId::from_raw(0)
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alloc_contiguous_fails_without_a_long_enough_run() {
|
||||
let mut t: FrameTable<4> = FrameTable::new(PhysicalAddress::from_raw(BASE), 4);
|
||||
let a = t.alloc(user(0, 0x1000)).unwrap();
|
||||
let _b = t.alloc(user(0, 0x2000)).unwrap();
|
||||
let _c = t.alloc(user(0, 0x3000)).unwrap();
|
||||
t.free(a);
|
||||
// free frames: 0 and 3 — no run of 2
|
||||
assert!(
|
||||
t.alloc_contiguous(2, |_| FrameUse::PageTable { owner: pid(0) })
|
||||
.is_none()
|
||||
);
|
||||
assert_eq!(t.free_count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn free_owned_by_releases_only_that_processes_frames() {
|
||||
let mut t = table();
|
||||
t.alloc(user(1, 0x1000)).unwrap();
|
||||
t.alloc(FrameUse::PageTable { owner: pid(1) }).unwrap();
|
||||
let other = t.alloc(user(2, 0x1000)).unwrap();
|
||||
let region = t
|
||||
.alloc(FrameUse::Region {
|
||||
region: RegionId::from_raw(1),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(t.free_owned_by(pid(1)), 2);
|
||||
assert_eq!(t.free_count(), 6);
|
||||
assert!(t.get(other).is_some());
|
||||
assert!(t.get(region).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_frames_of_lists_mappings_of_one_process() {
|
||||
let mut t = table();
|
||||
t.alloc(user(1, 0x1000)).unwrap();
|
||||
t.alloc(user(2, 0x2000)).unwrap();
|
||||
t.alloc(user(1, 0x3000)).unwrap();
|
||||
t.alloc(FrameUse::PageTable { owner: pid(1) }).unwrap();
|
||||
let mut vas = [0usize; 4];
|
||||
let mut n = 0;
|
||||
for (_, v) in t.user_frames_of(pid(1)) {
|
||||
vas[n] = v.raw();
|
||||
n += 1;
|
||||
}
|
||||
assert_eq!(&vas[..n], &[0x1000, 0x3000]);
|
||||
}
|
||||
@@ -1,58 +1,83 @@
|
||||
use types::{
|
||||
address::PhysicalAddress,
|
||||
address::{PhysicalAddress, VirtualAddress},
|
||||
bounded::PageCount,
|
||||
generation::Generation,
|
||||
id::{ProcessId, RegionId},
|
||||
mapping::PagePerms,
|
||||
};
|
||||
|
||||
pub mod page_pool;
|
||||
pub mod process_memory;
|
||||
pub mod frame;
|
||||
pub mod quota;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "quota_tests.rs"]
|
||||
mod quota_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests.rs"]
|
||||
mod tests;
|
||||
|
||||
const MAX_REGIONS: usize = 256;
|
||||
pub const MAX_REGIONS: usize = 64;
|
||||
pub const MAX_REGION_MAPPINGS: usize = 4;
|
||||
|
||||
pub struct MemoryRegion {
|
||||
id: RegionId,
|
||||
owner: ProcessId,
|
||||
base: PhysicalAddress,
|
||||
size: PageCount,
|
||||
generation: Generation,
|
||||
/// Where a region's frames come from.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RegionBacking {
|
||||
/// Frames from the user pool, freed with the region.
|
||||
Pool,
|
||||
/// Device MMIO; never freed.
|
||||
Device,
|
||||
}
|
||||
|
||||
impl MemoryRegion {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct RegionMapping {
|
||||
pub process: ProcessId,
|
||||
pub va: VirtualAddress,
|
||||
pub perms: PagePerms,
|
||||
}
|
||||
|
||||
/// Physically contiguous memory that may be mapped into several address
|
||||
/// spaces. `owner` is charged for pool-backed frames.
|
||||
pub struct Region {
|
||||
owner: ProcessId,
|
||||
backing: RegionBacking,
|
||||
base: PhysicalAddress,
|
||||
pages: PageCount,
|
||||
generation: Generation,
|
||||
mappings: [Option<RegionMapping>; MAX_REGION_MAPPINGS],
|
||||
}
|
||||
|
||||
impl Region {
|
||||
pub fn new(
|
||||
id: RegionId,
|
||||
owner: ProcessId,
|
||||
backing: RegionBacking,
|
||||
base: PhysicalAddress,
|
||||
size: PageCount,
|
||||
generation: Generation,
|
||||
pages: PageCount,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
owner,
|
||||
backing,
|
||||
base,
|
||||
size,
|
||||
generation,
|
||||
pages,
|
||||
generation: Generation::initial(),
|
||||
mappings: [None; MAX_REGION_MAPPINGS],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> RegionId {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn owner(&self) -> ProcessId {
|
||||
self.owner
|
||||
}
|
||||
|
||||
pub fn backing(&self) -> RegionBacking {
|
||||
self.backing
|
||||
}
|
||||
|
||||
pub fn base(&self) -> PhysicalAddress {
|
||||
self.base
|
||||
}
|
||||
|
||||
pub fn size(&self) -> PageCount {
|
||||
self.size
|
||||
pub fn pages(&self) -> PageCount {
|
||||
self.pages
|
||||
}
|
||||
|
||||
pub fn generation(&self) -> &Generation {
|
||||
@@ -62,10 +87,43 @@ impl MemoryRegion {
|
||||
pub fn revoke(&mut self) {
|
||||
self.generation = self.generation.next();
|
||||
}
|
||||
|
||||
pub fn mappings(&self) -> impl Iterator<Item = &RegionMapping> {
|
||||
self.mappings.iter().flatten()
|
||||
}
|
||||
|
||||
/// Records that `mapping.process` maps this region. Each process maps a
|
||||
/// region at most once.
|
||||
pub fn add_mapping(&mut self, mapping: RegionMapping) -> bool {
|
||||
if self.mappings().any(|m| m.process == mapping.process) {
|
||||
return false;
|
||||
}
|
||||
match self.mappings.iter_mut().find(|m| m.is_none()) {
|
||||
Some(slot) => {
|
||||
*slot = Some(mapping);
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_mapping(&mut self, process: ProcessId) -> Option<RegionMapping> {
|
||||
self.mappings
|
||||
.iter_mut()
|
||||
.find(|m| m.is_some_and(|m| m.process == process))?
|
||||
.take()
|
||||
}
|
||||
|
||||
/// Whether `va` falls inside this region's mapping in `process`.
|
||||
pub fn covers(&self, process: ProcessId, va: VirtualAddress) -> bool {
|
||||
let len = self.pages.value() as usize * frame::PAGE_SIZE;
|
||||
self.mappings()
|
||||
.any(|m| m.process == process && va.raw() >= m.va.raw() && va.raw() - m.va.raw() < len)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RegionTable {
|
||||
regions: [Option<MemoryRegion>; MAX_REGIONS],
|
||||
regions: [Option<Region>; MAX_REGIONS],
|
||||
}
|
||||
|
||||
impl RegionTable {
|
||||
@@ -75,50 +133,60 @@ impl RegionTable {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, region: MemoryRegion) -> Option<RegionId> {
|
||||
/// The id the next `insert` will return.
|
||||
pub fn next_free_id(&self) -> Option<RegionId> {
|
||||
let idx = self.regions.iter().position(|s| s.is_none())?;
|
||||
Some(RegionId::from_raw(idx as u16))
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, region: Region) -> Option<RegionId> {
|
||||
let idx = self.regions.iter().position(|s| s.is_none())?;
|
||||
let id = RegionId::from_raw(idx as u16);
|
||||
self.regions[idx] = Some(region);
|
||||
Some(id)
|
||||
Some(RegionId::from_raw(idx as u16))
|
||||
}
|
||||
|
||||
pub fn get(&self, id: RegionId) -> Option<&MemoryRegion> {
|
||||
self.regions.get(id.raw() as usize).and_then(|s| s.as_ref())
|
||||
pub fn get(&self, id: RegionId) -> Option<&Region> {
|
||||
self.regions.get(id.raw() as usize)?.as_ref()
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, id: RegionId) -> Option<&mut MemoryRegion> {
|
||||
self.regions
|
||||
.get_mut(id.raw() as usize)
|
||||
.and_then(|s| s.as_mut())
|
||||
pub fn get_mut(&mut self, id: RegionId) -> Option<&mut Region> {
|
||||
self.regions.get_mut(id.raw() as usize)?.as_mut()
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, id: RegionId) -> Option<MemoryRegion> {
|
||||
self.regions
|
||||
.get_mut(id.raw() as usize)
|
||||
.and_then(|s| s.take())
|
||||
pub fn remove(&mut self, id: RegionId) -> Option<Region> {
|
||||
self.regions.get_mut(id.raw() as usize)?.take()
|
||||
}
|
||||
|
||||
pub fn find_by_base(
|
||||
&self,
|
||||
owner: ProcessId,
|
||||
base: PhysicalAddress,
|
||||
) -> Option<(RegionId, &MemoryRegion)> {
|
||||
for (i, slot) in self.regions.iter().enumerate() {
|
||||
if let Some(region) = slot
|
||||
&& region.owner() == owner
|
||||
&& region.base() == base
|
||||
{
|
||||
return Some((RegionId::from_raw(i as u16), region));
|
||||
}
|
||||
}
|
||||
None
|
||||
pub fn ids_owned_by(&self, owner: ProcessId) -> impl Iterator<Item = RegionId> + '_ {
|
||||
self.regions.iter().enumerate().filter_map(move |(i, r)| {
|
||||
r.as_ref()
|
||||
.filter(|r| r.owner == owner)
|
||||
.map(|_| RegionId::from_raw(i as u16))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn regions_for_process(&self, process: ProcessId) -> impl Iterator<Item = &MemoryRegion> {
|
||||
pub fn iter(&self) -> impl Iterator<Item = (RegionId, &Region)> {
|
||||
self.regions
|
||||
.iter()
|
||||
.filter_map(|s| s.as_ref())
|
||||
.filter(move |r| r.owner() == process)
|
||||
.enumerate()
|
||||
.filter_map(|(i, r)| r.as_ref().map(|r| (RegionId::from_raw(i as u16), r)))
|
||||
}
|
||||
|
||||
/// Every region, by id, so callers can drop a process's mappings.
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = (RegionId, &mut Region)> {
|
||||
self.regions
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.filter_map(|(i, r)| r.as_mut().map(|r| (RegionId::from_raw(i as u16), r)))
|
||||
}
|
||||
|
||||
/// The region whose mapping in `process` covers `va`.
|
||||
pub fn covering(&self, process: ProcessId, va: VirtualAddress) -> Option<RegionId> {
|
||||
self.regions.iter().enumerate().find_map(|(i, r)| {
|
||||
r.as_ref()
|
||||
.filter(|r| r.covers(process, va))
|
||||
.map(|_| RegionId::from_raw(i as u16))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
use types::address::PhysicalAddress;
|
||||
|
||||
const MAX_PAGES: usize = 16384;
|
||||
|
||||
pub struct PagePool {
|
||||
pages: [bool; MAX_PAGES],
|
||||
base: PhysicalAddress,
|
||||
total: usize,
|
||||
free_count: usize,
|
||||
}
|
||||
|
||||
impl PagePool {
|
||||
pub fn new(base: PhysicalAddress, total_pages: usize) -> Self {
|
||||
let mut pages = [false; MAX_PAGES];
|
||||
pages[..total_pages].fill(true);
|
||||
Self {
|
||||
pages,
|
||||
base,
|
||||
total: total_pages,
|
||||
free_count: total_pages,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn allocate(&mut self, count: usize) -> Option<PhysicalAddress> {
|
||||
let start = self.find_contiguous(count)?;
|
||||
for i in start..start + count {
|
||||
self.pages[i] = false;
|
||||
}
|
||||
self.free_count -= count;
|
||||
Some(self.base.offset(start))
|
||||
}
|
||||
|
||||
pub fn free(&mut self, addr: PhysicalAddress, count: usize) -> bool {
|
||||
if addr.raw() < self.base.raw() {
|
||||
return false;
|
||||
}
|
||||
let start = (addr.raw() - self.base.raw()) / 4096;
|
||||
if start + count > self.total {
|
||||
return false;
|
||||
}
|
||||
for i in start..start + count {
|
||||
self.pages[i] = true;
|
||||
}
|
||||
self.free_count += count;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn free_count(&self) -> usize {
|
||||
self.free_count
|
||||
}
|
||||
|
||||
fn find_contiguous(&self, count: usize) -> Option<usize> {
|
||||
let mut run = 0;
|
||||
for i in 0..self.total {
|
||||
if self.pages[i] {
|
||||
run += 1;
|
||||
if run == count {
|
||||
return Some(i + 1 - count);
|
||||
}
|
||||
} else {
|
||||
run = 0;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
use types::address::{PhysicalAddress, VirtualAddress};
|
||||
use types::id::ProcessId;
|
||||
|
||||
use crate::error::KernelError;
|
||||
|
||||
const USER_HEAP_START: usize = 0x40000;
|
||||
const PAGE_SIZE: usize = 4096;
|
||||
const MAX_ALLOCATIONS: usize = 16;
|
||||
|
||||
pub struct ProcessMemory {
|
||||
process: ProcessId,
|
||||
page_count: usize,
|
||||
limit: usize,
|
||||
next_heap_va: usize,
|
||||
allocations: [(PhysicalAddress, usize); MAX_ALLOCATIONS],
|
||||
alloc_count: usize,
|
||||
}
|
||||
|
||||
impl ProcessMemory {
|
||||
pub const DEFAULT_PAGE_LIMIT: usize = 1024;
|
||||
|
||||
pub fn new(process: ProcessId, limit: usize) -> Self {
|
||||
Self {
|
||||
process,
|
||||
page_count: 0,
|
||||
limit,
|
||||
next_heap_va: USER_HEAP_START,
|
||||
allocations: [(PhysicalAddress::from_raw(0), 0); MAX_ALLOCATIONS],
|
||||
alloc_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn alloc_user_va(&mut self, pages: usize) -> VirtualAddress {
|
||||
let va = self.next_heap_va;
|
||||
self.next_heap_va += pages * PAGE_SIZE;
|
||||
VirtualAddress::from_raw(va)
|
||||
}
|
||||
|
||||
pub fn record_allocation(&mut self, pa: PhysicalAddress, count: usize) {
|
||||
if self.alloc_count < MAX_ALLOCATIONS {
|
||||
self.allocations[self.alloc_count] = (pa, count);
|
||||
self.alloc_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn allocations(&self) -> &[(PhysicalAddress, usize)] {
|
||||
&self.allocations[..self.alloc_count]
|
||||
}
|
||||
|
||||
pub fn process(&self) -> ProcessId {
|
||||
self.process
|
||||
}
|
||||
|
||||
pub fn page_count(&self) -> usize {
|
||||
self.page_count
|
||||
}
|
||||
|
||||
pub fn limit(&self) -> usize {
|
||||
self.limit
|
||||
}
|
||||
|
||||
pub fn grant(&mut self, count: usize) -> Result<(), KernelError> {
|
||||
if self.page_count + count > self.limit {
|
||||
return Err(KernelError::OutOfMemory);
|
||||
}
|
||||
self.page_count += count;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn release(&mut self, count: usize) -> Result<(), KernelError> {
|
||||
if count > self.page_count {
|
||||
return Err(KernelError::OutOfMemory);
|
||||
}
|
||||
self.page_count -= count;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
57
crates/domain/src/memory/quota.rs
Normal file
57
crates/domain/src/memory/quota.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
/// Per-process page budget. Every frame charged to a process (user pages
|
||||
/// and its page tables) counts against it.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Quota {
|
||||
used: usize,
|
||||
limit: usize,
|
||||
}
|
||||
|
||||
impl Quota {
|
||||
pub const DEFAULT_LIMIT: usize = 1024;
|
||||
|
||||
pub const fn new(limit: usize) -> Self {
|
||||
Self { used: 0, limit }
|
||||
}
|
||||
|
||||
pub fn used(&self) -> usize {
|
||||
self.used
|
||||
}
|
||||
|
||||
pub fn limit(&self) -> usize {
|
||||
self.limit
|
||||
}
|
||||
|
||||
pub fn charge(&mut self, pages: usize) -> bool {
|
||||
match self.used.checked_add(pages) {
|
||||
Some(total) if total <= self.limit => {
|
||||
self.used = total;
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn uncharge(&mut self, pages: usize) -> bool {
|
||||
match self.used.checked_sub(pages) {
|
||||
Some(rest) => {
|
||||
self.used = rest;
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_limit(&mut self, limit: usize) -> bool {
|
||||
if limit < self.used {
|
||||
return false;
|
||||
}
|
||||
self.limit = limit;
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Quota {
|
||||
fn default() -> Self {
|
||||
Self::new(Self::DEFAULT_LIMIT)
|
||||
}
|
||||
}
|
||||
37
crates/domain/src/memory/quota_tests.rs
Normal file
37
crates/domain/src/memory/quota_tests.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use super::quota::Quota;
|
||||
|
||||
#[test]
|
||||
fn charge_within_limit_succeeds() {
|
||||
let mut q = Quota::new(4);
|
||||
assert!(q.charge(3));
|
||||
assert_eq!(q.used(), 3);
|
||||
assert!(q.charge(1));
|
||||
assert_eq!(q.used(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn charge_past_limit_fails_and_changes_nothing() {
|
||||
let mut q = Quota::new(4);
|
||||
q.charge(3);
|
||||
assert!(!q.charge(2));
|
||||
assert_eq!(q.used(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uncharge_cannot_underflow() {
|
||||
let mut q = Quota::new(4);
|
||||
q.charge(1);
|
||||
assert!(!q.uncharge(2));
|
||||
assert_eq!(q.used(), 1);
|
||||
assert!(q.uncharge(1));
|
||||
assert_eq!(q.used(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn limit_can_be_raised_but_not_below_usage() {
|
||||
let mut q = Quota::new(4);
|
||||
q.charge(3);
|
||||
assert!(!q.set_limit(2));
|
||||
assert!(q.set_limit(10));
|
||||
assert!(q.charge(7));
|
||||
}
|
||||
@@ -1,221 +1,117 @@
|
||||
use super::page_pool::PagePool;
|
||||
use super::process_memory::ProcessMemory;
|
||||
use super::*;
|
||||
use types::address::PhysicalAddress;
|
||||
use types::address::{PhysicalAddress, VirtualAddress};
|
||||
use types::bounded::PageCount;
|
||||
use types::generation::Generation;
|
||||
use types::id::{ProcessId, RegionId};
|
||||
use types::mapping::PagePerms;
|
||||
|
||||
fn base() -> PhysicalAddress {
|
||||
PhysicalAddress::from_raw(0x8000_0000)
|
||||
fn pid(n: u16) -> ProcessId {
|
||||
ProcessId::from_raw(n)
|
||||
}
|
||||
|
||||
fn process(id: u16) -> ProcessId {
|
||||
ProcessId::from_raw(id)
|
||||
fn va(addr: usize) -> VirtualAddress {
|
||||
VirtualAddress::from_raw(addr)
|
||||
}
|
||||
|
||||
fn pages(n: u16) -> PageCount {
|
||||
PageCount::new(n).unwrap()
|
||||
fn pool_region(owner: u16, pages: u16) -> Region {
|
||||
Region::new(
|
||||
pid(owner),
|
||||
RegionBacking::Pool,
|
||||
PhysicalAddress::from_raw(0x8400_0000),
|
||||
PageCount::new(pages).unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
// --- PagePool tests ---
|
||||
|
||||
#[test]
|
||||
fn new_pool_has_all_pages_free() {
|
||||
let pool = PagePool::new(base(), 100);
|
||||
assert_eq!(pool.free_count(), 100);
|
||||
fn mapping(process: u16, addr: usize) -> RegionMapping {
|
||||
RegionMapping {
|
||||
process: pid(process),
|
||||
va: va(addr),
|
||||
perms: PagePerms::ReadWrite,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocate_reduces_free_count() {
|
||||
let mut pool = PagePool::new(base(), 100);
|
||||
pool.allocate(10).unwrap();
|
||||
assert_eq!(pool.free_count(), 90);
|
||||
fn new_region_has_no_mappings() {
|
||||
let r = pool_region(0, 4);
|
||||
assert_eq!(r.mappings().count(), 0);
|
||||
assert_eq!(r.owner(), pid(0));
|
||||
assert_eq!(r.backing(), RegionBacking::Pool);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocate_returns_base_aligned_address() {
|
||||
let mut pool = PagePool::new(base(), 100);
|
||||
let addr = pool.allocate(1).unwrap();
|
||||
assert_eq!(addr, base());
|
||||
fn a_process_maps_a_region_at_most_once() {
|
||||
let mut r = pool_region(0, 4);
|
||||
assert!(r.add_mapping(mapping(1, 0x30000)));
|
||||
assert!(!r.add_mapping(mapping(1, 0x50000)));
|
||||
assert!(r.add_mapping(mapping(2, 0x30000)));
|
||||
assert_eq!(r.mappings().count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocate_sequential_returns_contiguous() {
|
||||
let mut pool = PagePool::new(base(), 100);
|
||||
let a = pool.allocate(4).unwrap();
|
||||
let b = pool.allocate(4).unwrap();
|
||||
assert_eq!(a, base());
|
||||
assert_eq!(b, base().offset(4));
|
||||
fn mapping_slots_are_bounded() {
|
||||
let mut r = pool_region(0, 1);
|
||||
for p in 0..MAX_REGION_MAPPINGS as u16 {
|
||||
assert!(r.add_mapping(mapping(p, 0x30000)));
|
||||
}
|
||||
assert!(!r.add_mapping(mapping(99, 0x30000)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocate_too_many_returns_none() {
|
||||
let mut pool = PagePool::new(base(), 10);
|
||||
assert!(pool.allocate(11).is_none());
|
||||
fn remove_mapping_returns_it() {
|
||||
let mut r = pool_region(0, 4);
|
||||
r.add_mapping(mapping(1, 0x30000));
|
||||
assert_eq!(r.remove_mapping(pid(1)), Some(mapping(1, 0x30000)));
|
||||
assert_eq!(r.remove_mapping(pid(1)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn free_then_reallocate() {
|
||||
let mut pool = PagePool::new(base(), 10);
|
||||
let addr = pool.allocate(5).unwrap();
|
||||
assert_eq!(pool.free_count(), 5);
|
||||
assert!(pool.free(addr, 5));
|
||||
assert_eq!(pool.free_count(), 10);
|
||||
let addr2 = pool.allocate(5).unwrap();
|
||||
assert_eq!(addr2, base());
|
||||
fn covers_checks_every_page_of_a_mapping() {
|
||||
let mut r = pool_region(0, 4);
|
||||
r.add_mapping(mapping(1, 0x30000));
|
||||
assert!(r.covers(pid(1), va(0x30000)));
|
||||
assert!(r.covers(pid(1), va(0x33000)));
|
||||
assert!(!r.covers(pid(1), va(0x34000)));
|
||||
assert!(!r.covers(pid(1), va(0x2F000)));
|
||||
assert!(!r.covers(pid(2), va(0x30000)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn free_out_of_range_returns_false() {
|
||||
let mut pool = PagePool::new(base(), 10);
|
||||
assert!(!pool.free(PhysicalAddress::from_raw(0), 1));
|
||||
fn region_table_insert_get_remove() {
|
||||
let mut t = RegionTable::new();
|
||||
let id = t.insert(pool_region(0, 2)).unwrap();
|
||||
assert_eq!(t.get(id).map(|r| r.owner()), Some(pid(0)));
|
||||
assert!(t.remove(id).is_some());
|
||||
assert!(t.get(id).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocate_fragmented_finds_gap() {
|
||||
let mut pool = PagePool::new(base(), 10);
|
||||
let a = pool.allocate(3).unwrap();
|
||||
let _b = pool.allocate(3).unwrap();
|
||||
let _c = pool.allocate(3).unwrap();
|
||||
pool.free(a, 3);
|
||||
// now pages 0-2 are free, 3-8 allocated, 9 free
|
||||
// asking for 3 contiguous should find 0-2
|
||||
let d = pool.allocate(3).unwrap();
|
||||
assert_eq!(d, base());
|
||||
fn region_table_is_bounded() {
|
||||
let mut t = RegionTable::new();
|
||||
for _ in 0..MAX_REGIONS {
|
||||
assert!(t.insert(pool_region(0, 1)).is_some());
|
||||
}
|
||||
assert!(t.insert(pool_region(0, 1)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocate_exact_remaining() {
|
||||
let mut pool = PagePool::new(base(), 5);
|
||||
let addr = pool.allocate(5).unwrap();
|
||||
assert_eq!(addr, base());
|
||||
assert_eq!(pool.free_count(), 0);
|
||||
assert!(pool.allocate(1).is_none());
|
||||
}
|
||||
|
||||
// --- ProcessMemory tests ---
|
||||
|
||||
#[test]
|
||||
fn new_process_memory_has_zero_pages() {
|
||||
let pm = ProcessMemory::new(process(0), 100);
|
||||
assert_eq!(pm.page_count(), 0);
|
||||
assert_eq!(pm.limit(), 100);
|
||||
fn region_table_finds_regions_owned_by_a_process() {
|
||||
let mut t = RegionTable::new();
|
||||
let a = t.insert(pool_region(1, 1)).unwrap();
|
||||
t.insert(pool_region(2, 1)).unwrap();
|
||||
let c = t.insert(pool_region(1, 1)).unwrap();
|
||||
let mut ids = [RegionId::from_raw(u16::MAX); 4];
|
||||
let mut n = 0;
|
||||
for id in t.ids_owned_by(pid(1)) {
|
||||
ids[n] = id;
|
||||
n += 1;
|
||||
}
|
||||
assert_eq!(&ids[..n], &[a, c]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grant_pages_within_limit() {
|
||||
let mut pm = ProcessMemory::new(process(0), 100);
|
||||
assert!(pm.grant(10).is_ok());
|
||||
assert_eq!(pm.page_count(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grant_pages_exceeding_limit_fails() {
|
||||
let mut pm = ProcessMemory::new(process(0), 10);
|
||||
assert!(pm.grant(11).is_err());
|
||||
assert_eq!(pm.page_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_pages_reduces_count() {
|
||||
let mut pm = ProcessMemory::new(process(0), 100);
|
||||
pm.grant(20).unwrap();
|
||||
assert!(pm.release(5).is_ok());
|
||||
assert_eq!(pm.page_count(), 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_more_than_owned_fails() {
|
||||
let mut pm = ProcessMemory::new(process(0), 100);
|
||||
pm.grant(5).unwrap();
|
||||
assert!(pm.release(10).is_err());
|
||||
assert_eq!(pm.page_count(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grant_up_to_exact_limit() {
|
||||
let mut pm = ProcessMemory::new(process(0), 10);
|
||||
pm.grant(10).unwrap();
|
||||
assert!(pm.grant(1).is_err());
|
||||
}
|
||||
|
||||
// --- MemoryRegion tests ---
|
||||
|
||||
#[test]
|
||||
fn memory_region_construction_and_accessors() {
|
||||
let region = MemoryRegion::new(
|
||||
RegionId::from_raw(1),
|
||||
process(0),
|
||||
base(),
|
||||
pages(4),
|
||||
Generation::initial(),
|
||||
);
|
||||
assert_eq!(region.id(), RegionId::from_raw(1));
|
||||
assert_eq!(region.owner(), process(0));
|
||||
assert_eq!(region.base(), base());
|
||||
assert_eq!(region.size(), pages(4));
|
||||
assert_eq!(region.generation(), &Generation::initial());
|
||||
}
|
||||
|
||||
// --- RegionTable tests ---
|
||||
|
||||
#[test]
|
||||
fn region_table_insert_and_get() {
|
||||
let mut table = RegionTable::new();
|
||||
let id = table.insert(MemoryRegion::new(
|
||||
RegionId::from_raw(0),
|
||||
process(0),
|
||||
base(),
|
||||
pages(4),
|
||||
Generation::initial(),
|
||||
));
|
||||
assert!(id.is_some());
|
||||
let rid = id.unwrap();
|
||||
assert!(table.get(rid).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn region_table_remove() {
|
||||
let mut table = RegionTable::new();
|
||||
let id = table
|
||||
.insert(MemoryRegion::new(
|
||||
RegionId::from_raw(0),
|
||||
process(0),
|
||||
base(),
|
||||
pages(4),
|
||||
Generation::initial(),
|
||||
))
|
||||
.unwrap();
|
||||
let removed = table.remove(id);
|
||||
assert!(removed.is_some());
|
||||
assert!(table.get(id).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn region_table_regions_for_process() {
|
||||
let mut table = RegionTable::new();
|
||||
table.insert(MemoryRegion::new(
|
||||
RegionId::from_raw(0),
|
||||
process(0),
|
||||
base(),
|
||||
pages(4),
|
||||
Generation::initial(),
|
||||
));
|
||||
table.insert(MemoryRegion::new(
|
||||
RegionId::from_raw(1),
|
||||
process(1),
|
||||
base(),
|
||||
pages(4),
|
||||
Generation::initial(),
|
||||
));
|
||||
table.insert(MemoryRegion::new(
|
||||
RegionId::from_raw(2),
|
||||
process(0),
|
||||
base(),
|
||||
pages(2),
|
||||
Generation::initial(),
|
||||
));
|
||||
assert_eq!(table.regions_for_process(process(0)).count(), 2);
|
||||
assert_eq!(table.regions_for_process(process(1)).count(), 1);
|
||||
fn region_table_finds_mapping_covering_a_va() {
|
||||
let mut t = RegionTable::new();
|
||||
let id = t.insert(pool_region(1, 2)).unwrap();
|
||||
t.get_mut(id).unwrap().add_mapping(mapping(3, 0x30000));
|
||||
assert_eq!(t.covering(pid(3), va(0x31000)), Some(id));
|
||||
assert_eq!(t.covering(pid(3), va(0x32000)), None);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user