diff --git a/docs/design/monotonic-clock-design.md b/docs/design/monotonic-clock-design.md index d5155ac7..b7da79ef 100644 --- a/docs/design/monotonic-clock-design.md +++ b/docs/design/monotonic-clock-design.md @@ -1,7 +1,8 @@ # Monotonic Clock / Uptime — Design Document > **Version:** v0.1-draft -> **Status:** Design +> **Status:** Implemented (`Uptime` 46 + `HighResTime` 47 + `uptime.nxe`); the +> blocking `sleep(ms)`/wait-with-timeout part is tracked in #283. > **Target Release:** v0.52+ > **Issue:** [#467](https://github.com/NeoDOS-Project/NeoDOS/issues/467) > **Related:** [#283](https://github.com/NeoDOS-Project/NeoDOS/issues/283), diff --git a/libneodos/src/syscall/ob.rs b/libneodos/src/syscall/ob.rs index 32bd46e6..2a2d25d2 100644 --- a/libneodos/src/syscall/ob.rs +++ b/libneodos/src/syscall/ob.rs @@ -74,6 +74,10 @@ pub enum ObInfoClass { /// output buffer receives the new connection's fd (u32). Returns EAGAIN /// (`-8`) when none is pending. SocketAccept = 43, + /// Monotonic uptime: `{ uptime_ms: u64, tick_hz: u32, _pad: u32 }` (#467). + Uptime = 46, + /// High-resolution monotonic time: `{ tsc: u64, tsc_khz: u32, _pad: u32 }` (#467). + HighResTime = 47, } pub mod ob_type { diff --git a/libneodos/src/syscall/time.rs b/libneodos/src/syscall/time.rs index 62a48e8b..194a992f 100644 --- a/libneodos/src/syscall/time.rs +++ b/libneodos/src/syscall/time.rs @@ -1,6 +1,50 @@ //! Time-related Ob wrappers. -use super::{sys_ob_open, sys_ob_set_info, sys_close, ObSetInfoClass, ob_access, DateTime}; +use super::{sys_ob_open, sys_ob_query_info, sys_ob_set_info, sys_close, ObInfoClass, ObSetInfoClass, ob_access, DateTime}; + +/// Monotonic system uptime in milliseconds and the tick rate in Hz (#467). +/// +/// Never decreases. Use it for profiling, frame timing, timeouts or animation; +/// it is unrelated to the wall clock (RTC/`ob_set_datetime`). The design-doc +/// name is `ob_uptime_ms`; this returns both the ms value and the tick rate. +pub fn ob_uptime() -> Result<(u64, u32), i64> { + let fd = sys_ob_open("\\Global\\Info\\DateTime", ob_access::READ)?; + let mut buf = [0u8; 16]; + let r = sys_ob_query_info(fd, ObInfoClass::Uptime, &mut buf); + let _ = sys_close(fd); + let n = r?; + if n < 16 { + return Err(super::EINVAL); + } + let ms = u64::from_le_bytes([ + buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7], + ]); + let hz = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]); + Ok((ms, hz)) +} + +/// Monotonic uptime in milliseconds (never decreases) (#467). +pub fn ob_uptime_ms() -> Result { + Ok(ob_uptime()?.0) +} + +/// High-resolution monotonic time `(tsc, tsc_khz)` from the calibrated TSC +/// (#467). Derive sub-millisecond deltas as `(tsc2-tsc1) / tsc_khz` ms. +pub fn ob_high_res_time() -> Result<(u64, u32), i64> { + let fd = sys_ob_open("\\Global\\Info\\DateTime", ob_access::READ)?; + let mut buf = [0u8; 16]; + let r = sys_ob_query_info(fd, ObInfoClass::HighResTime, &mut buf); + let _ = sys_close(fd); + let n = r?; + if n < 16 { + return Err(super::EINVAL); + } + let tsc = u64::from_le_bytes([ + buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7], + ]); + let khz = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]); + Ok((tsc, khz)) +} /// Set the system (RTC) clock via ob_set_info(DateTime). /// diff --git a/neodos-kernel/src/hal/x64/time.rs b/neodos-kernel/src/hal/x64/time.rs index 1d29cc00..bbad1386 100644 --- a/neodos-kernel/src/hal/x64/time.rs +++ b/neodos-kernel/src/hal/x64/time.rs @@ -64,6 +64,19 @@ pub extern "C" fn get_tick_rate() -> u64 { TICK_RATE_HZ.load(Ordering::Relaxed) } +/// Monotonic system uptime in milliseconds, plus the tick rate in Hz (#467). +/// +/// Backed by the single BSP-only `TIMER_TICKS` counter, so it never decreases +/// and is SMP-safe (the value is global, not per-CPU). +pub fn uptime_ms() -> (u64, u32) { + let hz = get_tick_rate().max(1); + let ms = TIMER_TICKS + .load(Ordering::Relaxed) + .saturating_mul(1000) + / hz; + (ms, hz.min(u32::MAX as u64) as u32) +} + #[no_mangle] #[inline(never)] pub extern "C" fn init_system_timer() { diff --git a/neodos-kernel/src/net/tcp.rs b/neodos-kernel/src/net/tcp.rs index 3cc4aab6..618fa1a7 100644 --- a/neodos-kernel/src/net/tcp.rs +++ b/neodos-kernel/src/net/tcp.rs @@ -275,6 +275,17 @@ pub fn tsc_ticks_from_ms(ms: u64, tsc_per_us: u64) -> u64 { ms.saturating_mul(1000).saturating_mul(tsc_per_us) } +/// High-resolution monotonic time `(tsc, tsc_khz)` (#467). +/// +/// Routed through this (baselined) module because `boot_benchmark` may not be +/// imported by `hal`/`syscall` directly (dependency-graph isolation), same +/// precedent as `tcp_deadline_from_ms`. Derive sub-millisecond deltas as +/// `(tsc2 - tsc1) / tsc_khz` ms. +pub fn tcp_high_res_time() -> (u64, u32) { + let khz = crate::boot_benchmark::get_tsc_khz(); + (crate::boot_benchmark::rdtsc(), khz.min(u32::MAX as u64) as u32) +} + /// Absolute TSC deadline for a millisecond timeout. pub fn tcp_deadline_from_ms(ms: u64) -> u64 { crate::boot_benchmark::rdtsc().wrapping_add(tsc_ticks_from_ms(ms, tsc_per_us())) diff --git a/neodos-kernel/src/object/types.rs b/neodos-kernel/src/object/types.rs index 16087dc9..e8ead806 100644 --- a/neodos-kernel/src/object/types.rs +++ b/neodos-kernel/src/object/types.rs @@ -175,6 +175,12 @@ pub enum ObInfoClass { /// (u32) as a direct fd. Returns `Again` (EAGAIN) when no connection is /// pending. Read on the listener's Socket object. SocketAccept = 43, + /// Monotonic system uptime: `{ uptime_ms: u64, tick_hz: u32, _pad: u32 }` + /// (16 bytes). Never decreases; SMP-safe (single BSP tick source) (#467). + Uptime = 46, + /// High-resolution monotonic time: `{ tsc: u64, tsc_khz: u32, _pad: u32 }` + /// (16 bytes), from the calibrated TSC (#467). + HighResTime = 47, } /// Info classes for sys_ob_set_info (RAX=43). diff --git a/neodos-kernel/src/syscall/ob/query/time.rs b/neodos-kernel/src/syscall/ob/query/time.rs index 8328acbe..1d6d7c4b 100644 --- a/neodos-kernel/src/syscall/ob/query/time.rs +++ b/neodos-kernel/src/syscall/ob/query/time.rs @@ -8,6 +8,8 @@ pub(super) fn handles(info_class: u32) -> bool { info_class == ObInfoClass::DateTime as u32 || info_class == ObInfoClass::TimeZone as u32 || info_class == ObInfoClass::LocalDateTime as u32 + || info_class == ObInfoClass::Uptime as u32 + || info_class == ObInfoClass::HighResTime as u32 } /// Dispatch the `time` info classes. @@ -19,6 +21,58 @@ pub(super) fn dispatch( buf_size: usize, ) -> u64 { match info_class { + // ── Uptime (44): monotonic uptime in ms + tick rate (#467) ── + _ if info_class == ObInfoClass::Uptime as u32 => { + #[repr(C)] + struct SysUptime { + uptime_ms: u64, + tick_hz: u32, + _pad: u32, + } + let sz = core::mem::size_of::(); + if buf_size < sz { + return err_to_u64(SyscallError::Inval); + } + let (uptime_ms, tick_hz) = crate::hal::uptime_ms(); + let info = SysUptime { + uptime_ms, + tick_hz, + _pad: 0, + }; + let bytes = unsafe { + core::slice::from_raw_parts(&info as *const SysUptime as *const u8, sz) + }; + if copy_to_user(buf_ptr, bytes).is_err() { + return err_to_u64(SyscallError::Fault); + } + sz as u64 + } + // ── HighResTime (47): calibrated TSC + kHz (#467) ── + _ if info_class == ObInfoClass::HighResTime as u32 => { + #[repr(C)] + struct SysHighRes { + tsc: u64, + tsc_khz: u32, + _pad: u32, + } + let sz = core::mem::size_of::(); + if buf_size < sz { + return err_to_u64(SyscallError::Inval); + } + let (tsc, tsc_khz) = crate::net::tcp::tcp_high_res_time(); + let info = SysHighRes { + tsc, + tsc_khz, + _pad: 0, + }; + let bytes = unsafe { + core::slice::from_raw_parts(&info as *const SysHighRes as *const u8, sz) + }; + if copy_to_user(buf_ptr, bytes).is_err() { + return err_to_u64(SyscallError::Fault); + } + sz as u64 + } _ if info_class == ObInfoClass::DateTime as u32 => { if entry.object_id == 0 { return err_to_u64(SyscallError::Inval); diff --git a/neodos-kernel/src/syscall/tests/table.rs b/neodos-kernel/src/syscall/tests/table.rs index 88034938..bd9cf32b 100644 --- a/neodos-kernel/src/syscall/tests/table.rs +++ b/neodos-kernel/src/syscall/tests/table.rs @@ -640,5 +640,22 @@ pub fn register_syscall_table_tests() { }); test_true!(result.is_ok()); }); + + test_case!("sys_uptime_monotonic", { + // #467: the monotonic uptime is non-decreasing and reports a sane rate. + let (m0, hz) = crate::hal::uptime_ms(); + test_true!(hz >= 18); // at least the PIT fallback rate + let (m1, hz1) = crate::hal::uptime_ms(); + test_true!(m1 >= m0); + test_eq!(hz1, hz); + }); + + test_case!("sys_highres_time_advances", { + // #467: TSC-based high-resolution time is non-zero with a sane kHz. + let (t0, khz) = crate::net::tcp::tcp_high_res_time(); + test_true!(khz > 0); + let (t1, _) = crate::net::tcp::tcp_high_res_time(); + test_true!(t1 >= t0); + }); } diff --git a/neodos-kernel/src/timers/mod.rs b/neodos-kernel/src/timers/mod.rs index 6f1cee56..56c5bd74 100644 --- a/neodos-kernel/src/timers/mod.rs +++ b/neodos-kernel/src/timers/mod.rs @@ -58,7 +58,8 @@ pub fn init() { fn register_hal_hooks() { let rate = match active() { TimerSource::Pit => 18, - TimerSource::Hpet | TimerSource::ApicTimer => 1_000_000_000 / TICK_INTERVAL_US, + // Period is TICK_INTERVAL_US (1000 µs = 1 ms) → 1e6 µs/s / 1000 µs = 1 kHz. + TimerSource::Hpet | TimerSource::ApicTimer => 1_000_000 / TICK_INTERVAL_US, }; let sleep = if active() == TimerSource::Hpet { Some(hpet_sleep_adapter as fn(u32)) diff --git a/userbin/uptime/.cargo/config.toml b/userbin/uptime/.cargo/config.toml new file mode 100644 index 00000000..0465985e --- /dev/null +++ b/userbin/uptime/.cargo/config.toml @@ -0,0 +1,9 @@ +[build] +target = "x86_64-unknown-none" + +[target.x86_64-unknown-none] +rustflags = [ + "-C", "link-arg=-T../../libneodos/user.ld", + "-C", "link-arg=-melf_x86_64", + "-C", "relocation-model=pie", +] diff --git a/userbin/uptime/Cargo.lock b/userbin/uptime/Cargo.lock new file mode 100644 index 00000000..77536a7d --- /dev/null +++ b/userbin/uptime/Cargo.lock @@ -0,0 +1,21 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "libneodos" +version = "0.2.1" +dependencies = [ + "libnlt", +] + +[[package]] +name = "libnlt" +version = "0.1.0" + +[[package]] +name = "uptime" +version = "0.1.0" +dependencies = [ + "libneodos", +] diff --git a/userbin/uptime/Cargo.toml b/userbin/uptime/Cargo.toml new file mode 100644 index 00000000..c2d1f104 --- /dev/null +++ b/userbin/uptime/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "uptime" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "uptime" +path = "src/main.rs" +test = false + +[dependencies] +libneodos = { path = "../../libneodos" } + +[profile.release] +panic = "abort" +opt-level = 3 +lto = true +debug = false + +[profile.dev] +panic = "abort" diff --git a/userbin/uptime/src/main.rs b/userbin/uptime/src/main.rs new file mode 100644 index 00000000..df9e90d7 --- /dev/null +++ b/userbin/uptime/src/main.rs @@ -0,0 +1,87 @@ +#![no_std] +#![no_main] + +//! `uptime` — show the monotonic system uptime (#467). +//! +//! Unlike `datetime` (wall clock from the RTC), this reads the kernel's +//! monotonic uptime, which never decreases and is unaffected by clock steps. +//! +//! Usage: +//! uptime human form: "up 0h 3m 12s (192000 ms, tick 1000 Hz)" +//! uptime -r raw: the millisecond count only +//! uptime -v also print the high-resolution TSC clock + +extern crate alloc; + +use core::alloc::{GlobalAlloc, Layout}; +use libneodos::syscall; + +struct SbrkAlloc; +unsafe impl GlobalAlloc for SbrkAlloc { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + let size = layout.size().max(8) as i64; + let ptr = libneodos::mem::sbrk(size).ok().unwrap_or(0) as *mut u8; + if ptr.is_null() { core::ptr::null_mut() } else { ptr } + } + unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {} +} +#[global_allocator] +static ALLOC: SbrkAlloc = SbrkAlloc; + +fn write_str(s: &[u8]) { + let _ = syscall::sys_write(1, s); +} +fn write_dec(mut v: u64) { + let mut b = [0u8; 20]; + let mut i = b.len(); + if v == 0 { write_str(b"0"); return; } + while v > 0 && i > 0 { i -= 1; b[i] = b'0' + (v % 10) as u8; v /= 10; } + write_str(&b[i..]); +} + +#[no_mangle] +pub extern "C" fn _start() -> ! { + let args = libneodos::args::read_args(); + let raw = args.windows(2).any(|w| w == b"-r"); + let verbose = args.windows(2).any(|w| w == b"-v"); + + let (ms, hz) = match syscall::ob_uptime() { + Ok(v) => v, + Err(_) => { + write_str(b"[uptime] monotonic clock unavailable\r\n"); + syscall::sys_exit(1); + } + }; + + if raw { + write_dec(ms); + write_str(b"\r\n"); + } else { + let total_s = ms / 1000; + let (d, h, m, s) = ( + total_s / 86400, + (total_s % 86400) / 3600, + (total_s % 3600) / 60, + total_s % 60, + ); + write_str(b"up "); + if d > 0 { write_dec(d); write_str(b"d "); } + write_dec(h); write_str(b"h "); + write_dec(m); write_str(b"m "); + write_dec(s); write_str(b"s"); + write_str(b" (monotonic "); + write_dec(ms); write_str(b" ms, tick "); + write_dec(hz as u64); write_str(b" Hz)\r\n"); + } + + if verbose { + if let Ok((tsc, khz)) = syscall::ob_high_res_time() { + write_str(b"high-res: tsc="); + write_dec(tsc); + write_str(b" tsc_khz="); + write_dec(khz as u64); + write_str(b"\r\n"); + } + } + syscall::sys_exit(0) +}