From 8a385403123221de6e3f7ccad75d8d0fe8a21edb Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Mon, 13 Jul 2026 20:35:03 +0100 Subject: [PATCH] Phase 2d (i): a kernel wall-clock from the CMOS RTC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds real (calendar) time, the foundation for filesystem timestamps. Monotonic time (`clock`) says how long since boot; this says what time it actually is. - cpu.zig (x86_64): readRtcUnixSeconds() reads the CMOS real-time clock (ports 0x70/0x71) — waits out an update-in-progress, reads twice until stable, handles BCD-vs-binary and 12-vs-24-hour per status register B — and converts to Unix epoch seconds (UTC). - kernel/wall-clock.zig: reads the RTC once at boot and anchors it to the monotonic clock, so a query is a cheap arithmetic offset — no per-call CMOS poll, no lock, no SMP hazard on the shared ports. kmain calls init() once the monotonic clock is final and logs the epoch. - wall_clock() syscall (33) -> Unix epoch seconds, wrapped by runtime.system .wallClock(). Wall-clock *seconds* are mechanism the kernel owns like the monotonic clock; calendars/timezones are user-space policy (the stale comment on systemClock that called wall-clock a "user-space service" is updated in spirit by the new handler's doc). - A `wall-clock` kernel test asserts the boot RTC read is a plausible current epoch. Verified against the host: the guest read epoch 1783971244 while `date -u +%s` gave 1783971245 (one second of boot lag) — the CMOS read + epoch conversion are correct to the second. zig build, zig build test, and smoke/clock/init are green. --- library/runtime/system.zig | 8 ++ system/abi.zig | 1 + system/kernel/architecture/x86_64/cpu.zig | 89 +++++++++++++++++++++++ system/kernel/kernel.zig | 5 ++ system/kernel/process.zig | 9 +++ system/kernel/tests.zig | 14 ++++ system/kernel/wall-clock.zig | 26 +++++++ test/qemu_test.py | 5 ++ 8 files changed, 157 insertions(+) create mode 100644 system/kernel/wall-clock.zig diff --git a/library/runtime/system.zig b/library/runtime/system.zig index b3bbe54..52b9b2a 100644 --- a/library/runtime/system.zig +++ b/library/runtime/system.zig @@ -51,6 +51,14 @@ pub fn clock() u64 { return @intCast(sc.systemCall0(.clock)); } +/// Wall-clock time in Unix epoch seconds (UTC) — the real date/time, from the RTC. +/// Unlike `clock` (monotonic since boot), this tracks calendar time, so it is what a +/// filesystem stamps as a file's modification time. Formatting it into a calendar +/// date/timezone is user-space policy layered on top. +pub fn wallClock() u64 { + return @intCast(sc.systemCall0(.wall_clock)); +} + /// Copy bytes out of the kernel's in-memory diagnostic log — the accumulated /// stream of everything `write` (and the kernel itself) has emitted — starting at /// `offset`, into `out`. Returns the number of bytes copied (0 at end of buffer). diff --git a/system/abi.zig b/system/abi.zig index 5080cdd..6fbadb2 100644 --- a/system/abi.zig +++ b/system/abi.zig @@ -59,6 +59,7 @@ pub const SystemCall = enum(u64) { process_signal = 30, // process_signal(id, signal) -> 0/-errno: post a signal to a child (or to yourself) timer_bind = 31, // timer_bind(endpoint, ms) -> 0/-errno: one-shot timer — posts a notification when ms elapse klog_read = 32, // klog_read(offset, ptr, len) -> bytes copied: copy the kernel RAM log buffer out to a user buffer (for persisting the boot log to disk) + wall_clock = 33, // wall_clock() -> Unix epoch seconds (UTC): the RTC wall-clock time, for filesystem timestamps (mtime). Monotonic time is `clock`. _, }; diff --git a/system/kernel/architecture/x86_64/cpu.zig b/system/kernel/architecture/x86_64/cpu.zig index 238cccf..4fc9e22 100644 --- a/system/kernel/architecture/x86_64/cpu.zig +++ b/system/kernel/architecture/x86_64/cpu.zig @@ -469,6 +469,95 @@ pub fn clockHz() u64 { return apic.tscHz(); } +// --- real-time clock (CMOS) -------------------------------------------------- +// +// The battery-backed CMOS clock, read once at boot and thereafter anchored to the +// monotonic clock (see kernel/wall-clock.zig) — so this is never on a hot path and +// needs no lock. Wall-clock *seconds* are mechanism the kernel owns (the hardware's +// value), like the monotonic clock; calendars/timezones are policy layered on top. + +fn cmosRead(register: u8) u8 { + io.outb(0x70, register); + return io.inb(0x71); +} + +const RtcFields = struct { second: u8, minute: u8, hour: u8, day: u8, month: u8, year: u8 }; + +fn rtcRaw() RtcFields { + while (cmosRead(0x0A) & 0x80 != 0) {} // wait out any update in progress (status A bit 7) + return .{ + .second = cmosRead(0x00), + .minute = cmosRead(0x02), + .hour = cmosRead(0x04), + .day = cmosRead(0x07), + .month = cmosRead(0x08), + .year = cmosRead(0x09), + }; +} + +fn bcdToBinary(v: u8) u8 { + return (v & 0x0F) + ((v >> 4) * 10); +} + +fn isLeapYear(y: u32) bool { + return (y % 4 == 0 and y % 100 != 0) or (y % 400 == 0); +} + +/// Read the CMOS real-time clock and convert it to Unix epoch seconds (UTC). +pub fn readRtcUnixSeconds() u64 { + // Read until two consecutive reads agree, so we never latch a half-updated time. + var a = rtcRaw(); + while (true) { + const b = rtcRaw(); + if (a.second == b.second and a.minute == b.minute and a.hour == b.hour and + a.day == b.day and a.month == b.month and a.year == b.year) break; + a = b; + } + + const status_b = cmosRead(0x0B); + const binary_mode = status_b & 0x04 != 0; // else BCD + const hour_24 = status_b & 0x02 != 0; // else 12-hour with a PM bit + + var second = a.second; + var minute = a.minute; + var hour_field = a.hour; + var day = a.day; + var month = a.month; + var year = a.year; + if (!binary_mode) { + second = bcdToBinary(second); + minute = bcdToBinary(minute); + hour_field = bcdToBinary(hour_field & 0x7F) | (hour_field & 0x80); // preserve the PM bit + day = bcdToBinary(day); + month = bcdToBinary(month); + year = bcdToBinary(year); + } + + var hour: u32 = hour_field & 0x7F; + if (!hour_24) { + const pm = hour_field & 0x80 != 0; + hour %= 12; // 12 AM/PM -> 0 + if (pm) hour += 12; + } + + // The CMOS year is 0..99; QEMU and modern hardware mean 20xx (there is no + // reliable century register on QEMU). Treat < 70 as 20xx, else 19xx. + const full_year: u32 = if (year < 70) 2000 + @as(u32, year) else 1900 + @as(u32, year); + + var days: u64 = 0; + var y: u32 = 1970; + while (y < full_year) : (y += 1) days += if (isLeapYear(y)) 366 else 365; + const month_lengths = [_]u8{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + var m: u8 = 1; + while (m < month) : (m += 1) { + days += month_lengths[m - 1]; + if (m == 2 and isLeapYear(full_year)) days += 1; + } + days += @as(u64, day) - 1; + + return ((days * 24 + hour) * 60 + minute) * 60 + second; +} + /// Whether the CPU guarantees an **invariant** TSC (CPUID 0x80000007 EDX[8] on /// x86; the analogous architectural guarantee elsewhere). When false the TSC is not /// used as the clocksource. diff --git a/system/kernel/kernel.zig b/system/kernel/kernel.zig index 141892e..e4c5fc2 100644 --- a/system/kernel/kernel.zig +++ b/system/kernel/kernel.zig @@ -5,6 +5,7 @@ const parameters = @import("parameters"); const architecture = @import("architecture"); const console = @import("console.zig"); const log = @import("log.zig"); +const wall_clock = @import("wall-clock.zig"); const pmm = @import("pmm.zig"); const heap = @import("heap.zig"); const scheduler = @import("scheduler.zig"); @@ -285,6 +286,10 @@ fn kmain(boot_information: *const BootInformation) noreturn { if (!architecture.clockSynchronized()) log.write("/system/kernel: WARNING: per-core TSCs are not synchronized; monotonic clock moved off the TSC\n"); + // Anchor wall-clock time: read the RTC once, now the monotonic clock is final. + wall_clock.init(); + log.print("/system/kernel: wall clock {d} (Unix epoch seconds, UTC, from the RTC)\n", .{wall_clock.nowSeconds()}); + // In a test build (`zig build -Dtest-case=`), run that case and stop. // Normal builds fall through to the idle halt. if (build_options.test_case) |case| { diff --git a/system/kernel/process.zig b/system/kernel/process.zig index 5904fbb..bdcee36 100644 --- a/system/kernel/process.zig +++ b/system/kernel/process.zig @@ -34,6 +34,7 @@ const devices_broker = @import("devices-broker.zig"); const irq = @import("irq.zig"); const initial_ramdisk = @import("initial-ramdisk"); const log = @import("log.zig"); +const wall_clock = @import("wall-clock.zig"); const page_size = abi.page_size; const SystemCall = abi.SystemCall; @@ -207,6 +208,7 @@ fn system_call(state: *architecture.CpuState) void { .process_signal => systemProcessSignal(state), .timer_bind => systemTimerBind(state), .klog_read => systemKlogRead(state), + .wall_clock => systemWallClock(state), _ => fail(state), } } @@ -1336,3 +1338,10 @@ pub fn spawnProcessSupervised(image: []const u8, priority: u3, argv: []const []c fn systemClock(state: *architecture.CpuState) void { architecture.setSystemCallResult(state, architecture.nanos()); } + +/// wall_clock() -> Unix epoch seconds (UTC). The RTC value, read at boot and offset +/// by the monotonic clock (wall-clock.zig) — mechanism, not policy: calendars and +/// timezones layer on top in user space. Needed for filesystem timestamps (mtime). +fn systemWallClock(state: *architecture.CpuState) void { + architecture.setSystemCallResult(state, wall_clock.nowSeconds()); +} diff --git a/system/kernel/tests.zig b/system/kernel/tests.zig index 01ba467..ea353c2 100644 --- a/system/kernel/tests.zig +++ b/system/kernel/tests.zig @@ -14,6 +14,7 @@ const boot_handoff = @import("boot-handoff"); const abi = @import("abi"); const device_abi = @import("device-abi"); const architecture = @import("architecture"); +const wall_clock = @import("wall-clock.zig"); const devices_broker = @import("devices-broker.zig"); const platform = @import("platform"); const pmm = @import("pmm.zig"); @@ -66,6 +67,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void { timer(); } else if (eql(case, "clock")) { clock(); + } else if (eql(case, "wall-clock")) { + wallClock(); } else if (eql(case, "vmm")) { vmm(); } else if (eql(case, "heap")) { @@ -458,6 +461,17 @@ fn heapTest() void { /// Verify the calibrated clocks: sane measured frequencies, monotonic uptime that /// advances with real ticks, and — the point of the TSC clock — nanosecond /// resolution far finer than the 1 ms tick, with the unit functions consistent. +fn wallClock() void { + log("DANOS-TEST-BEGIN: wall-clock\n", .{}); + // The RTC was read and anchored at boot (kmain -> wall_clock.init()). + const seconds = wall_clock.nowSeconds(); + log(" epoch: {d}\n", .{seconds}); + // A plausible current wall-clock: after 2020-01-01 (1577836800) and before 2050 + // (2524608000) — catches a broken CMOS read or a wrong epoch conversion. + check("wall clock reads a plausible current epoch", seconds > 1_577_836_800 and seconds < 2_524_608_000); + result(); +} + fn clock() void { log("DANOS-TEST-BEGIN: clock\n", .{}); diff --git a/system/kernel/wall-clock.zig b/system/kernel/wall-clock.zig new file mode 100644 index 0000000..3d1c46f --- /dev/null +++ b/system/kernel/wall-clock.zig @@ -0,0 +1,26 @@ +//! Wall-clock time: the CMOS real-time clock read once at boot and anchored to the +//! monotonic clock, so a query is a cheap arithmetic offset — no per-call CMOS poll, +//! no lock, no SMP hazard on the shared 0x70/0x71 ports. +//! +//! Wall-clock *seconds* are mechanism the kernel owns, exactly like the monotonic +//! clock ([[time-architecture]]): reading the hardware's value is not policy. +//! Calendars, timezones, and formatting layer on top in user space. It exists so the +//! filesystem can stamp real timestamps (mtime) — see docs/zig-self-hosting.md. + +const architecture = @import("architecture"); + +var boot_unix_seconds: u64 = 0; +var boot_nanos: u64 = 0; + +/// Read the RTC once and anchor it to the monotonic clock. Call at boot, after the +/// monotonic clock is calibrated. +pub fn init() void { + boot_unix_seconds = architecture.readRtcUnixSeconds(); + boot_nanos = architecture.nanos(); +} + +/// The current wall-clock time in Unix epoch seconds (UTC): the boot RTC value plus +/// the monotonic time elapsed since. Zero until `init` runs. +pub fn nowSeconds() u64 { + return boot_unix_seconds + (architecture.nanos() -% boot_nanos) / 1_000_000_000; +} diff --git a/test/qemu_test.py b/test/qemu_test.py index 3d482ff..20cdc89 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -108,6 +108,11 @@ CASES = [ {"name": "clock", "expect": r"DANOS-TEST-RESULT: PASS", "fail": r"DANOS-TEST-RESULT: FAIL"}, + # Wall-clock: the CMOS RTC read at boot gives a plausible current epoch (the + # foundation for filesystem mtime). + {"name": "wall-clock", + "expect": r"DANOS-TEST-RESULT: PASS", + "fail": r"DANOS-TEST-RESULT: FAIL"}, {"name": "vmm", "expect": r"DANOS-TEST-RESULT: PASS", "fail": r"DANOS-TEST-RESULT: FAIL"},