feat(init): watchdog: pinger thread PINGs servers (framework answers, no server code), main kills after 5s unanswered -> crash path; not pinged: terminal, pm (handlers wait for user); give-up drains queued ping; spawntest hang check

This commit is contained in:
2026-10-28 01:51:39 +01:00
parent 28f1ace00a
commit 3d69c10079
9 changed files with 236 additions and 18 deletions

View File

@@ -200,6 +200,22 @@ fn restarted_after_crashes() -> Result<(), &'static str> {
Ok(()) Ok(())
} }
/// A server stuck in a handler is noticed by the watchdog, killed (which
/// releases its caller) and restarted.
fn restarted_after_hanging() -> Result<(), &'static str> {
let asked = syscall::get_time().raw();
// Comes back, with an error, once the hung server is killed.
let _ = syscall::request(FLAKY, [flaky::op::HANG, 0, 0, 0]);
let hung_ms = (syscall::get_time().raw() - asked) / 10_000;
if hung_ms < 4_500 {
return Err("the hung server was stopped before the watchdog's deadline");
}
if flaky()? != 1 {
return Err("the hung server was not restarted");
}
Ok(())
}
fn check() -> Result<(), &'static str> { fn check() -> Result<(), &'static str> {
let mut fs = Storage::attach(STORAGE, STORAGE_BUF_VA).map_err(|_| "cannot attach to storage")?; let mut fs = Storage::attach(STORAGE, STORAGE_BUF_VA).map_err(|_| "cannot attach to storage")?;
write_a_file(&mut fs)?; write_a_file(&mut fs)?;
@@ -232,6 +248,9 @@ fn check() -> Result<(), &'static str> {
restarted_after_crashes()?; restarted_after_crashes()?;
libkos::println!("spawntest: a crashed server is restarted, a crash loop backs off: ok"); libkos::println!("spawntest: a crashed server is restarted, a crash loop backs off: ok");
restarted_after_hanging()?;
libkos::println!("spawntest: a hung server is noticed, stopped and restarted: ok");
Ok(()) Ok(())
} }

View File

@@ -47,6 +47,10 @@ pub(crate) struct Launcher<'a> {
console: Option<CapSlot>, console: Option<CapSlot>,
/// Signals the death notification's timer bit. /// Signals the death notification's timer bit.
pub(crate) timer: CapSlot, pub(crate) timer: CapSlot,
/// Signals that came in while init waited elsewhere, still to handle.
pub(crate) pending: usize,
/// When the watchdog's last round of pings started.
pub(crate) last_round: u64,
} }
/// What became of starting a program. /// What became of starting a program.
@@ -88,6 +92,8 @@ impl<'a> Launcher<'a> {
supervised: [None; MAX_SUPERVISED], supervised: [None; MAX_SUPERVISED],
console: libkos::start::console(), console: libkos::start::console(),
timer, timer,
pending: 0,
last_round: 0,
}) })
} }

View File

@@ -13,6 +13,7 @@ mod selftest;
mod session; mod session;
mod spawn; mod spawn;
mod supervise; mod supervise;
mod watchdog;
use libkos::fs::Storage; use libkos::fs::Storage;
use libkos::syscall; use libkos::syscall;
@@ -65,6 +66,11 @@ pub fn run() -> ! {
if !launcher.watch_storage() { if !launcher.watch_storage() {
crate::outln!("init: cannot watch storage"); crate::outln!("init: cannot watch storage");
} }
if !watchdog::start() {
crate::outln!("init: no watchdog thread; hangs go unnoticed");
}
// Starts the clock the watchdog runs on.
launcher.supervise(0);
// The test image carries the SMP self-test; it and the tests run // The test image carries the SMP self-test; it and the tests run
// instead of the desktop, and the last one halts the machine. // instead of the desktop, and the last one halts the machine.

View File

@@ -58,6 +58,9 @@ pub struct Program {
/// Its clients keep state in it (a subscription, focus, a handle), so /// Its clients keep state in it (a subscription, focus, a handle), so
/// they are restarted after it is. /// they are restarted after it is.
pub restarts_clients: bool, pub restarts_clients: bool,
/// Pinged by the watchdog (see init's watchdog.rs): not if a handler of
/// its waits for the user, which a ping cannot tell from a hang.
pub pinged: bool,
} }
/// Enough for a small program: its image, stack and a little heap. /// Enough for a small program: its image, stack and a little heap.
@@ -85,6 +88,7 @@ const fn server(name: &'static str, file: &'static [u8], limits: Limits) -> Prog
takes_ctrl_c: false, takes_ctrl_c: false,
restart: true, restart: true,
restarts_clients: false, restarts_clients: false,
pinged: true,
} }
} }
@@ -103,8 +107,8 @@ pub const SERVERS: &[Program] = &[
..server("display", b"bin/display", Limits { pages: 1280, regions: 4, ..SMALL }) ..server("display", b"bin/display", Limits { pages: 1280, regions: 4, ..SMALL })
}, },
// Text on the display, lines from the keyboard (it takes input focus). // Text on the display, lines from the keyboard (it takes input focus).
// Its clients wait on lines and Ctrl+C. // Its clients wait on lines and Ctrl+C; `READ` waits for the user.
Program { uses: &["display"], restarts_clients: true, ..server("terminal", b"bin/terminal", SMALL) }, Program { uses: &["display"], restarts_clients: true, pinged: false, ..server("terminal", b"bin/terminal", SMALL) },
server("session", b"bin/session", SMALL), server("session", b"bin/session", SMALL),
]; ];
@@ -133,6 +137,8 @@ pub const SESSION: &[Program] = &[Program {
spawns: Some(Limits { pages: 512, endpoints: 8, notifications: 8, regions: 8 }), spawns: Some(Limits { pages: 512, endpoints: 8, notifications: 8, regions: 8 }),
// The shell holds handles to the apps it ran, which die with it. // The shell holds handles to the apps it ran, which die with it.
restarts_clients: true, restarts_clients: true,
// Installing waits for the user's consent.
pinged: false,
..server("permission", b"bin/permission", Limits { pages: 96, ..SMALL }) ..server("permission", b"bin/permission", Limits { pages: 96, ..SMALL })
}]; }];

View File

@@ -19,6 +19,7 @@ use types::time::Ticks;
use crate::launch::{Launched, Launcher}; use crate::launch::{Launched, Launcher};
use crate::manifest::Program; use crate::manifest::Program;
use crate::watchdog;
/// Programs watched at once, at most. /// Programs watched at once, at most.
pub(crate) const MAX_SUPERVISED: usize = 12; pub(crate) const MAX_SUPERVISED: usize = 12;
@@ -35,6 +36,8 @@ const TICKS_PER_MS: u64 = 10_000;
const DELAYS_MS: [u64; 4] = [0, 1_000, 5_000, 30_000]; const DELAYS_MS: [u64; 4] = [0, 1_000, 5_000, 30_000];
/// A run this long was not part of a crash loop. /// A run this long was not part of a crash loop.
const STABLE_MS: u64 = 60_000; const STABLE_MS: u64 = 60_000;
/// How often init looks at the clock for the watchdog.
const TICK_MS: u64 = 1_000;
/// How many crashes in a row, and so how long before the next restart. /// How many crashes in a row, and so how long before the next restart.
#[derive(Clone, Copy, Default)] #[derive(Clone, Copy, Default)]
@@ -76,6 +79,10 @@ pub(crate) struct Supervised {
spawn: Option<Badge>, spawn: Option<Badge>,
/// Until what this run held is let go of. /// Until what this run held is let go of.
holding: bool, holding: bool,
/// Its endpoint, if it serves one.
ep: Option<CapSlot>,
/// Killed for hanging: the watchdog's ping is queued on its endpoint.
hung: bool,
} }
impl Supervised { impl Supervised {
@@ -90,7 +97,7 @@ impl Supervised {
} }
} }
fn now_ms() -> u64 { pub(crate) fn now_ms() -> u64 {
syscall::get_time().raw() / TICKS_PER_MS syscall::get_time().raw() / TICKS_PER_MS
} }
@@ -105,6 +112,7 @@ impl Launcher<'_> {
let index = self.supervised.iter().position(Option::is_none).ok_or("too many programs to watch")?; let index = self.supervised.iter().position(Option::is_none).ok_or("too many programs to watch")?;
let started = self.launch(p)?; let started = self.launch(p)?;
let note = started.note; let note = started.note;
let ep = started.ep;
if !self.watch(started.process, bit(index)) { if !self.watch(started.process, bit(index)) {
let _ = syscall::kill(started.process); let _ = syscall::kill(started.process);
self.forget(started); self.forget(started);
@@ -118,7 +126,10 @@ impl Launcher<'_> {
grants: started.grants, grants: started.grants,
spawn: started.spawn, spawn: started.spawn,
holding: true, holding: true,
ep,
hung: false,
}); });
self.ping(index);
Ok(note) Ok(note)
} }
@@ -161,8 +172,18 @@ impl Launcher<'_> {
self.release(grants, spawn); self.release(grants, spawn);
} }
/// Has the watchdog ping program `index`, if it is to be pinged.
fn ping(&self, index: usize) {
if let Some(s) = self.supervised[index]
&& let Some(ep) = s.ep.filter(|_| s.program.pinged)
{
watchdog::watch(index, ep);
}
}
/// Stops a running program on purpose; `restart` it once it is dead. /// Stops a running program on purpose; `restart` it once it is dead.
fn stop(&mut self, index: usize, restart: bool) { fn stop(&mut self, index: usize, restart: bool) {
watchdog::unwatch(index);
let Some(s) = self.supervised[index].as_mut() else { return }; let Some(s) = self.supervised[index].as_mut() else { return };
if let State::Running { process, .. } = s.state { if let State::Running { process, .. } = s.state {
let _ = syscall::kill(process); let _ = syscall::kill(process);
@@ -187,19 +208,45 @@ impl Launcher<'_> {
/// Death signals and the timer, from init's server loop. /// Death signals and the timer, from init's server loop.
pub(crate) fn supervise(&mut self, bits: usize) { pub(crate) fn supervise(&mut self, bits: usize) {
if bits & STORAGE_BIT != 0 { self.pending |= bits;
crate::outln!("init: storage died; nothing can go on without it"); while self.pending != 0 {
crate::idle(); let bits = core::mem::take(&mut self.pending);
} if bits & STORAGE_BIT != 0 {
for index in 0..MAX_SUPERVISED { crate::outln!("init: storage died; nothing can go on without it");
if bits & bit(index) != 0 { crate::idle();
self.died(index); }
for index in 0..MAX_SUPERVISED {
if bits & bit(index) != 0 {
self.died(index);
}
} }
} }
self.watch_the_clock();
self.restart_due(); self.restart_due();
} }
/// Kills a server whose ping has gone unanswered; starts ping rounds.
fn watch_the_clock(&mut self) {
let now = now_ms();
if let Some(index) = watchdog::hung(now)
&& let Some(s) = self.supervised[index].as_mut()
&& let State::Running { process, .. } = s.state
&& !s.hung
{
crate::outln!("init: {} stopped answering; restarting it", s.program.name);
s.hung = true;
watchdog::unwatch(index);
// Its death comes back as a crash.
let _ = syscall::kill(process);
}
if now.saturating_sub(self.last_round) >= watchdog::ROUND_MS {
self.last_round = now;
watchdog::round();
}
}
fn died(&mut self, index: usize) { fn died(&mut self, index: usize) {
watchdog::unwatch(index);
// Nothing left to let go of if init stopped it. // Nothing left to let go of if init stopped it.
self.let_go(index); self.let_go(index);
let Some(s) = self.supervised[index] else { return }; let Some(s) = self.supervised[index] else { return };
@@ -219,15 +266,23 @@ impl Launcher<'_> {
let mut backoff = s.backoff; let mut backoff = s.backoff;
let delay = restart.and_then(|ran| if crashed { backoff.after_crash(ran) } else { Some(0) }); let delay = restart.and_then(|ran| if crashed { backoff.after_crash(ran) } else { Some(0) });
match (crashed, delay) { match (crashed, delay) {
(true, Some(_)) => crate::outln!("init: {} died, restarting it", s.program.name), (true, Some(_)) if !s.hung => crate::outln!("init: {} died, restarting it", s.program.name),
(true, None) if s.program.restart => crate::outln!("init: {} keeps dying; not restarting it again", s.program.name), (true, None) if s.program.restart => crate::outln!("init: {} keeps dying; not restarting it again", s.program.name),
_ => {} _ => {}
} }
// Nobody will take the ping a hung server left queued: init does.
if delay.is_none()
&& s.hung
&& let Some(ep) = s.ep.filter(|_| watchdog::pinging(index))
{
self.pending |= watchdog::release_pinger(ep);
}
self.supervised[index] = delay.map(|d| Supervised { self.supervised[index] = delay.map(|d| Supervised {
state: State::Waiting { at: now_ms() + d }, state: State::Waiting { at: now_ms() + d },
backoff, backoff,
grants: [None; 2], grants: [None; 2],
spawn: None, spawn: None,
hung: false,
..s ..s
}); });
} }
@@ -250,8 +305,10 @@ impl Launcher<'_> {
grants: started.grants, grants: started.grants,
spawn: started.spawn, spawn: started.spawn,
holding: true, holding: true,
ep: started.ep,
..s ..s
}); });
self.ping(index);
if s.program.restarts_clients { if s.program.restarts_clients {
self.restart_users_of(s.program.name); self.restart_users_of(s.program.name);
} }
@@ -268,13 +325,13 @@ impl Launcher<'_> {
} }
} }
} }
// The clock ticks for the watchdog whatever else is due.
let next = self.supervised.iter().flatten().filter_map(|s| match s.state { let next = self.supervised.iter().flatten().filter_map(|s| match s.state {
State::Waiting { at } => Some(at), State::Waiting { at } => Some(at),
_ => None, _ => None,
}).min(); });
if let Some(at) = next { let tick = now + TICK_MS;
self.wake_at(at); self.wake_at(next.fold(tick, u64::min));
}
} }
/// Whoever calls `name` kept state in the old one: restart them too. /// Whoever calls `name` kept state in the old one: restart them too.

108
crates/init/src/watchdog.rs Normal file
View File

@@ -0,0 +1,108 @@
//! Noticing a server that hangs (see the vision's Watchdog note). A thread
//! of init's own calls each watched server with `server_framework::PING`
//! in turn; the framework answers it without the server's code, so only a
//! server stuck inside a handler (a deadlock, a loop) leaves it unanswered.
//! Init's main thread, which must never wait on a server, only watches the
//! clock: a ping unanswered for `HANG_MS` gets the server killed, and the
//! death is handled like a crash (supervise.rs). The ping stays queued and
//! the restarted server answers it.
//!
//! Servers whose handlers wait for the user (the terminal's `READ`, the
//! permission manager's consent prompt) cannot be told from hung servers
//! this way, so they are not pinged.
use core::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use libkos::syscall;
use types::bounded::CapSlot;
use crate::supervise::MAX_SUPERVISED;
/// Unanswered this long, a server is hung.
pub(crate) const HANG_MS: u64 = 5_000;
/// How often a round of pings starts.
pub(crate) const ROUND_MS: u64 = 2_000;
const NONE: usize = usize::MAX;
/// Each supervised program's endpoint, if it is pinged.
static TARGETS: [AtomicUsize; MAX_SUPERVISED] = [const { AtomicUsize::new(NONE) }; MAX_SUPERVISED];
/// Which one the pinger is waiting on (`NONE`: none), and since when.
static PINGING: AtomicUsize = AtomicUsize::new(NONE);
static SINCE_MS: AtomicU64 = AtomicU64::new(0);
/// The pinger's notification: a signal starts a round.
static WAKE: AtomicUsize = AtomicUsize::new(NONE);
fn slot(raw: usize) -> Option<CapSlot> {
u8::try_from(raw).ok().and_then(CapSlot::new)
}
/// Starts the pinger.
pub(crate) fn start() -> bool {
let Ok(wake) = syscall::create_notification() else { return false };
WAKE.store(wake.value() as usize, Ordering::Release);
libkos::thread::spawn(&WAKE, pinger)
}
pub(crate) fn watch(index: usize, ep: CapSlot) {
TARGETS[index].store(ep.value() as usize, Ordering::Release);
}
pub(crate) fn unwatch(index: usize) {
TARGETS[index].store(NONE, Ordering::Release);
}
/// Has the pinger start a round (it finishes the last one first).
pub(crate) fn round() {
if let Some(wake) = slot(WAKE.load(Ordering::Acquire)) {
let _ = syscall::signal(wake);
}
}
/// The program whose ping has gone unanswered too long, if any.
pub(crate) fn hung(now_ms: u64) -> Option<usize> {
let index = PINGING.load(Ordering::Acquire);
let since = SINCE_MS.load(Ordering::Acquire);
(index != NONE && now_ms.saturating_sub(since) >= HANG_MS).then_some(index)
}
/// Whether the pinger waits on `index` right now.
pub(crate) fn pinging(index: usize) -> bool {
PINGING.load(Ordering::Acquire) == index
}
fn pinger(_: &'static AtomicUsize) -> ! {
loop {
if let Some(wake) = slot(WAKE.load(Ordering::Acquire)) {
let _ = syscall::wait(wake);
}
for (index, target) in TARGETS.iter().enumerate() {
let Some(ep) = slot(target.load(Ordering::Acquire)) else { continue };
SINCE_MS.store(crate::supervise::now_ms(), Ordering::Release);
PINGING.store(index, Ordering::Release);
let _ = syscall::request(ep, [server_framework::PING, 0, 0, 0]);
PINGING.store(NONE, Ordering::Release);
}
}
}
/// Init gave up on a server it killed for hanging: the pinger's ping is
/// still queued on its endpoint, and nothing will ever take it. Init takes
/// it (failing anyone else queued there), so the pinger goes on. Signals
/// that arrive meanwhile are returned, to be handled after.
pub(crate) fn release_pinger(ep: CapSlot) -> usize {
use libkos::syscall::Received;
let mut signals = 0;
loop {
match syscall::receive_msg(ep) {
Ok(Received::Call { badge: 0, words, .. }) if words[0] == server_framework::PING => {
let _ = syscall::reply([server_framework::PING, 0, 0, 0], None);
return signals;
}
Ok(Received::Call { .. }) => {
let _ = syscall::reply([server_framework::MALFORMED, 0, 0, 0], None);
}
Ok(Received::Message { .. }) => {}
Ok(Received::Signal { bits }) => signals |= bits,
Err(_) => return signals,
}
}
}

View File

@@ -7,4 +7,4 @@ pub mod server;
pub use codec::{Decode, Encode}; pub use codec::{Decode, Encode};
pub use executor::block_on; pub use executor::block_on;
pub use server::{MALFORMED, Server, serve}; pub use server::{MALFORMED, PING, Server, serve};

View File

@@ -9,6 +9,11 @@ use types::bounded::CapSlot;
/// tag, so every client decodes it as an error. /// tag, so every client decodes it as an error.
pub const MALFORMED: usize = usize::MAX; pub const MALFORMED: usize = usize::MAX;
/// Init's watchdog asking whether the server still serves: answered with
/// the same word by the loop itself, never seen by the server's code. No
/// protocol uses it either.
pub const PING: usize = usize::MAX - 1;
pub trait Server { pub trait Server {
type Request: Decode; type Request: Decode;
type Response: Encode; type Response: Encode;
@@ -37,8 +42,9 @@ pub trait Server {
/// Serves `endpoint` until receiving on it fails (it was destroyed or /// Serves `endpoint` until receiving on it fails (it was destroyed or
/// the capability revoked), then returns why. Every `Call` gets a /// the capability revoked), then returns why. Every `Call` gets a
/// reply, `[MALFORMED, ..]` if it does not decode; a one-way `Send` is /// reply, `[MALFORMED, ..]` if it does not decode, `[PING, ..]` to a
/// handled without one; notification signals go to `signal`. /// `PING`; a one-way `Send` is handled without one; notification
/// signals go to `signal`.
fn run(&mut self, endpoint: CapSlot) -> libkos::syscall::Error { fn run(&mut self, endpoint: CapSlot) -> libkos::syscall::Error {
use libkos::syscall::{Received, receive_msg, reply_recv}; use libkos::syscall::{Received, receive_msg, reply_recv};
let mut next = receive_msg(endpoint); let mut next = receive_msg(endpoint);
@@ -48,6 +54,7 @@ pub trait Server {
Err(e) => return e, Err(e) => return e,
}; };
next = match message { next = match message {
Received::Call { words, .. } if words[0] == PING => reply_recv(endpoint, [PING, 0, 0, 0], None),
Received::Call { badge, words, cap } => { Received::Call { badge, words, cap } => {
let (reply, give) = self.handle_call(badge, words, cap); let (reply, give) = self.handle_call(badge, words, cap);
reply_recv(endpoint, reply, give) reply_recv(endpoint, reply, give)

View File

@@ -136,6 +136,15 @@ it holds the key and every grant. Init's main thread never waits on a
server it restarts: its own output goes through a thread of its own server it restarts: its own output goes through a thread of its own
(`init/src/output.rs`), which waits on the log server instead. (`init/src/output.rs`), which waits on the log server instead.
Hangs are noticed by pinging (`init/src/watchdog.rs`): another thread of
init's calls each server with `server_framework::PING` every 2 s, which
the framework's loop answers without the server's code. A ping
unanswered for 5 s means the server is stuck in a handler; init kills it,
and the death is handled as a crash, so the restarted server answers the
still-queued ping. The terminal and the permission manager are not
pinged: their handlers wait for the user (`READ`, a consent prompt),
which a ping cannot tell from a hang.
A program's `uses` endpoints arrive as badged copies with `Execute` and A program's `uses` endpoints arrive as badged copies with `Execute` and
`Delegate`: the badge (unique per copy) tells the server which client is `Delegate`: the badge (unique per copy) tells the server which client is
calling, and `Delegate` lets the client send capabilities with its calls calling, and `Delegate` lets the client send capabilities with its calls