RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

authorEdward <edward@localhost>2026-10-31 17:22:26 -0700
commita91bf2c1317b4b7dc0c293615e379acf56ae0183
parentfc5e78aac8e1fcad62e645581d205f6b5b391b7a
treebrowse files at this commit
Stage 18 prep: fixes from Clarke's 0.17.0 review

- timer_getoverrun / si_overrun: while a POSIX timer's signal is still
  pending, further expirations add to the overrun count (they used to
  overwrite it). The count is latched when the signal is delivered:
  getoverrun reports the last delivered signal's overrun (0 before the
  first delivery), and counting starts over for the next one.
- timerd starts at boot, so the first timer no longer costs 16 frames
  after the headless report's "before" snapshot (report is flat again).
- coretest statfs: stat/lstat/fstat of mount points (/tmp, /dev,
  /mnt/disk1) report the mounted fs's st_dev (and ext2's root ino 2).
  These already passed: the probe's "/tmp has /'s st_dev" came from
  printing st.st_dev in the same printf call as a later stat("/", &st)
  argument (C evaluates arguments in unspecified order).
Msrc/main.rs+4 -0
Msrc/signal.rs+14 -3
Msrc/timer.rs+67 -19
Muser/coretest.rs+38 -0

4 files changed, 123 insertions, 22 deletions

@@ -109,6 +109,10 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! {         kprintln!("[net] no virtio-net card (start QEMU with -device virtio-net-pci)");     } +    // --- Timers (timer.rs): start timerd now, so its one-off cost (a+    // kernel stack: 16 frames) isn't counted as the shell's (stage 18) --+    rustbelt::timer::start();+     // --- The shell (user/sh.rs), run by its own kernel thread ---------------     thread::spawn("shell", shell_supervisor); 
@@ -566,7 +566,8 @@ pub fn deliver_outcome(frame: &mut TrapFrame) -> Option<bool> {             let (sig, info) = ts.pending.take(allowed).or_else(|| s.pending.take(allowed))?;             Some((sig, info, s.action(sig), ts.reason.take()))         });-        let Some((sig, info, action, reason)) = next else { break };+        let Some((sig, mut info, action, reason)) = next else { break };+        crate::timer::delivered(p.pid, &mut info); // a POSIX timer's: latch its overrun count         match action.handler {             SIG_IGN if sig != SIGKILL && sig != SIGSTOP => continue,             SIG_IGN | SIG_DFL => match default_action(sig) {@@ -1090,11 +1091,21 @@ pub fn pending_blocked(t: &Task) -> u64 { /// rt_sigtimedwait's dequeue: take a pending signal of `set` (the /// thread's own first, then the process's), without running any handler. pub fn take_waited(t: &Task, set: u64) -> Option<(u32, SigInfo)> {-    without_interrupts(|| {+    let (sig, mut info) = without_interrupts(|| {         let mut s = t.process.signals.lock();         let mut ts = t.sig.lock();         ts.pending.take(set).or_else(|| s.pending.take(set))-    })+    })?;+    crate::timer::delivered(t.process.pid, &mut info);+    Some((sig, info))+}++/// Is POSIX timer `id`'s signal `sig` still pending in `p` (for the+/// process or one of its threads)? timer.rs counts overruns meanwhile.+pub fn timer_pending(p: &Process, sig: u32, id: i32) -> bool {+    let is_it = |pending: &Pending| pending.info.iter().any(|(s, i)| *s == sig && i.code == crate::timer::SI_TIMER && i.fields[0] as u32 == id as u32);+    let tasks = p.tasks();+    without_interrupts(|| is_it(&p.signals.lock().pending) || tasks.iter().any(|t| is_it(&t.sig.lock().pending))) }  /// Is the process's queue of pending signals full (sigqueue: EAGAIN)?
@@ -10,7 +10,8 @@ //! sleep. Why a thread and not the timer interrupt? Sending a signal takes //! locks and may wake threads; doing that from normal thread context keeps //! the interrupt handler tiny and follows the locking rules in thread.rs.-//! timerd is started the first time anyone arms a timer.+//! timerd is started at boot (stage 18; it used to start the first time+//! anyone armed a timer). //! //! Resolution: timerd sleeps in PIT ticks (~55 ms), so a timer fires up //! to ~110 ms late, never early. A periodic timer whose signals come late@@ -63,8 +64,12 @@ struct Timer {     /// SIGEV_THREAD_ID's thread.     tid: u64,     sigval: u64,-    /// Expirations missed at the last firing (timer_getoverrun).+    /// Expirations counted since the signal was sent, while it is still+    /// pending (stage 18 fix: this *adds up*; it used to be overwritten).     overrun: u64,+    /// What timer_getoverrun reports: `overrun` as it was when the last+    /// signal was delivered (0 before the first delivery), like Linux.+    overrun_last: u64, }  static TIMERS: Mutex<Vec<Timer>> = Mutex::new(Vec::new());@@ -77,11 +82,19 @@ fn with_table<R>(f: impl FnOnce(&mut Vec<Timer>) -> R) -> R {     without_interrupts(|| f(&mut TIMERS.lock())) } -/// Tell timerd to look again (starting it the first time).-fn poke() {+/// Start timerd. The kernel does this at boot (stage 18: before, the+/// first timer started it, so its stack showed up in the frame count of+/// whatever program armed a timer first). Calling it again does nothing.+pub fn start() {     if !STARTED.swap(true, Ordering::AcqRel) {         crate::thread::spawn("timerd", timerd);     }+}++/// Tell timerd to look again (starting it, if nobody did yet: the test+/// kernels in tests/ don't run the boot code).+fn poke() {+    start();     CHANGED.store(true, Ordering::Release);     CHANGED_QUEUE.wake_all(); }@@ -103,20 +116,33 @@ fn timerd() {                 if at > now {                     continue;                 }-                if timer.interval > 0 {-                    let missed = (now - at) / timer.interval;-                    timer.overrun = missed;-                    timer.next = Some(at + (missed + 1) * timer.interval);-                } else {-                    timer.overrun = 0;-                    timer.next = None;-                }-                due.push(*timer);+                // Periods that went by since `at` without a wake-up of+                // ours (timerd sleeps in ~55 ms ticks): expirations too.+                let missed = if timer.interval > 0 { (now - at) / timer.interval } else { 0 };+                timer.next = (timer.interval > 0).then(|| at + (missed + 1) * timer.interval);+                due.push((*timer, missed));             }             t.iter().filter_map(|t| t.next).min()         });-        for timer in due {-            fire(&timer);+        for (timer, missed) in due {+            // A POSIX timer whose last signal hasn't been delivered yet+            // (blocked, say) doesn't send another: like Linux, it counts+            // the expirations as overruns, which the delivery reports.+            // (Checked outside the table lock: signal locks come first.)+            let posix = timer.id != ITIMER_ID && timer.notify != SIGEV_NONE;+            let still_pending = posix && process::get(timer.pid).is_some_and(|p| signal::timer_pending(&p, timer.signo, timer.id));+            let send = with_table(|t| {+                let Some(x) = t.iter_mut().find(|x| x.pid == timer.pid && x.id == timer.id) else { return false };+                if still_pending {+                    x.overrun = x.overrun.saturating_add(1 + missed);+                } else {+                    x.overrun = missed;+                }+                !still_pending+            });+            if send {+                fire(&timer);+            }         }         let changed = || CHANGED.swap(false, Ordering::AcqRel).then_some(());         match next {@@ -139,7 +165,7 @@ fn fire(timer: &Timer) {         SigInfo::from_pid(timer.signo, 0) // SI_KERNEL, like Linux's itimers     } else {         let mut info = SigInfo::new(timer.signo, SI_TIMER);-        info.fields[0] = timer.id as u32 as u64 | (timer.overrun.min(i32::MAX as u64)) << 32; // si_tid, si_overrun+        info.fields[0] = timer.id as u32 as u64; // si_tid; si_overrun is filled in at delivery (`delivered`)         info.fields[1] = timer.sigval; // si_value         info     };@@ -183,6 +209,7 @@ pub fn set_itimer(pid: u64, value: u64, interval: u64) -> (u64, u64) {                 tid: 0,                 sigval: 0,                 overrun: 0,+                overrun_last: 0,             });         }         old.map_or((0, 0), |x| read(&x, now))@@ -214,7 +241,7 @@ pub fn create(pid: u64, clock: u64, ev: SigEvent) -> Result<i32, Errno> {         let id = (0..).find(|i| !ids.contains(i)).unwrap();         // No sigevent: SIGALRM with the timer id as the value (Linux's default).         let sigval = ev.sigval.unwrap_or(id as u64);-        t.push(Timer { pid, id, clock, next: None, interval: 0, signo: ev.signo, notify: ev.notify, tid: ev.tid, sigval, overrun: 0 });+        t.push(Timer { pid, id, clock, next: None, interval: 0, signo: ev.signo, notify: ev.notify, tid: ev.tid, sigval, overrun: 0, overrun_last: 0 });         Ok(id)     }) }@@ -227,6 +254,7 @@ pub fn settime(pid: u64, id: i32, absolute: bool, value: u64, interval: u64) ->          let timer = t.iter_mut().find(|x| x.pid == pid && x.id == id && id != ITIMER_ID).ok_or(Errno::EINVAL)?;         let old = read(timer, now);         timer.overrun = 0;+        timer.overrun_last = 0;         timer.interval = interval;         timer.next = match (value, absolute) {             (0, _) => None,@@ -248,9 +276,29 @@ pub fn gettime(pid: u64, id: i32) -> Result<(u64, u64), Errno> {     with_table(|t| t.iter().find(|x| x.pid == pid && x.id == id && id != ITIMER_ID).map(|x| read(x, now)).ok_or(Errno::EINVAL)) } -/// timer_getoverrun: periods missed at the last expiry.+/// timer_getoverrun: the overrun count of the last *delivered* signal. pub fn getoverrun(pid: u64, id: i32) -> Result<u64, Errno> {-    with_table(|t| t.iter().find(|x| x.pid == pid && x.id == id && id != ITIMER_ID).map(|x| x.overrun.min(i32::MAX as u64)).ok_or(Errno::EINVAL))+    with_table(|t| t.iter().find(|x| x.pid == pid && x.id == id && id != ITIMER_ID).map(|x| x.overrun_last.min(i32::MAX as u64)).ok_or(Errno::EINVAL))+}++/// A signal was just taken off a pending set to be delivered (or picked up+/// by sigwaitinfo). If it is a POSIX timer's, latch that timer's overrun+/// count: it goes into si_overrun and becomes what timer_getoverrun says,+/// and counting starts again from 0. Call without signal locks held.+pub fn delivered(pid: u64, info: &mut SigInfo) {+    if info.code != SI_TIMER {+        return;+    }+    let id = info.fields[0] as u32 as i32;+    let overrun = with_table(|t| {+        let x = t.iter_mut().find(|x| x.pid == pid && x.id == id && id != ITIMER_ID)?;+        x.overrun_last = x.overrun;+        x.overrun = 0;+        Some(x.overrun_last.min(i32::MAX as u64))+    });+    if let Some(n) = overrun {+        info.fields[0] = id as u32 as u64 | n << 32; // si_tid, si_overrun+    } }  /// timer_delete.
@@ -361,6 +361,20 @@ fn statfs(c: &mut Checks) {     close(w);     // st_dev tells file systems apart.     c.check(st("/tmp").dev != st("/mnt/disk1").dev && st("/mnt/disk1/").dev == st("/mnt/disk1/lost+found").dev, "st_dev: one per file system");+    // Stage 18 fix check (Clarke): a mount point itself reports the+    // *mounted* file system's st_dev and root inode, not the directory it+    // covers. find -xdev, du -x and cp -x compare st_dev with this.+    write_file("/tmp/devchk", b"x");+    c.check(st("/tmp").dev == st("/tmp/devchk").dev && st("/tmp").dev != st("/").dev, "stat(\"/tmp\"): the mounted ramfs's st_dev, not /'s");+    c.check(st("/dev").dev == st("/dev/null").dev && st("/dev").dev != st("/").dev, "stat(\"/dev\"): devfs's st_dev");+    c.check(st("/mnt/disk1").dev == st("/mnt/disk1/lost+found").dev && st("/mnt/disk1").ino == 2, "stat(\"/mnt/disk1\"): ext2's st_dev and its root inode (2)");+    let mut ls = Stat::default();+    c.check(lstat("/tmp", &mut ls) == 0 && ls.dev == st("/tmp/devchk").dev, "lstat of a mount point: the mounted fs's st_dev too");+    let fd = open("/mnt/disk1", O_RDONLY | O_DIRECTORY);+    let mut fs_ = Stat::default();+    c.check(fstat(fd, &mut fs_) == 0 && fs_.dev == st("/mnt/disk1/lost+found").dev && fs_.ino == 2, "fstat of an fd opened on a mount point: same");+    close(fd);+    unlink("/tmp/devchk");     // statx     write_file("/tmp/sx.txt", b"twelve bytes");     let mut sx = [0u8; 256];@@ -535,6 +549,7 @@ fn sockets(c: &mut Checks) { static ALRM: AtomicI64 = AtomicI64::new(0); static TCODE: AtomicI64 = AtomicI64::new(0); static TVAL: AtomicU64 = AtomicU64::new(0);+static TOVR: AtomicI64 = AtomicI64::new(-1);  extern "C" fn on_alarm(_sig: i32) {     ALRM.fetch_add(1, Ordering::SeqCst);@@ -544,6 +559,7 @@ extern "C" fn on_timer(_sig: i32, info: *const SigInfo, _ctx: *mut u8) {     unsafe {         TCODE.store((*info).code as i64, Ordering::SeqCst);         TVAL.store(*((info as *const u8).add(24) as *const u64), Ordering::SeqCst);+        TOVR.store(*((info as *const u8).add(20) as *const i32) as i64, Ordering::SeqCst); // si_overrun     }     ALRM.fetch_add(1, Ordering::SeqCst); }@@ -638,6 +654,28 @@ fn timers(c: &mut Checks) {     c.check(old[1] == 50_000_000, "disarming returns the old interval");     c.check(syscall3(SYS_TIMER_DELETE, id as u64, 0, 0) == 0, "timer_delete");     c.check(syscall3(SYS_TIMER_DELETE, id as u64, 0, 0) == -EINVAL, "timer_delete again: EINVAL");+    // Overruns (stage 18 fix, found by Clarke): a 20 ms timer whose signal+    // is blocked for 0.5 s expires ~25 times but sends one signal; the+    // other ~24 add up as overruns. timer_getoverrun reports the count of+    // the last *delivered* signal: 0 while it is still pending.+    ALRM.store(0, Ordering::SeqCst);+    TOVR.store(-1, Ordering::SeqCst);+    let r = syscall3(SYS_TIMER_CREATE, 1, sev.as_ptr() as u64, &mut id as *mut i32 as u64);+    let old_mask = sigprocmask(SIG_BLOCK, sigbit(SIGUSR1)) as u64;+    let spec = [0u64, 20_000_000, 0, 20_000_000]; // every 20 ms+    syscall6(SYS_TIMER_SETTIME, id as u64, 0, spec.as_ptr() as u64, 0, 0, 0);+    sleep_through(500);+    c.check(r == 0 && syscall3(SYS_TIMER_GETOVERRUN, id as u64, 0, 0) == 0, "overrun: 0 before the first signal is delivered");+    sigprocmask(SIG_SETMASK, old_mask); // delivered now+    let first = TOVR.load(Ordering::SeqCst);+    let got = syscall3(SYS_TIMER_GETOVERRUN, id as u64, 0, 0);+    c.check(ALRM.load(Ordering::SeqCst) == 1 && (18..=30).contains(&first), &format!("blocked 0.5 s: one signal, si_overrun {} (Linux: ~24)", first));+    c.check(got == first, &format!("timer_getoverrun = the delivered signal's si_overrun ({})", got));+    c.check(wait_for(2, 1000), "unblocked: the next signal comes");+    let next = TOVR.load(Ordering::SeqCst);+    // (Not 0: timerd and signal delivery both work in ~55 ms ticks.)+    c.check((0..=10).contains(&next), &format!("...with a fresh count ({}), not the old one added", next));+    syscall3(SYS_TIMER_DELETE, id as u64, 0, 0);     // SIGEV_NONE: nothing is sent, the time just runs down.     sev[1] = 1u64 << 32; // notify = SIGEV_NONE     syscall3(SYS_TIMER_CREATE, 0, sev.as_ptr() as u64, &mut id as *mut i32 as u64);