fix: pm running apps grow (death-bit bound 60); spawntest stress: 30 files open, apps until BUSY then room comes back; docs: limits and why

This commit is contained in:
2026-10-28 16:22:19 +01:00
parent 1f2134fcec
commit 0de13f124d
3 changed files with 130 additions and 4 deletions

View File

@@ -27,6 +27,9 @@ const STORAGE_BUF_VA: types::address::VirtualAddress = types::address::VirtualAd
const NARROW_BUF_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x0190_0000); const NARROW_BUF_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x0190_0000);
const REQUEST_PAGE_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x01C0_0000); const REQUEST_PAGE_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x01C0_0000);
const HOME_BUF_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x01B0_0000); const HOME_BUF_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x01B0_0000);
const CROWD_PAGE_VA: types::address::VirtualAddress = types::address::VirtualAddress::from_raw(0x01E0_0000);
/// Apps the old tables held beside Home; the test wants more than this.
const OLD_APP_ROOM: usize = 7;
/// Read back from the disk image by scripts/test-qemu.sh. /// Read back from the disk image by scripts/test-qemu.sh.
const NOTE: &[u8] = b"written by k-os on ext4\n"; const NOTE: &[u8] = b"written by k-os on ext4\n";
@@ -267,6 +270,89 @@ fn restarted_after_hanging() -> Result<(), &'static str> {
Ok(()) Ok(())
} }
/// Waits about `ms` on `alarm` (woken early by anything else it carries).
fn nap(alarm: CapSlot, ms: u64) {
let at = syscall::get_time().raw() + ms * types::time::TICKS_PER_MS;
if syscall::set_timer(alarm, types::time::Ticks::from_raw(at)).is_ok() {
let _ = syscall::wait(alarm);
}
}
/// Apps run until the permission manager says there is no room (busy, not
/// a failure), well past what the old tables held; once they are stopped,
/// the room comes back, all of it.
fn many_apps_and_their_room_comes_back(alarm: CapSlot) -> Result<usize, &'static str> {
use types::apps::reply;
let apps = libkos::apps::Apps::connect(PERMISSION, CROWD_PAGE_VA).ok_or("no permission manager")?;
let mut running = [None; 32];
let mut n = 0;
let refusal = loop {
match apps.launch(&["sleep", "300"], None, None) {
Ok((p, _)) if n < running.len() => {
running[n] = Some(p);
n += 1;
}
Ok(_) => return Err("more room than the test can hold"),
Err(r) => break r.code,
}
};
let stop = |running: &mut [Option<CapSlot>]| {
for p in running.iter_mut().filter_map(Option::take) {
let _ = syscall::kill(p);
syscall::drop_cap(p);
}
};
stop(&mut running);
if refusal != reply::BUSY {
return Err("a full permission manager did not say it was busy");
}
if n <= OLD_APP_ROOM {
return Err("no more apps ran than the old tables held");
}
// The deaths are its to hear; give it a moment, then all of it again.
let mut again = 0;
for _ in 0..100 {
match apps.launch(&["sleep", "300"], None, None) {
Ok((p, _)) => {
running[again] = Some(p);
again += 1;
if again == n {
break;
}
}
Err(r) if r.code == reply::BUSY => nap(alarm, 50),
Err(_) => {
stop(&mut running);
return Err("launching again failed");
}
}
}
stop(&mut running);
if again < n {
return Err("the room did not all come back");
}
Ok(n)
}
/// More files open at once than the old tables held (8 in ext4).
fn many_files_open_at_once(fs: &mut Storage) -> Result<(), &'static str> {
let mut open = [None; 30];
let mut result = Ok(());
for slot in open.iter_mut() {
match fs.open(b"selftest.txt") {
Ok(h) => *slot = Some(h),
Err(_) => {
result = Err("cannot hold 30 files open");
break;
}
}
}
for h in open.into_iter().flatten() {
let _ = fs.close(h);
}
result
}
/// A server that runs off its stack faults on the guard below it, and is /// A server that runs off its stack faults on the guard below it, and is
/// restarted like any other that died. /// restarted like any other that died.
fn restarted_after_overflowing() -> Result<(), &'static str> { fn restarted_after_overflowing() -> Result<(), &'static str> {
@@ -316,6 +402,12 @@ fn check() -> Result<(), &'static str> {
restarted_after_hanging()?; restarted_after_hanging()?;
libkos::println!("spawntest: a hung server is noticed, stopped and restarted: ok"); libkos::println!("spawntest: a hung server is noticed, stopped and restarted: ok");
many_files_open_at_once(&mut fs)?;
libkos::println!("spawntest: 30 files open at once: ok");
let crowd = many_apps_and_their_room_comes_back(died)?;
libkos::println!("spawntest: {} apps at once, then busy, and the room comes back: ok", crowd);
restarted_after_overflowing()?; restarted_after_overflowing()?;
libkos::println!("spawntest: a server that overflows its stack is caught and restarted: ok"); libkos::println!("spawntest: a server that overflows its stack is caught and restarted: ok");
Ok(()) Ok(())

View File

@@ -26,7 +26,10 @@ use crate::store::DiskStore;
/// An app's limits; its pages are what its manifest asks for. /// An app's limits; its pages are what its manifest asks for.
const PROGRAM: Limits = Limits { pages: 0, endpoints: 1, notifications: 1, regions: 1 }; const PROGRAM: Limits = Limits { pages: 0, endpoints: 1, notifications: 1, regions: 1 };
/// Apps running at once; a death is a bit each on the notification. /// Apps running at once; a death is a bit each on the notification.
const MAX_RUNNING: usize = 8; /// Apps running at once, at most: each has a bit of the deaths
/// notification, and the bits from 60 up mark launcher and manager copies.
/// What there is room for is the allowance's to say, well before this.
const MAX_RUNNING: usize = 60;
const MAX_ARGS: usize = 16; const MAX_ARGS: usize = 16;
/// The session's desktop, started with it and again if it dies. /// The session's desktop, started with it and again if it dies.
const HOME: &str = "kos.home"; const HOME: &str = "kos.home";
@@ -72,7 +75,8 @@ pub struct PermissionServer {
/// Bound to the serving thread: deaths of running apps. /// Bound to the serving thread: deaths of running apps.
deaths: CapSlot, deaths: CapSlot,
staged: Staged, staged: Staged,
running: [Option<Running>; MAX_RUNNING], /// By death bit; grown as apps start.
running: Vec<Option<Running>>,
/// Home's starts this session. /// Home's starts this session.
homes: u8, homes: u8,
} }
@@ -136,7 +140,7 @@ fn parts(page: &[u8]) -> impl Iterator<Item = &str> {
impl PermissionServer { impl PermissionServer {
pub fn new(store: DiskStore, consent: TerminalConsent, ports: Ports, deaths: CapSlot) -> Self { pub fn new(store: DiskStore, consent: TerminalConsent, ports: Ports, deaths: CapSlot) -> Self {
let Ports { init, display, session, endpoint } = ports; let Ports { init, display, session, endpoint } = ports;
Self { store, consent, init, display, session, endpoint, deaths, staged: Staged::default(), running: [const { None }; MAX_RUNNING], homes: 0 } Self { store, consent, init, display, session, endpoint, deaths, staged: Staged::default(), running: Vec::new(), homes: 0 }
} }
/// What this device has a server for. /// What this device has a server for.
@@ -210,7 +214,10 @@ impl PermissionServer {
let command = *args.first().ok_or(code(reply::NOT_INSTALLED))?; let command = *args.first().ok_or(code(reply::NOT_INSTALLED))?;
let available = self.available(); let available = self.available();
let plan = policy::plan_launch(&mut self.store, command, available).map_err(refused)?; let plan = policy::plan_launch(&mut self.store, command, available).map_err(refused)?;
let index = self.running.iter().position(Option::is_none).ok_or(code(reply::BUSY))?; let index = self.running.iter().position(Option::is_none).unwrap_or(self.running.len());
if index >= MAX_RUNNING {
return Err(code(reply::BUSY));
}
let file = self.store.binary_grant(&plan.app).ok_or(code(reply::FAILED))?; let file = self.store.binary_grant(&plan.app).ok_or(code(reply::FAILED))?;
let spawned = libkos::spawn::spawn(self.init, file, Limits { pages: plan.app.memory.pages(), ..PROGRAM }); let spawned = libkos::spawn::spawn(self.init, file, Limits { pages: plan.app.memory.pages(), ..PROGRAM });
@@ -278,6 +285,9 @@ impl PermissionServer {
syscall::drop_cap(child.process); syscall::drop_cap(child.process);
return Err(code(reply::FAILED)); return Err(code(reply::FAILED));
} }
if self.running.len() <= index {
self.running.resize_with(index + 1, || None);
}
self.running[index] = Some(Running { id: plan.app.id, process: child.process, granted: plan.granted, data, window, manage }); self.running[index] = Some(Running { id: plan.app.id, process: child.process, granted: plan.granted, data, window, manage });
Ok((child.process, plan.granted)) Ok((child.process, plan.granted))
} }

View File

@@ -365,6 +365,30 @@ registers are saved when a trap finds them changed and loaded whenever it
runs; a thread without them runs with the FPU off, so it can never read runs; a thread without them runs with the FPU off, so it can never read
another's. The kernel itself never uses the FPU. another's. The kernel itself never uses the FPU.
## Limits
What is fixed, and why (the rest grows with the server's memory):
- **Kernel tables** (a kernel without a heap): processes 64, threads 256
(4 a process), endpoints 256, notifications 128, regions 256, 255
capability slots a process (`CapSlot`, a byte with room for
`start_arg`'s plus one). Kani proves the same code with tiny tables.
- **What owns a big resource is counted by it**: a window is a pixel
buffer of the display's (`types::display::window::MAX_WINDOWS`, 8), a
storage connection a buffer of storage's (`types::storage::MAX_CLIENTS`,
16); those budgets are computed from the counts, in one place, and the
address layouts are checked against them when built.
- **Bits**: init has a death bit per spawned child (16) and per supervised
program; the permission manager one per running app (60); both are
checked when built.
- **Grown, not counted**: storage's handles, open files and grants, the
permission manager's running apps. They stop where the server's memory
or its spawn allowance does, and say so: storage answers `FULL`, init
`STORAGE_BUSY`, the permission manager `BUSY`.
The self-test pushes past the old counts (30 files open, as many apps as
the allowance holds, a stack overflow), so a regression shows.
## Budgets ## Budgets
Every process has a budget: frames (its pages, page tables and regions' Every process has a budget: frames (its pages, page tables and regions'