diff --git a/build/build.rs b/build/build.rs index f8a8355efbc..de159d2ea9f 100644 --- a/build/build.rs +++ b/build/build.rs @@ -3461,6 +3461,7 @@ mod tests { "tests/images/virtpaniccase.toml", "tests/images/virtrebootcase.toml", "tests/images/virtsmpcase.toml", + "tests/images/virttmpcase.toml", ]; fn load(cfg: &str) -> SystemConfig { diff --git a/issues/a-pager-holds-a-fixed-capacity-until-the-memory-service-reclaims-its-clean-pages.md b/issues/a-pager-holds-a-fixed-capacity-until-the-memory-service-reclaims-its-clean-pages.md index 49ac4d0fa10..2f609c05d4b 100644 --- a/issues/a-pager-holds-a-fixed-capacity-until-the-memory-service-reclaims-its-clean-pages.md +++ b/issues/a-pager-holds-a-fixed-capacity-until-the-memory-service-reclaims-its-clean-pages.md @@ -20,11 +20,12 @@ which 32 MiB may be dirty. The eighth is the bound netstack and the compositor take for want of a kernel memory limit (`issues/no-physical-memory-fairness.md`); it is an interim, not a sizing. -A pager also serves at most the objects it was made for: DATA one per 16 -pages of its capacity, between 1,024 and 65,536, LOG and BOOT 1,024 each. So -the DATA server holds at most that many files' objects machine-wide; past it, -a file nobody holds goes to make room, and an open is refused `NoSpace` only -while every one is held open. +A pager's objects are charged as they are made. A server on a disk keeps at +most a count of files (`Files::opened` in `system/fileserver/src/pages.rs`): +DATA one per 16 pages of its capacity, between 1,024 and 65,536, LOG and +BOOT 1,024 each. Past it, a file nobody holds goes to make room, and an open +is refused `NoSpace` only while every one is held open. A memory volume keeps +no count (`issues/one-client-can-fill-the-tmp-servers-memory-and-deny-every-other-client.md`). No other job can take a pager's clean pages back. A pager withdraws them only for its own sake (`system/fileserver/src/pages.rs`, `Files::make_room`), diff --git a/issues/no-physical-memory-fairness.md b/issues/no-physical-memory-fairness.md index 9b815328c03..97a75100ad7 100644 --- a/issues/no-physical-memory-fairness.md +++ b/issues/no-physical-memory-fairness.md @@ -55,6 +55,9 @@ supervisor's own memory, which closes it (below). instead. Owner: the supervisor track. Exit: a start refused for the supervisor's own memory waits on the supervisor's own job's readiness, as one refused for the service's waits on the service's. +- **A client's `/tmp` files are counted to the file server that holds them**, + so the job a person would stop for them is a service every client needs + (`issues/one-client-can-fill-the-tmp-servers-memory-and-deny-every-other-client.md`). - **Connect-time capacity.** Both rings of a connection are its connecting job's, allocated at each one's first write, so a server's reply spends nothing of its own; but a client's sends are not yet capacity its receiver diff --git a/issues/one-client-can-fill-the-tmp-servers-memory-and-deny-every-other-client.md b/issues/one-client-can-fill-the-tmp-servers-memory-and-deny-every-other-client.md new file mode 100644 index 00000000000..df6cda5acdf --- /dev/null +++ b/issues/one-client-can-fill-the-tmp-servers-memory-and-deny-every-other-client.md @@ -0,0 +1,52 @@ +--- +status: open +kind: defect +opened: 2026-11-12 +severity: [security] +--- + +# One client can fill the `/tmp` server's memory and deny every other client + +A memory volume's files are bounded only by what its server's job can be +charged (`pager` in `system/fileserver/src/main.rs`). Everything a `/tmp` file +costs is the MEMORY file server's: + +- its object's slot in the kernel (`kernel/src/object/paged.rs`); +- the server's own record of it; +- its pages, up to the pager's capacity. + +A directory or a link there costs the server's record alone. None of it is +charged to the client that made it, and nothing bounds one client's part. So +one client can make files, directories and links in `/tmp` until the memory is +spent. Then every other client's create there is refused. The server's next +allocation of its own may be among the refused, which ends it, and with it +every client's `/tmp`. The memory is counted to the file server, so nothing +names the client that took it (`issues/no-physical-memory-fairness.md`). + +**What one client leaves charged.** The kernel makes a pager's object slots +256 at a time and keeps every piece for the pager's life (`kernel/pure/slots.rs`). +A freed slot is taken again, but its piece never goes back. A slot is 72 +bytes: a 56-byte entry and a 12-byte mark, measured with a host replica of +the layouts: +https://github.com/ToyOSOrg/evidence/tree/f81e4b2ddb7d89a92f82f9eb8da1fdf58587a563/pr-1273/sizes +So the server's job stays charged 72 bytes for each `/tmp` file of the most +it held at once, until the server ends. A client that makes and removes a +million files leaves about 72 MB. The manual has unused backing return under +pressure; this never returns. + +Owner: the file server and the kernel's paged objects, once the owner has +decided the charge below. + +**Exit**: + +- **The charge, proposed and the owner's to decide.** What a server keeps for + one client is charged to that client's job: a pager's object, and the pages + supplied to it, are charged to a job the client's grant names, which the + kernel lets the server charge by that authority. This changes the manual's + rule that services own their private state. The alternatives are a + per-share allowance in the server, which is a fixed part again, and leaving + the charge with the server. +- **The slots.** A piece whose every slot is free goes back to the heap, and + its charge with it. The free list takes the lowest free slot first, so the + highest pieces drain. A host test of `Slots` shows a table that held many + slots and let them go holding one piece's charge. diff --git a/kernel/pure/lib.rs b/kernel/pure/lib.rs index f1cc11a762f..e976880561a 100644 --- a/kernel/pure/lib.rs +++ b/kernel/pure/lib.rs @@ -29,6 +29,7 @@ pub mod pipe; pub mod pointers; pub mod rmrr; pub mod sched; +pub mod slots; pub mod stackslots; pub mod vm; pub mod wx; diff --git a/kernel/pure/slots.rs b/kernel/pure/slots.rs new file mode 100644 index 00000000000..2e364008381 --- /dev/null +++ b/kernel/pure/slots.rs @@ -0,0 +1,308 @@ +//! A table that names what it holds by slot. Its slots are made a piece at a +//! time as they are wanted and never move, so a slot names one value for as +//! long as it is held, and a slot let go is the next one taken. What the next +//! piece costs is known before it is made, so a caller charges it first; +//! letting a slot go never allocates, and a piece is kept with the table. +//! +//! A held slot may be marked. The marked slots are a list through the slots +//! themselves, so marking, unmarking and taking the last marked cost the same +//! however many slots there are, and none allocates. + +#![forbid(unsafe_code)] + +use alloc::boxed::Box; +use alloc::vec::Vec; + +use crate::chunked::CHUNK_BYTES; + +/// Slots one piece holds. +pub const PIECE: usize = 256; + +const NONE: u32 = u32::MAX; + +enum Slot { + /// Free, linked to the next free slot. + Free(u32), + Held(T, Mark), +} + +/// Whether a held slot is marked, and its neighbours on the marked list. +#[derive(Clone, Copy)] +enum Mark { + Off, + On { prev: u32, next: u32 }, +} + +pub struct Slots { + pieces: Vec]>>, + /// The first free slot, `NONE` while every slot made is held. + free: u32, + /// The slot marked last, `NONE` while none is. + marked: u32, +} + +impl Default for Slots { + fn default() -> Self { + Self::new() + } +} + +impl Slots { + /// The most slots a table makes: as many pieces as one list of + /// [`CHUNK_BYTES`] names. + pub const MAX: usize = CHUNK_BYTES / core::mem::size_of::]>>() * PIECE; + + /// The heap one piece takes. + pub const PIECE_BYTES: usize = PIECE * core::mem::size_of::>(); + + pub const fn new() -> Self { + Self { pieces: Vec::new(), free: NONE, marked: NONE } + } + + /// Slots made, each held or free. + pub fn len(&self) -> usize { + self.pieces.len() * PIECE + } + + pub fn is_empty(&self) -> bool { + self.pieces.is_empty() + } + + /// The heap the table holds. + pub fn heap_bytes(&self) -> usize { + Self::bytes(self.pieces.len(), self.pieces.capacity()) + } + + fn bytes(pieces: usize, listed: usize) -> usize { + pieces * Self::PIECE_BYTES + listed * core::mem::size_of::]>>() + } + + /// The pieces the list names once it holds `pieces`: doubling, so a + /// growth seldom moves it, and never past [`Self::MAX`]. + fn listed_for(&self, pieces: usize) -> usize { + match pieces <= self.pieces.capacity() { + true => self.pieces.capacity(), + false => pieces.next_power_of_two().min(Self::MAX / PIECE), + } + } + + /// Whether a slot is free for the next [`insert_with`](Self::insert_with). + pub fn has_free(&self) -> bool { + self.free != NONE + } + + /// [`heap_bytes`](Self::heap_bytes) once [`grow`](Self::grow) has made + /// one more piece: `None` where the table holds [`Self::MAX`] slots. + pub fn grown_bytes(&self) -> Option { + let pieces = self.pieces.len() + 1; + (pieces * PIECE <= Self::MAX).then(|| Self::bytes(pieces, self.listed_for(pieces))) + } + + /// One more piece of free slots: `false`, with nothing changed, where the + /// table holds [`Self::MAX`] or the heap refuses. + pub fn grow(&mut self) -> bool { + let pieces = self.pieces.len() + 1; + if pieces * PIECE > Self::MAX { + return false; + } + // The piece first: a refused list then drops it, and the list keeps + // the capacity it had. + let mut piece = Vec::new(); + if piece.try_reserve_exact(PIECE).is_err() { + return false; + } + let listed = self.listed_for(pieces); + if self.pieces.try_reserve_exact(listed - self.pieces.len()).is_err() { + return false; + } + let first = self.len() as u32; + let last = first + PIECE as u32 - 1; + let free = self.free; + piece.extend((first..=last).map(|slot| Slot::Free(if slot == last { free } else { slot + 1 }))); + self.pieces.push(piece.into_boxed_slice()); + self.free = first; + true + } + + /// The value `f` makes of the first free slot, held there: its slot. + /// `None` where no slot is free. + pub fn insert_with(&mut self, f: impl FnOnce(u32) -> T) -> Option { + let slot = self.free; + let at = self.at_mut(slot)?; + let Slot::Free(next) = *at else { panic!("slots: the free list named held slot {slot}") }; + *at = Slot::Held(f(slot), Mark::Off); + self.free = next; + Some(slot) + } + + /// The value at `slot`, unmarked and its slot free from here; `None` + /// where it holds none. + pub fn remove(&mut self, slot: u32) -> Option { + self.unmark(slot); + let free = self.free; + let at = self.at_mut(slot)?; + if let Slot::Free(_) = at { + return None; + } + let Slot::Held(value, _) = core::mem::replace(at, Slot::Free(free)) else { unreachable!("held above") }; + self.free = slot; + Some(value) + } + + pub fn get(&self, slot: u32) -> Option<&T> { + match self.pieces.get(slot as usize / PIECE)?.get(slot as usize % PIECE)? { + Slot::Held(value, _) => Some(value), + Slot::Free(_) => None, + } + } + + pub fn get_mut(&mut self, slot: u32) -> Option<&mut T> { + match self.at_mut(slot)? { + Slot::Held(value, _) => Some(value), + Slot::Free(_) => None, + } + } + + /// Mark the held `slot`: whether it was not marked already. + pub fn mark(&mut self, slot: u32) -> bool { + let last = self.marked; + let Some(mark) = self.mark_of(slot) else { return false }; + if let Mark::On { .. } = mark { + return false; + } + *mark = Mark::On { prev: NONE, next: last }; + if last != NONE { + *self.link(last).0 = slot; + } + self.marked = slot; + true + } + + /// Unmark `slot`: whether it was marked. + pub fn unmark(&mut self, slot: u32) -> bool { + let Some(mark) = self.mark_of(slot) else { return false }; + let Mark::On { prev, next } = *mark else { return false }; + *mark = Mark::Off; + match prev { + NONE => self.marked = next, + _ => *self.link(prev).1 = next, + } + if next != NONE { + *self.link(next).0 = prev; + } + true + } + + /// The slot marked last, unmarked. + pub fn take_marked(&mut self) -> Option { + let slot = self.marked; + self.unmark(slot).then_some(slot) + } + + pub fn any_marked(&self) -> bool { + self.marked != NONE + } + + fn mark_of(&mut self, slot: u32) -> Option<&mut Mark> { + match self.at_mut(slot)? { + Slot::Held(_, mark) => Some(mark), + Slot::Free(_) => None, + } + } + + /// The marked `slot`'s neighbours on the list. + fn link(&mut self, slot: u32) -> (&mut u32, &mut u32) { + match self.mark_of(slot) { + Some(Mark::On { prev, next }) => (prev, next), + _ => panic!("slots: the marked list named slot {slot}, which is not marked"), + } + } + + fn at_mut(&mut self, slot: u32) -> Option<&mut Slot> { + self.pieces.get_mut(slot as usize / PIECE)?.get_mut(slot as usize % PIECE) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Slots are made a piece at a time from 0, a value stays at its slot + /// while the table grows, and a slot let go is the next one taken. + #[test] + fn a_slot_names_its_value_until_it_goes_and_is_taken_next() { + let mut t: Slots = Slots::new(); + assert_eq!(t.insert_with(u64::from), None, "no piece made"); + let mut held = alloc::vec::Vec::new(); + for n in 0..3 * PIECE as u32 + 1 { + if !t.has_free() { + assert!(t.grow()); + } + held.push(t.insert_with(|slot| u64::from(slot) * 7).unwrap()); + assert_eq!(*held.last().unwrap(), n); + } + assert_eq!(t.len(), 4 * PIECE); + assert!(held.iter().all(|&s| t.get(s) == Some(&(u64::from(s) * 7)))); + assert_eq!(t.remove(5), Some(35)); + assert_eq!(t.remove(700), Some(4900)); + assert_eq!(t.get(5), None, "a free slot holds nothing"); + assert_eq!(t.remove(5), None, "nor gives anything back"); + assert_eq!(t.insert_with(|_| 1), Some(700)); + assert_eq!(t.insert_with(|_| 2), Some(5)); + assert_eq!(t.insert_with(|_| 3), Some(3 * PIECE as u32 + 1), "then the piece's own free slots"); + assert_eq!(t.get_mut(5).map(|v| core::mem::replace(v, 9)), Some(2)); + assert_eq!(t.get(5), Some(&9)); + } + + /// What a growth is said to cost before it is made is what the table then + /// holds, so a charge taken first covers it. + #[test] + fn a_growth_costs_what_it_said_it_would() { + let mut t: Slots = Slots::new(); + assert_eq!(t.heap_bytes(), 0); + for _ in 0..9 { + let said = t.grown_bytes().unwrap(); + assert!(t.grow()); + assert_eq!(t.heap_bytes(), said); + } + } + + /// The marked slots are taken last marked first, each once: a second mark + /// is none, an unmark or a removal takes a slot off the list wherever it + /// is on it, a free slot is never marked, and marks hold across a growth. + #[test] + fn a_marked_slot_is_taken_once_unless_unmarked_or_removed_first() { + let mut t: Slots = Slots::new(); + assert!(t.grow()); + for n in 0..6 { + t.insert_with(|slot| slot).unwrap(); + assert!(t.mark(n)); + } + assert!(!t.mark(3), "marked twice"); + assert!(!t.mark(PIECE as u32 - 1) && !t.mark(NONE), "a free slot and none"); + assert!(t.unmark(0) && t.unmark(3) && t.unmark(5), "the first, a middle one and the last"); + assert!(!t.unmark(3), "unmarked twice"); + assert_eq!(t.remove(2), Some(2)); + while t.has_free() { + t.insert_with(|slot| slot).unwrap(); + } + assert!(t.grow()); + assert_eq!(t.insert_with(|slot| slot), Some(PIECE as u32)); + assert!(t.mark(0) && t.mark(PIECE as u32)); + let taken: alloc::vec::Vec = core::iter::from_fn(|| t.take_marked()).collect(); + assert_eq!(taken, [PIECE as u32, 0, 4, 1]); + assert!(!t.any_marked()); + assert!(t.mark(4), "a taken slot is marked again"); + assert!(t.any_marked()); + } + + /// Past the slots made, nothing is named, and a removal changes nothing. + #[test] + fn a_slot_past_the_table_names_nothing() { + let mut t: Slots = Slots::new(); + assert!(t.grow()); + assert_eq!((t.get(PIECE as u32), t.get(NONE)), (None, None)); + assert_eq!(t.remove(NONE), None); + assert!(t.has_free()); + } +} diff --git a/kernel/src/object/paged.rs b/kernel/src/object/paged.rs index b5a976fbb6b..0d7f5c5ca19 100644 --- a/kernel/src/object/paged.rs +++ b/kernel/src/object/paged.rs @@ -36,8 +36,11 @@ //! its core — `dirty` pages and a read's headroom — committed at its making //! and kept for its life, every frame of it either promised, in the index or //! held aside for the next supply; the rest of `capacity` promised a page at -//! a time and given back as each goes. Its index, objects and the frames it -//! holds aside are charged with it, so no call on it allocates. +//! a time and given back as each goes. Its index and the frames it holds +//! aside are charged with it. Its objects' slots are made a piece at a time, +//! each piece charged as an object first needs it and kept for the pager's +//! life (`issues/one-client-can-fill-the-tmp-servers-memory-and-deny-every-other-client.md`), +//! so making an object is the one call that allocates. use alloc::sync::Arc; use core::cell::{Cell, UnsafeCell}; @@ -46,11 +49,13 @@ use core::pin::Pin; use core::ptr; use core::sync::atomic::{AtomicU32, Ordering}; -use kernel::chunked::{Chunked, Stack}; +use kernel::chunked::Stack; use kernel::paged::{Core, Draw, Leave, Lookup, Object, Pages, Refused, Supplied, Version, Waiters, Want, PAGE}; use kernel::sched::task::WaitClass; +use kernel::slots::Slots; use toyos_abi::syscall::{PagedNotice, SyscallError, NOTICE_NO_HOLDERS, NOTICE_REQUESTED}; +use crate::mm::charge::Charge; use crate::mm::pmm; use crate::process::Pid; use crate::sync::Lock; @@ -64,6 +69,9 @@ use super::{KObjectVariant, ObjectCore, ZeroHandles}; // Two pieces of a table and their allocator headers share one superframe. const _: () = assert!(2 * kernel::chunked::CHUNK_BYTES < crate::mm::MAX_HEAP_ALLOC); +// A piece of slots is one allocation the heap takes. +const _: () = assert!(Slots::::PIECE_BYTES <= kernel::chunked::CHUNK_BYTES); + /// Pages one copy pins, and one supply or write draws, at once. const CHUNK: usize = 16; @@ -96,18 +104,17 @@ struct Entry { holders: u32, /// Not released: owner calls reach it. owned: bool, - /// The last holder went and no notice has said so. - no_holders: bool, } struct State { pages: Pages, - /// Every object's slot, made with the pager. - entries: Chunked>, - free: Stack, + /// Every object, at its id's slot, marked while its last holder has gone + /// and no notice has said so. + entries: Slots, + /// The heap the slots take, charged to the pager's job as each piece is + /// made. + slots_charge: Charge, requests: *mut Request, - /// Entries whose `no_holders` is set. - no_holders: usize, held: Held, gone: bool, } @@ -160,18 +167,18 @@ unsafe impl Send for State {} impl State { fn entry(&self, slot: u32) -> &Entry { - self.entries[slot as usize].as_ref().expect("paged: a slot an object names is held") + self.entries.get(slot).expect("paged: a slot an object names is held") } fn entry_mut(&mut self, slot: u32) -> &mut Entry { - self.entries[slot as usize].as_mut().expect("paged: a slot an object names is held") + self.entries.get_mut(slot).expect("paged: a slot an object names is held") } /// The slot of the unreleased object `id` names: the caller's word, /// refused `InvalidArgument` where it names none. fn live(&self, id: u64) -> Result { let slot = u32::try_from(id).map_err(|_| SyscallError::InvalidArgument)?; - let entry = (slot as usize) < self.entries.len() && self.entries[slot as usize].as_ref().is_some_and(|e| e.owned); + let entry = self.entries.get(slot).is_some_and(|e| e.owned); match entry { true if !self.gone => Ok(slot), _ if self.gone => Err(SyscallError::Gone), @@ -181,10 +188,25 @@ impl State { /// The index and the object at `slot`, borrowed apart. fn split(&mut self, slot: u32) -> (&mut Pages, &mut Object) { - let entry = self.entries[slot as usize].as_mut().expect("paged: a slot an object names is held"); + let entry = self.entries.get_mut(slot).expect("paged: a slot an object names is held"); (&mut self.pages, &mut entry.object) } + /// A free slot for the next object, its piece charged where one is made: + /// `ResourceExhausted` where the charge is refused or the table is whole. + fn make_slot(&mut self) -> Result<(), SyscallError> { + if self.entries.has_free() { + return Ok(()); + } + let grown = self.entries.grown_bytes().ok_or(SyscallError::ResourceExhausted)?; + let more = grown - self.entries.heap_bytes(); + self.slots_charge.grow(more)?; + if !self.entries.grow() { + self.slots_charge.shrink(more); + return Err(SyscallError::ResourceExhausted); + } + Ok(()) + } } pub struct PagerObject { @@ -211,26 +233,19 @@ impl PagerObject { toyos_abi::handle::Rights::READ.union(toyos_abi::handle::Rights::WRITE).union(toyos_abi::handle::Rights::WAIT); /// The calling process's pager: `capacity` pages, `dirty` of them dirty at - /// once, at most `objects` objects, readers overdue past `report_ns`. - pub fn create(capacity: u64, dirty: u64, objects: u64, report_ns: u64) -> Result, Unmade> { - let fits = |n: u64| usize::try_from(n).ok().filter(|&n| n > 0 && n <= Pages::::MAX_CAPACITY); - let (Some(slots), Some(count)) = (fits(capacity), fits(objects)) else { return Err(Unmade::Invalid) }; + /// once, readers overdue past `report_ns`. + pub fn create(capacity: u64, dirty: u64, report_ns: u64) -> Result, Unmade> { + let Some(slots) = usize::try_from(capacity).ok().filter(|&n| n > 0 && n <= Pages::::MAX_CAPACITY) else { + return Err(Unmade::Invalid); + }; if dirty > capacity { return Err(Unmade::Invalid); } let core_pages = dirty + (capacity - dirty).min(HEADROOM); let held = core_pages as usize; - let tables = Pages::::heap_bytes(slots) - + Chunked::>::heap_bytes(count) - + Stack::::heap_bytes(count) - + 2 * Stack::::heap_bytes(held); + let tables = Pages::::heap_bytes(slots) + 2 * Stack::::heap_bytes(held); let core = Self::new_core(tables).map_err(|_| Unmade::Exhausted)?; let pages = Pages::new(slots, dirty as usize).ok_or(Unmade::Exhausted)?; - let entries = Chunked::new(count, || None).ok_or(Unmade::Exhausted)?; - let mut free = Stack::new(count).ok_or(Unmade::Exhausted)?; - for slot in (0..count as u32).rev() { - free.push(slot); - } let spare = Stack::new(held).ok_or(Unmade::Exhausted)?; let draining = Stack::new(held).ok_or(Unmade::Exhausted)?; if !pmm::promise(core.job(), core_pages) { @@ -238,10 +253,9 @@ impl PagerObject { } let state = State { pages, - entries, - free, + entries: Slots::new(), + slots_charge: Charge::none(core.job()), requests: ptr::null_mut(), - no_holders: 0, held: Held { core: Core::new(core_pages), spare, draining }, gone: false, }; @@ -272,11 +286,12 @@ impl PagerObject { let state = self.state.lock(); // SAFETY: every node on the list is live while linked, under this lock. let unreported = requests(&state).any(|r| unsafe { (*r).want.unreported() }); - unreported || state.no_holders > 0 + unreported || state.entries.any_marked() } - /// A new object of `size` bytes: its id. - /// `ResourceExhausted` where every slot holds one. + /// A new object of `size` bytes: its id. `ResourceExhausted` where its + /// job cannot be charged the slots for another, or the table holds as + /// many as it can. pub fn add(&self, size: u64, anonymous: bool) -> u64 { if size > kernel::paged::LAST_END { return SyscallError::InvalidArgument.to_u64(); @@ -285,10 +300,15 @@ impl PagerObject { if state.gone { return SyscallError::Gone.to_u64(); } - let Some(slot) = state.free.pop() else { return SyscallError::ResourceExhausted.to_u64() }; - let object = if anonymous { state.pages.anonymous(slot, size) } else { state.pages.object(slot, size) }; - state.entries[slot as usize] = Some(Entry { object, refs: 1, holders: 0, owned: true, no_holders: false }); - u64::from(slot) + if let Err(e) = state.make_slot() { + return e.to_u64(); + } + let State { pages, entries, .. } = &mut *state; + let made = entries.insert_with(|slot| { + let object = if anonymous { pages.anonymous(slot, size) } else { pages.object(slot, size) }; + Entry { object, refs: 1, holders: 0, owned: true } + }); + u64::from(made.expect("paged: a slot was made free")) } /// `f` over the index, the live object `id` and its waiters, each frame @@ -302,7 +322,7 @@ impl PagerObject { let slot = state.live(id)?; let job = self.core.job(); let State { pages, entries, held, gone, requests, .. } = &mut *state; - let object = &mut entries[slot as usize].as_mut().expect("live").object; + let object = &mut entries.get_mut(slot).expect("live").object; let mut waiters = Linked { head: *requests, slot }; let answer = f(pages, object, &mut waiters, &mut |phys| held.give_back(job, *gone, phys, true)); drop(state); @@ -328,13 +348,11 @@ impl PagerObject { /// Under the lock, `state`: the unreleased object at `slot` released. fn let_go(&self, state: &mut State, slot: u32) { let job = self.core.job(); - let State { pages, entries, held, gone, no_holders, .. } = state; - let entry = entries[slot as usize].as_mut().expect("paged: a slot an object names is held"); + let State { pages, entries, held, gone, .. } = state; + let entry = entries.get_mut(slot).expect("paged: a slot an object names is held"); pages.invalidate(&mut entry.object, &mut |phys| held.give_back(job, *gone, phys, true)); assert!(core::mem::take(&mut entry.owned), "paged: an object released twice"); - if core::mem::take(&mut entry.no_holders) { - *no_holders -= 1; - } + entries.unmark(slot); self.unref_locked(state, slot); } @@ -355,9 +373,7 @@ impl PagerObject { let gone = state.gone; let entry = state.entry_mut(slot); entry.holders -= 1; - if entry.holders == 0 && entry.owned && !gone && !entry.no_holders { - entry.no_holders = true; - state.no_holders += 1; + if entry.holders == 0 && entry.owned && !gone && state.entries.mark(slot) { self.watch.post(); } } @@ -374,12 +390,10 @@ impl PagerObject { if entry.refs > 0 { return; } - let entry = state.entries[slot as usize].take().expect("paged: a slot an object names is held"); - state.no_holders -= usize::from(entry.no_holders); + let entry = state.entries.remove(slot).expect("paged: a slot an object names is held"); let job = self.core.job(); let State { pages, held, gone, .. } = state; pages.release(entry.object, &mut |phys| held.give_back(job, *gone, phys, true)); - state.free.push(slot); } /// Fill `out` with notices, each answered once. @@ -407,23 +421,14 @@ impl PagerObject { } at = next; } - let mut owed = state.no_holders; - for slot in 0..state.entries.len() { - if n == want || owed == 0 { - break; - } - let Some(entry) = state.entries[slot].as_mut() else { continue }; - if entry.no_holders { - entry.no_holders = false; - owed -= 1; - // Said only while still so: a holder made since is held. - if entry.holders == 0 { - batch[n] = PagedNotice { object: slot as u64, kind: NOTICE_NO_HOLDERS, first: 0, count: 0 }; - n += 1; - } + while n < want { + let Some(slot) = state.entries.take_marked() else { break }; + // Said only while still so: a holder made since is held. + if state.entry(slot).holders == 0 { + batch[n] = PagedNotice { object: u64::from(slot), kind: NOTICE_NO_HOLDERS, first: 0, count: 0 }; + n += 1; } } - state.no_holders = owed; n }; // SAFETY: a `PagedNotice` is `UserSafe`: every byte of it is a @@ -1125,13 +1130,10 @@ impl ZeroHandles for PagerObject { let mut state = self.state.lock(); state.gone = true; for slot in 0..state.entries.len() as u32 { - let Some(entry) = state.entries[slot as usize].as_mut() else { continue }; - entry.no_holders = false; - if entry.owned { + if state.entries.get(slot).is_some_and(|e| e.owned) { self.let_go(&mut state, slot); } } - state.no_holders = 0; let job = self.core.job(); while let Some(phys) = state.held.spare.pop().or_else(|| state.held.draining.pop()) { // SAFETY: a frame this pager drew and held aside, which nothing diff --git a/kernel/src/syscall/dispatch.rs b/kernel/src/syscall/dispatch.rs index 8688bb06250..e65f07ac541 100644 --- a/kernel/src/syscall/dispatch.rs +++ b/kernel/src/syscall/dispatch.rs @@ -549,7 +549,7 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> // kernel decides each access by what its address is. SYS_ACPI => sys_acpi(&ctx, RawHandle(a1 as u32), a2, a3), SYS_DEVICE_REG_READ => sys_device_reg_read(RawHandle(a1 as u32), a2, a3), - SYS_PAGER_CREATE => sys_pager_create(a1, a2, a3, a4), + SYS_PAGER_CREATE => sys_pager_create(a1, a2, a3), SYS_PAGED_CREATE => match a3 { 0 | 1 => sys_paged_create(RawHandle(a1 as u32), a2, a3 != 0), _ => SyscallError::InvalidArgument.to_u64(), diff --git a/kernel/src/syscall/paged.rs b/kernel/src/syscall/paged.rs index 2c459417e0a..953861cdf0a 100644 --- a/kernel/src/syscall/paged.rs +++ b/kernel/src/syscall/paged.rs @@ -16,12 +16,12 @@ use crate::process; use crate::user_ptr::{SyscallContext, UserBytesMut}; use crate::UserAddr; -pub(super) fn sys_pager_create(capacity: u64, dirty: u64, objects: u64, report_ns: u64) -> u64 { +pub(super) fn sys_pager_create(capacity: u64, dirty: u64, report_ns: u64) -> u64 { let handles = process::handles(); if let Err(e) = handles.claim_pager() { return e.to_u64(); } - let made = PagerObject::create(capacity, dirty, objects, report_ns).map_err(|why| match why { + let made = PagerObject::create(capacity, dirty, report_ns).map_err(|why| match why { Unmade::Invalid => SyscallError::InvalidArgument, Unmade::Exhausted => SyscallError::ResourceExhausted, }); diff --git a/sdk/abi/src/syscall.rs b/sdk/abi/src/syscall.rs index a572ec6896f..d4e5763dca3 100644 --- a/sdk/abi/src/syscall.rs +++ b/sdk/abi/src/syscall.rs @@ -3174,21 +3174,21 @@ pub const NOTICE_REQUESTED: u64 = 1; pub const NOTICE_NO_HOLDERS: u64 = 2; /// Make this process's one pager, whose objects' pages are at most -/// `capacity`, `dirty` of them dirty at once, and which serves at most -/// `objects` objects. A reader waiting past `report_ns` (bounded by the -/// kernel) is reported overdue and waits on. Committed at once, and charged -/// with the pager: `dirty` pages and the headroom a read's pages need, and -/// the index and object table, which a capacity the job cannot hold is -/// refused for (`ResourceExhausted`). `PermissionDenied` for a process not -/// started as a pager, `AlreadyExists` for a second. -pub fn pager_create(capacity: u64, dirty: u64, objects: u64, report_ns: u64) -> Result { - check(syscall(SYS_PAGER_CREATE, capacity, dirty, objects, report_ns)).map(|raw| RawHandle(raw as u32)) +/// `capacity`, `dirty` of them dirty at once. A reader waiting past +/// `report_ns` (bounded by the kernel) is reported overdue and waits on. +/// Committed at once, and charged with the pager: `dirty` pages and the +/// headroom a read's pages need, and the index, which a capacity the job +/// cannot hold is refused for (`ResourceExhausted`). Its objects are charged +/// to the pager's job as they are made ([`paged_create`]). `PermissionDenied` +/// for a process not started as a pager, `AlreadyExists` for a second. +pub fn pager_create(capacity: u64, dirty: u64, report_ns: u64) -> Result { + check(syscall(SYS_PAGER_CREATE, capacity, dirty, report_ns, 0)).map(|raw| RawHandle(raw as u32)) } /// A paged object of `size` bytes, its pages absent: /// its id, which `pager`'s calls and notices name it by. -/// `ResourceExhausted` where the pager serves as many objects as it was made -/// for. +/// `ResourceExhausted` where the pager's job cannot be charged the object, or +/// the pager serves as many as the kernel's table holds. pub fn paged_create(pager: RawHandle, size: u64) -> Result { check(syscall(SYS_PAGED_CREATE, pager.0 as u64, size, 0, 0)) } diff --git a/sdk/toyos/src/paged.rs b/sdk/toyos/src/paged.rs index f09b6e76b42..98aa2d10ff7 100644 --- a/sdk/toyos/src/paged.rs +++ b/sdk/toyos/src/paged.rs @@ -25,7 +25,8 @@ pub const PAGE: u64 = syscall::PAGE_SIZE as u64; pub enum Refused { /// The first page is absent: supply it. Absent, - /// Every page, or every object, the pager may hold is held: let one go first. + /// Every page the pager may hold is held, or its job cannot be charged + /// another object: let one go first. NoRoom, /// The pager's dirty ceiling is reached: write back first. DirtyFull, @@ -61,12 +62,12 @@ pub enum Notice { pub struct Pager(OwnedHandle); impl Pager { - /// `capacity` pages for every object, `dirty` of them dirty at once, at - /// most `objects` objects; a reader waiting past `report` is reported - /// overdue, and waits on. - pub fn create(capacity: u64, dirty: u64, objects: u64, report: Duration) -> Result { + /// `capacity` pages for every object, `dirty` of them dirty at once, its + /// objects each charged as it is made; a reader waiting past `report` is + /// reported overdue, and waits on. + pub fn create(capacity: u64, dirty: u64, report: Duration) -> Result { let report = u64::try_from(report.as_nanos()).unwrap_or(u64::MAX); - let raw = syscall::pager_create(capacity, dirty, objects, report)?; + let raw = syscall::pager_create(capacity, dirty, report)?; // SAFETY: the kernel just installed it in this process's table. Ok(Self(unsafe { OwnedHandle::from_raw(raw) })) } diff --git a/system/fileserver/src/main.rs b/system/fileserver/src/main.rs index b063d926584..90a14e5cfda 100644 --- a/system/fileserver/src/main.rs +++ b/system/fileserver/src/main.rs @@ -238,7 +238,8 @@ fn main() { dirs.iter().map(|d| d.dir).collect::>().join(", "), volume.describe() ); - let files = Files::new(pager(role, volume.memory())); + let (pager, keeps) = pager(role, volume.memory()); + let files = Files::new(pager, keeps); Server { volume, files, @@ -271,25 +272,26 @@ const SMALL_PAGES: u64 = (16 << 20) / toyos::paged::PAGE; /// The most pages a DATA pager holds dirty: 32 MiB. const DATA_DIRTY: u64 = 8 * 1024; -/// The role's pager: how many pages its files hold, how many of them may be -/// dirty and how many files it keeps. None for SYSTEM, whose files are +/// The role's pager: how many pages its files hold and how many of them may +/// be dirty, and how many files it keeps. None for SYSTEM, whose files are /// immutable and are served from ROOT's memory. -fn pager(role: Role, anonymous: bool) -> Option { - let (capacity, dirty, objects) = match role { - Role::System => return None, +fn pager(role: Role, anonymous: bool) -> (Option, Option) { + let (capacity, dirty, keeps) = match role { + Role::System => return (None, None), Role::Data | Role::Memory => { let share = toyos::system::memory_total() / PAGES_SHARE / toyos::paged::PAGE; let capacity = (share.saturating_sub(2 * SMALL_PAGES) / 2).max(SMALL_PAGES); - // A file held for every 16 pages, the table's own cost a small - // part of the pages': at most 65,536. + // A file kept for every 16 pages, at most 65,536. (capacity, DATA_DIRTY.min(capacity / 2), (capacity / 16).clamp(1024, 64 * 1024)) } Role::Log => (SMALL_PAGES, 512, 1024), Role::Boot => (SMALL_PAGES, 0, 1024), }; - let pager = toyos::paged::Pager::create(capacity, if anonymous { 0 } else { dirty }, objects, REPORT) + let pager = toyos::paged::Pager::create(capacity, if anonymous { 0 } else { dirty }, REPORT) .unwrap_or_else(|e| panic!("fileserver: the {role:?} server makes no pager ({e:?}); its row is no pager row")); - Some(pager) + // A memory volume's objects are its files, which nothing can let go: they + // are bounded by what each is charged as it is made. + (Some(pager), (!anonymous).then_some(keeps as usize)) } /// A session on the partition diskserver serves under `guid`. diff --git a/system/fileserver/src/pages.rs b/system/fileserver/src/pages.rs index c663992624d..343a29c02bd 100644 --- a/system/fileserver/src/pages.rs +++ b/system/fileserver/src/pages.rs @@ -9,8 +9,9 @@ //! **An object per file, by its volume identity**, which the notices name it //! by. It holds the volume's node for its life, so a dirty file stays open //! on the volume whoever closed it, and outlives its last close for its pages' -//! sake until room is wanted: then a file nobody holds and nothing dirties -//! goes first, oldest first. Anonymous objects stay while named or held. +//! sake until room is wanted, or until a volume on a disk keeps its count of +//! files: then a file nobody holds and nothing dirties goes first, oldest +//! first. Anonymous objects stay while named or held. //! //! **A write is one write of the object's**, however many calls it takes: //! a call that stops short leaves it under way until its end, so no holder @@ -132,6 +133,9 @@ impl File { pub struct Files { pager: Option

, + /// The most files kept on a volume whose files can be let go; `None` for + /// a memory volume, whose objects are its files. + keeps: Option, files: BTreeMap, /// Each object's file, by the object's id, which the notices name. by_object: BTreeMap, @@ -141,8 +145,8 @@ pub struct Files { } impl Files

{ - pub fn new(pager: Option

) -> Self { - Self { pager, files: BTreeMap::new(), by_object: BTreeMap::new(), tick: 0, buf: Vec::new() } + pub fn new(pager: Option

, keeps: Option) -> Self { + Self { pager, keeps, files: BTreeMap::new(), by_object: BTreeMap::new(), tick: 0, buf: Vec::new() } } /// The pager, which reads ready while a notice is owed. @@ -217,8 +221,12 @@ impl Files

{ None => {} } let meta = volume.node_meta(node)?; - // Objects are bounded by count as pages are: a file nobody holds goes - // to make room for another's. + // A file nobody holds goes to make room for another's: at the count + // kept, or where the pager's job is charged no more. + if self.keeps.is_some_and(|keeps| self.files.len() >= keeps) && !self.drop_oldest(volume, identity) { + println!("fileserver: every file kept is held; file {identity} is refused"); + return Err(Error::NoSpace); + } let object = loop { let created = if volume.memory() { self.pg().anonymous(meta.size) } else { self.pg().object(meta.size) }; match created { diff --git a/system/fileserver/src/pages/tests.rs b/system/fileserver/src/pages/tests.rs index 720cb309585..f9278cae52e 100644 --- a/system/fileserver/src/pages/tests.rs +++ b/system/fileserver/src/pages/tests.rs @@ -37,7 +37,8 @@ impl crate::cache::Disk for RefusingRead {} /// The kernel's paged objects as far as the server can tell them apart: a /// table of `capacity` pages, `ceiling` of them dirty or under write-back, -/// and at most `objects` objects. +/// and at most `objects` objects, as many as the pager's job can be charged +/// for. #[derive(Default)] struct Model { capacity: usize, @@ -329,7 +330,7 @@ fn pattern(len: usize, seed: u8) -> Vec { fn memory_bytes_survive_close_sync_and_namespace_replacement_until_the_last_holder() { let mut volume = crate::memory::MemoryVolume::new(&[], clock); let fake = Fake::new(4, 0); - let mut files = Files::new(Some(fake.clone())); + let mut files = Files::new(Some(fake.clone()), None); let (node, id) = open(&mut files, &mut volume, "file"); files.write(&mut volume, id, node, 0, b"old contents").unwrap(); files.closed(&mut volume, id, node).unwrap(); @@ -358,7 +359,7 @@ fn memory_bytes_survive_close_sync_and_namespace_replacement_until_the_last_hold fn persistent_unlink_preserves_dirty_bytes_until_the_last_holder() { let mut v = volume(); let fake = Fake::new(64, 16); - let mut files = Files::new(Some(fake)); + let mut files = Files::new(Some(fake), None); let (node, id) = open(&mut files, &mut v, "home/held"); files.write(&mut v, id, node, 0, b"still held").unwrap(); files.held(id); @@ -378,7 +379,7 @@ fn persistent_unlink_preserves_dirty_bytes_until_the_last_holder() { fn memory_sparse_files_do_not_resurrect_truncated_bytes_or_evict_existing_data() { let mut volume = crate::memory::MemoryVolume::new(&[], clock); let fake = Fake::new(2, 0); - let mut files = Files::new(Some(fake.clone())); + let mut files = Files::new(Some(fake.clone()), None); let (node, id) = open(&mut files, &mut volume, "file"); files.write(&mut volume, id, node, PAGE + 5, b"tail").unwrap(); assert_eq!(fake.pages_of(files.object(id).unwrap()), 1, "the hole holds no pages"); @@ -408,7 +409,7 @@ fn a_read_is_supplied_off_the_volume_once_and_then_served_from_the_object() { v.write(n, 0, &data).unwrap(); v.close(n).unwrap(); let fake = Fake::new(64, 16); - let mut files = Files::new(Some(fake.clone())); + let mut files = Files::new(Some(fake.clone()), None); let at = open(&mut files, &mut v, "home/f"); assert_eq!(read(&mut files, &mut v, at, 0, data.len() + 50), data, "the whole file, and no byte past it"); assert_eq!(fake.0.borrow().supplies, 1, "four pages, one supply"); @@ -423,7 +424,7 @@ fn a_write_lands_in_the_object_and_reaches_the_volume_at_the_sync() { let n = v.open("home/f", CREATE).unwrap(); v.write(n, 0, &old).unwrap(); v.close(n).unwrap(); - let mut files = Files::new(Some(Fake::new(64, 16))); + let mut files = Files::new(Some(Fake::new(64, 16)), None); let (node, id) = open(&mut files, &mut v, "home/f"); files.write(&mut v, id, node, 5000, b"new bytes over the end of the file").unwrap(); files.write(&mut v, id, node, 5990, b"and past it").unwrap(); @@ -442,7 +443,7 @@ fn a_write_lands_in_the_object_and_reaches_the_volume_at_the_sync() { #[test] fn a_write_past_the_dirty_ceiling_writes_back_and_goes_on() { let mut v = volume(); - let mut files = Files::new(Some(Fake::new(64, 4))); + let mut files = Files::new(Some(Fake::new(64, 4)), None); let (node, id) = open(&mut files, &mut v, "home/f"); let data = pattern(16 * PAGE as usize, 3); files.write(&mut v, id, node, 0, &data).unwrap(); @@ -460,7 +461,7 @@ fn room_is_made_by_a_file_nobody_holds_before_any_other() { v.close(n).unwrap(); } let fake = Fake::new(8, 4); - let mut files = Files::new(Some(fake.clone())); + let mut files = Files::new(Some(fake.clone()), None); let a = open(&mut files, &mut v, "home/a"); read(&mut files, &mut v, a, 0, 4 * PAGE as usize); let b = open(&mut files, &mut v, "home/b"); @@ -476,7 +477,7 @@ fn room_is_made_by_a_file_nobody_holds_before_any_other() { #[test] fn a_shrink_drops_the_dirty_pages_before_the_volume_frees_their_blocks() { let mut v = volume(); - let mut files = Files::new(Some(Fake::new(64, 16))); + let mut files = Files::new(Some(Fake::new(64, 16)), None); let (node, id) = open(&mut files, &mut v, "home/f"); files.write(&mut v, id, node, 0, &pattern(3 * PAGE as usize, 5)).unwrap(); files.truncate(&mut v, id, node, 1000).unwrap(); @@ -492,7 +493,7 @@ fn a_shrink_drops_the_dirty_pages_before_the_volume_frees_their_blocks() { fn a_write_refused_part_way_is_cut_back_to_what_it_put() { let mut v = volume(); let fake = Fake::new(64, 16); - let mut files = Files::new(Some(fake.clone())); + let mut files = Files::new(Some(fake.clone()), None); let (node, id) = open(&mut files, &mut v, "home/f"); files.write(&mut v, id, node, 0, b"kept").unwrap(); fake.0.borrow_mut().refusing = true; @@ -507,7 +508,7 @@ fn a_write_refused_part_way_is_cut_back_to_what_it_put() { fn an_overwrite_keeps_incoming_capacity_while_its_older_dirty_page_is_flushed() { let mut v = volume(); let fake = Fake::new(4, 1); - let mut files = Files::new(Some(fake.clone())); + let mut files = Files::new(Some(fake.clone()), None); let (node, id) = open(&mut files, &mut v, "home/f"); files.write(&mut v, id, node, 0, &pattern(2 * PAGE as usize, 1)).unwrap(); files.fsync(&mut v, Some((id, node))).unwrap(); @@ -540,7 +541,7 @@ fn a_partial_writeback_cleans_only_its_prefix_and_retries_the_same_reserved_suff v.sync().unwrap(); let fake = Fake::new(4, 4); - let mut files = Files::new(Some(fake.clone())); + let mut files = Files::new(Some(fake.clone()), None); let (node, id) = open(&mut files, &mut v, "home/f"); let new = pattern(PAGE as usize + TAIL, 8); files.write(&mut v, id, node, PAGE, &new[PAGE as usize..]).unwrap(); @@ -568,7 +569,7 @@ fn a_partial_writeback_cleans_only_its_prefix_and_retries_the_same_reserved_suff fn cancelling_an_overwrite_before_copy_keeps_the_older_dirty_reservation() { let mut v = volume(); let fake = Fake::new(4, 4); - let mut files = Files::new(Some(fake.clone())); + let mut files = Files::new(Some(fake.clone()), None); let (node, id) = open(&mut files, &mut v, "home/f"); let old = pattern(PAGE as usize, 4); files.write(&mut v, id, node, PAGE, &old).unwrap(); @@ -586,7 +587,7 @@ fn cancelling_an_overwrite_before_copy_keeps_the_older_dirty_reservation() { fn a_supply_stops_before_a_present_page_and_resumes_after_the_write_copies_it() { let mut v = volume(); let fake = Fake::new(4, 4); - let mut files = Files::new(Some(fake)); + let mut files = Files::new(Some(fake), None); let (node, id) = open(&mut files, &mut v, "home/f"); files.write(&mut v, id, node, PAGE, &pattern(PAGE as usize, 6)).unwrap(); let replacement = pattern(3 * PAGE as usize, 7); @@ -607,7 +608,7 @@ fn a_write_refused_in_a_hole_gives_back_the_block_it_took() { toyos_blockdev::block_on(toyos_blockdev::BlockDev::write(&disk, 0, &stale), || ()).unwrap(); let mut v = DataVolume::format(disk, 256, &["home"], clock).unwrap(); let fake = Fake::new(64, 16); - let mut files = Files::new(Some(fake.clone())); + let mut files = Files::new(Some(fake.clone()), None); let (node, id) = open(&mut files, &mut v, "home/f"); files.write(&mut v, id, node, 0, &pattern(PAGE as usize, 4)).unwrap(); files.truncate(&mut v, id, node, 3 * PAGE).unwrap(); @@ -638,7 +639,7 @@ fn a_write_in_part_into_a_hole_supplies_zeros_around_it() { toyos_blockdev::block_on(toyos_blockdev::BlockDev::write(&disk, 0, &stale), || ()).unwrap(); let mut v = DataVolume::format(disk, 256, &["home"], clock).unwrap(); let fake = Fake::new(64, 16); - let mut files = Files::new(Some(fake.clone())); + let mut files = Files::new(Some(fake.clone()), None); let (node, id) = open(&mut files, &mut v, "home/f"); files.write(&mut v, id, node, 0, &pattern(PAGE as usize, 4)).unwrap(); files.truncate(&mut v, id, node, 3 * PAGE).unwrap(); @@ -661,7 +662,7 @@ fn a_requested_page_is_supplied_by_its_notice() { v.write(n, 0, &pattern(2 * PAGE as usize, 7)).unwrap(); v.close(n).unwrap(); let fake = Fake::new(64, 16); - let mut files = Files::new(Some(fake.clone())); + let mut files = Files::new(Some(fake.clone()), None); let (_, id) = open(&mut files, &mut v, "home/f"); let object = files.object(id).unwrap(); files.notice(&mut v, Notice::Requested { object, first: 1, count: 1 }); @@ -676,7 +677,7 @@ fn a_requested_page_is_supplied_by_its_notice() { fn a_truncating_open_leaves_the_object_no_page_of_before() { let mut v = volume(); let fake = Fake::new(64, 16); - let mut files = Files::new(Some(fake.clone())); + let mut files = Files::new(Some(fake.clone()), None); let (node, id) = open(&mut files, &mut v, "home/f"); files.write(&mut v, id, node, 0, &pattern(2 * PAGE as usize, 8)).unwrap(); @@ -703,7 +704,7 @@ fn a_file_the_pager_holds_is_read_again_from_its_object_across_closes() { v.write(n, 0, &pattern(PAGES * PAGE as usize, 9)).unwrap(); v.close(n).unwrap(); let fake = Fake::new(2 * PAGES, 16); - let mut files = Files::new(Some(fake.clone())); + let mut files = Files::new(Some(fake.clone()), None); let round = |files: &mut Files, v: &mut DataVolume| { let before = fake.0.borrow().supplies; let at = open(files, v, "home/seq"); @@ -718,14 +719,14 @@ fn a_file_the_pager_holds_is_read_again_from_its_object_across_closes() { assert_eq!(again, [0; 3], "supplies each round after the first"); } -/// The pager serves a bounded count of objects: a refused create makes room -/// by letting a file nobody holds go, and an open one is kept. +/// A create the pager's job cannot be charged for makes room by letting a +/// file nobody holds go, and an open one is kept. #[test] -fn an_object_refused_for_room_is_made_by_a_closed_files_going() { +fn an_object_refused_its_charge_is_made_by_a_closed_files_going() { let mut v = volume(); let fake = Fake::new(64, 16); fake.0.borrow_mut().objects = 2; - let mut files = Files::new(Some(fake.clone())); + let mut files = Files::new(Some(fake.clone()), None); let a = open(&mut files, &mut v, "home/a"); let b = open(&mut files, &mut v, "home/b"); files.closed(&mut v, b.1, b.0).unwrap(); @@ -738,6 +739,26 @@ fn an_object_refused_for_room_is_made_by_a_closed_files_going() { assert_eq!(files.opened(&mut v, identity, n), Err(Error::NoSpace), "both held open: refused, by name"); } +/// A volume on a disk keeps at most its count of files, whatever its pager's +/// job could be charged for: at the count, the oldest nobody holds goes, and +/// with every one held an open is refused. +#[test] +fn a_disk_volume_keeps_its_count_and_lets_its_oldest_closed_file_go() { + let mut v = volume(); + let fake = Fake::new(64, 16); + let mut files = Files::new(Some(fake.clone()), Some(2)); + let a = open(&mut files, &mut v, "home/a"); + let b = open(&mut files, &mut v, "home/b"); + files.closed(&mut v, b.1, b.0).unwrap(); + let c = open(&mut files, &mut v, "home/c"); + assert!(files.object(b.1).is_none(), "the closed file's went at the count"); + assert!(files.object(a.1).is_some() && files.object(c.1).is_some()); + assert_eq!(fake.0.borrow().sizes.len(), 2, "the pager serves the count"); + let n = v.open("home/d", CREATE).unwrap(); + let identity = v.identity("home/d").unwrap(); + assert_eq!(files.opened(&mut v, identity, n), Err(Error::NoSpace), "both held open: refused, by name"); +} + /// XSH 2.9.7, the server's half: a write that takes many calls — an absent /// page supplied, the dirty ceiling written back, a last page in part — is /// one write of the object's, so every state a holder can read between calls @@ -753,7 +774,7 @@ fn a_write_of_many_calls_is_read_whole_or_not_at_all() { v.write(n, 0, &old).unwrap(); v.close(n).unwrap(); let fake = Fake::new(64, 4); - let mut files = Files::new(Some(fake.clone())); + let mut files = Files::new(Some(fake.clone()), None); let at = open(&mut files, &mut v, "home/f"); read(&mut files, &mut v, at, 0, PAGES * page); let object = files.object(at.1).unwrap(); @@ -802,7 +823,7 @@ fn after_a_stop(v: DataVolume) -> DataVolume { fn an_fsync_makes_its_file_durable_and_no_other_files_overwrite() { let page = PAGE as usize; let mut v = stale_volume(); - let mut files = Files::new(Some(Fake::new(64, 16))); + let mut files = Files::new(Some(Fake::new(64, 16)), None); let b = open(&mut files, &mut v, "home/b"); files.write(&mut v, b.1, b.0, 0, &pattern(2 * page, 1)).unwrap(); assert_eq!(files.sync(&mut v).unwrap(), []); @@ -825,7 +846,7 @@ fn an_fsync_makes_its_file_durable_and_no_other_files_overwrite() { fn an_fsync_commits_its_file_without_publishing_another_files_dirty_size() { let page = PAGE as usize; let mut v = stale_volume(); - let mut files = Files::new(Some(Fake::new(64, 16))); + let mut files = Files::new(Some(Fake::new(64, 16)), None); let a = open(&mut files, &mut v, "home/a"); let c = open(&mut files, &mut v, "home/c"); files.write(&mut v, a.1, a.0, 0, &pattern(page, 4)).unwrap(); @@ -847,7 +868,7 @@ fn a_file_a_sync_could_not_write_back_stays_owed_past_another_s_fsync() { let page = PAGE as usize; let mut v = volume(); let fake = Fake::new(64, 16); - let mut files = Files::new(Some(fake.clone())); + let mut files = Files::new(Some(fake.clone()), None); let a = open(&mut files, &mut v, "home/a"); let f = open(&mut files, &mut v, "home/f"); files.write(&mut v, f.1, f.0, 0, &pattern(page, 6)).unwrap(); @@ -864,7 +885,7 @@ fn a_file_a_sync_could_not_write_back_stays_owed_past_another_s_fsync() { #[test] fn a_directorys_fsync_makes_a_new_name_durable() { let mut v = stale_volume(); - let mut files = Files::new(Some(Fake::new(64, 16))); + let mut files = Files::new(Some(Fake::new(64, 16)), None); v.mkdir("home/d").unwrap(); files.fsync(&mut v, None).unwrap(); let mut v = after_a_stop(v); @@ -877,7 +898,7 @@ fn a_directorys_fsync_makes_a_new_name_durable() { fn an_objects_size_is_the_volumes_after_every_change() { let mut v = volume(); let fake = Fake::new(64, 16); - let mut files = Files::new(Some(fake.clone())); + let mut files = Files::new(Some(fake.clone()), None); let n = v.open("home/f", CREATE).unwrap(); v.write(n, 0, b"before").unwrap(); v.close(n).unwrap(); diff --git a/tests/guest/pagerprobe/src/main.rs b/tests/guest/pagerprobe/src/main.rs index ecde923fe28..bf1bbd6b8a5 100644 --- a/tests/guest/pagerprobe/src/main.rs +++ b/tests/guest/pagerprobe/src/main.rs @@ -39,7 +39,7 @@ fn main() { if std::env::args().nth(1).as_deref() == Some("--hold") { return hold(); } - let pager = Pager::create(LARGE + 16, 0, 4, Duration::from_secs(1)).expect("pagerprobe: its row starts it as a pager"); + let pager = Pager::create(LARGE + 16, 0, Duration::from_secs(1)).expect("pagerprobe: its row starts it as a pager"); dirty_supply_prefix(&pager); let large = pager.object(LARGE * PAGE).expect("pagerprobe: the large object"); fill(&pager, large); diff --git a/tests/guest/rust-tests/src/bin/a_served_file_reads_through_its_paged_object.rs b/tests/guest/rust-tests/src/bin/a_served_file_reads_through_its_paged_object.rs index bbe10d6d733..17e7d08990c 100644 --- a/tests/guest/rust-tests/src/bin/a_served_file_reads_through_its_paged_object.rs +++ b/tests/guest/rust-tests/src/bin/a_served_file_reads_through_its_paged_object.rs @@ -83,7 +83,7 @@ fn main() { println!("a_served_file_reads_through_its_paged_object: std reads the same"); assert_eq!( - Pager::create(1, 0, 1, std::time::Duration::from_secs(1)).err(), + Pager::create(1, 0, std::time::Duration::from_secs(1)).err(), Some(toyos_abi::syscall::SyscallError::PermissionDenied), "a process started as no pager made one" ); diff --git a/tests/guest/rust-tests/src/bin/tmp_empty_files.rs b/tests/guest/rust-tests/src/bin/tmp_empty_files.rs new file mode 100644 index 00000000000..a0758ce67c1 --- /dev/null +++ b/tests/guest/rust-tests/src/bin/tmp_empty_files.rs @@ -0,0 +1,36 @@ +//! `/tmp` holds files by what each costs: an empty file holds no page and a +//! few hundred bytes of its server's, so more of them than the machine has +//! mebibytes of memory are made, each opened to read and closed, which tells +//! its server its last holder went, and removed. + +use std::fs; + +const DIR: &str = "/tmp/tmp_empty_files"; + +/// Files made past one for each MiB of the machine's memory. +const MARGIN: usize = 1024; + +fn name(i: usize) -> String { + format!("{DIR}/f{i:07}") +} + +fn main() { + let files = (toyos::system::memory_total() >> 20) as usize + MARGIN; + fs::create_dir(DIR).expect("tmp_empty_files: its directory"); + for i in 0..files { + if let Err(e) = fs::File::create_new(name(i)).and_then(|_| fs::File::open(name(i))) { + // What it made goes first: a `/tmp` left full fails every later + // job of the boot that writes it. + for made in 0..=i { + let _ = fs::remove_file(name(made)); + } + let _ = fs::remove_dir(DIR); + panic!("tmp_empty_files: create and open file {i} of {files}: {e:?}"); + } + } + for i in 0..files { + fs::remove_file(name(i)).unwrap_or_else(|e| panic!("tmp_empty_files: remove file {i}: {e:?}")); + } + fs::remove_dir(DIR).expect("tmp_empty_files: the emptied directory removed"); + println!("tmp_empty_files: {files} empty files, one per MiB of memory and {MARGIN} more, made, read-opened and removed"); +} diff --git a/tests/images/virttmpcase.toml b/tests/images/virttmpcase.toml new file mode 100644 index 00000000000..d53e2d48eef --- /dev/null +++ b/tests/images/virttmpcase.toml @@ -0,0 +1,29 @@ +# One job, `test_rs_tmp_empty_files`, on AArch64: `/tmp` is the memory file +# server's, whose paged objects the kernel makes and charges as the job's +# files ask for them. +[boot] +start = ["logkeeper", "uart", "fileserver", "test-runner"] + +[programs.logkeeper] +service = true +syscap = ["logread", "console"] +serves = ["log-wire"] +holds = ["log-origins"] + +[programs.uart] +devices = ["uart"] +holds = ["console-input"] +receives = ["log-wire"] +restart = true + +[programs.fileserver] +restart = true +pager = true +roles = ["memory"] + +# Four minutes from boot: the job runs on an emulated CPU. `power` because a +# job list's bound is a reboot, which the runner refuses to start without: a +# job past it reboots the guest with the job named. +[programs.test-runner] +receives = ["power"] +args = ["--bound-ms=240000", "test_rs_tmp_empty_files"] diff --git a/tests/toyos.rs b/tests/toyos.rs index 79c8bab39fc..75133d5c45a 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -383,6 +383,10 @@ const DRIVEN_AND_SHARED: &[&str] = &[ // Its shared run is each bench's verdict on its own CPU's fault path; // `a_user_copy_faults_in_mid_copy_or_ends_as_bad_address` runs it under QEMU. "user_copy_faults", + // Its shared run is each bench's verdict; + // `tmp_holds_empty_files_by_what_each_costs` runs it under QEMU, and + // `virt_tmp_holds_empty_files_by_what_each_costs` on AArch64's. + "tmp_empty_files", // Its shared run is ring's known answers in a guest; the crypto boot runs it // first on each bench, and `crypto_instrument_prints_its_table` before the // instrument under QEMU. @@ -449,6 +453,9 @@ const SCREEN_TESTS: &[ScreenTest] = &[ // the unprivileged copies and their EL1 fixup (`arch::uaccess`). ("virt_user_copy_faults", qemu::Profile::Virt, |profile, _| virt_job(profile, &format!("test_rs_{VIRT_USER_COPY}"))), ("virt_vm_map_shares", qemu::Profile::Virt, |profile, _| virt_job(profile, &format!("test_rs_{VIRT_VM_MAP}"))), + // `tmp_holds_empty_files_by_what_each_costs` on AArch64: the paged + // objects' table and its notices are shared memory-management code. + ("virt_tmp_holds_empty_files_by_what_each_costs", qemu::Profile::Virt, |profile, _| virt_tmp_holds_empty_files_by_what_each_costs(profile)), ("virt_ring0_timer_in_syscall", qemu::Profile::Virt, |profile, _| virt_job(profile, &format!("test_rs_{VIRT_RING0_TIMER}"))), // Emulated, with `virt_el1_smp`, `virt_off_names_the_cpus_left_on` and // `virt_reboot`: under HVF a woken thread can go undispatched for seconds @@ -714,6 +721,12 @@ const MACHINE_TESTS: &[MachineTest] = &[ ("a_user_copy_highest_first_faults_or_ends_as_bad_address", |test_config| { a_user_copy_faults_in_mid_copy_or_ends_as_bad_address(test_config, &["copies-go-down"]) }), + // `/tmp` holds more empty files than its guest has MiB of memory, and + // each one's last holder going reaches its server: the memory file + // server's objects as the kernel makes, charges and reports them, which + // no host build has. One boot of QEMU's x86-64 machine; the shared boots + // run it on each bench. + ("tmp_holds_empty_files_by_what_each_costs", |test_config| tmp_holds_empty_files_by_what_each_costs(test_config)), // The boot's log written through usbd onto the stick it booted from, and // still there once the machine has stopped: the path every metal boot's // `/log` takes, which no other QEMU row reads, because their console is @@ -5917,6 +5930,46 @@ fn ring3_debug_traps_end_their_process(test_config: &Path) -> Result<(), String> Ok(()) } +/// What `tmp_empty_files` says once every file it made, one for each MiB of +/// the machine's memory and 1,024 more, was opened to read and removed. +const TMP_FILES_SAID: &str = "empty files, one per MiB of memory and 1024 more, made, read-opened and removed"; + +/// `tmp_empty_files` alone: more empty files in `/tmp` than the guest has +/// MiB of memory, each opened to read and closed, and removed. +fn tmp_holds_empty_files_by_what_each_costs(test_config: &Path) -> Result<(), String> { + const JOB: &str = "tmp_empty_files"; + let bin = qemu::build_toyos_bin(qemu::SUITE_ARCH, &compile::repo_root().join("tests/guest/rust-tests"), JOB); + let mut qemu = QemuInstance::boot_with_options(test_config, &[], &[(JOB.to_string(), bin)], BootOptions::default()); + serial::Serial::boot(&qemu).must_be_clean()?; + let result = qemu.run_test("test_rs_tmp_empty_files", Duration::from_secs(300)); + if let Some(why) = &result.error { + return Err(format!("{why}\nthe job said:\n{}", result.stdout)); + } + if result.exit_code != Some(0) { + return Err(format!("the job ended {:?}:\n{}", result.exit_code, result.stdout)); + } + let Some(said) = result.stdout.lines().find(|l| l.contains(TMP_FILES_SAID)) else { + return Err(format!("the job never said {TMP_FILES_SAID:?}:\n{}", result.stdout)); + }; + eprintln!(" [tmp] {}", said.trim()); + Ok(()) +} + +/// The same job on AArch64: `tests/images/virttmpcase.toml`, whose one job it +/// is, beside the memory file server alone. +fn virt_tmp_holds_empty_files_by_what_each_costs(profile: qemu::Profile) -> Result<(), String> { + let config = compile::repo_root().join("tests/images/virttmpcase.toml"); + let options = BootOptions { + profile, + ready_marker: "control registers: SCTLR_EL1=", + extra_root_files: vec![suite_bin(profile.arch(), "tmp_empty_files")], + ..Default::default() + }; + let mut qemu = QemuInstance::boot_with_options(&config, &[], &[], options); + let mut serial = virt_console(&qemu); + judge_virt_job(&mut qemu, &mut serial, "test_rs_tmp_empty_files", TMP_FILES_SAID) +} + /// `std_threads` on a guest of [`BootOptions`]' CPUs: it asserts what it /// frees itself, and the CPU count it prints is held here to the CPUs the /// guest was booted with.