Phase 2d (i): a kernel wall-clock from the CMOS RTC
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.
This commit is contained in:
@@ -51,6 +51,14 @@ pub fn clock() u64 {
|
|||||||
return @intCast(sc.systemCall0(.clock));
|
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
|
/// 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
|
/// 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).
|
/// `offset`, into `out`. Returns the number of bytes copied (0 at end of buffer).
|
||||||
|
|||||||
@@ -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)
|
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
|
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)
|
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`.
|
||||||
_,
|
_,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -469,6 +469,95 @@ pub fn clockHz() u64 {
|
|||||||
return apic.tscHz();
|
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
|
/// Whether the CPU guarantees an **invariant** TSC (CPUID 0x80000007 EDX[8] on
|
||||||
/// x86; the analogous architectural guarantee elsewhere). When false the TSC is not
|
/// x86; the analogous architectural guarantee elsewhere). When false the TSC is not
|
||||||
/// used as the clocksource.
|
/// used as the clocksource.
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const parameters = @import("parameters");
|
|||||||
const architecture = @import("architecture");
|
const architecture = @import("architecture");
|
||||||
const console = @import("console.zig");
|
const console = @import("console.zig");
|
||||||
const log = @import("log.zig");
|
const log = @import("log.zig");
|
||||||
|
const wall_clock = @import("wall-clock.zig");
|
||||||
const pmm = @import("pmm.zig");
|
const pmm = @import("pmm.zig");
|
||||||
const heap = @import("heap.zig");
|
const heap = @import("heap.zig");
|
||||||
const scheduler = @import("scheduler.zig");
|
const scheduler = @import("scheduler.zig");
|
||||||
@@ -285,6 +286,10 @@ fn kmain(boot_information: *const BootInformation) noreturn {
|
|||||||
if (!architecture.clockSynchronized())
|
if (!architecture.clockSynchronized())
|
||||||
log.write("/system/kernel: WARNING: per-core TSCs are not synchronized; monotonic clock moved off the TSC\n");
|
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=<name>`), run that case and stop.
|
// In a test build (`zig build -Dtest-case=<name>`), run that case and stop.
|
||||||
// Normal builds fall through to the idle halt.
|
// Normal builds fall through to the idle halt.
|
||||||
if (build_options.test_case) |case| {
|
if (build_options.test_case) |case| {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ const devices_broker = @import("devices-broker.zig");
|
|||||||
const irq = @import("irq.zig");
|
const irq = @import("irq.zig");
|
||||||
const initial_ramdisk = @import("initial-ramdisk");
|
const initial_ramdisk = @import("initial-ramdisk");
|
||||||
const log = @import("log.zig");
|
const log = @import("log.zig");
|
||||||
|
const wall_clock = @import("wall-clock.zig");
|
||||||
|
|
||||||
const page_size = abi.page_size;
|
const page_size = abi.page_size;
|
||||||
const SystemCall = abi.SystemCall;
|
const SystemCall = abi.SystemCall;
|
||||||
@@ -207,6 +208,7 @@ fn system_call(state: *architecture.CpuState) void {
|
|||||||
.process_signal => systemProcessSignal(state),
|
.process_signal => systemProcessSignal(state),
|
||||||
.timer_bind => systemTimerBind(state),
|
.timer_bind => systemTimerBind(state),
|
||||||
.klog_read => systemKlogRead(state),
|
.klog_read => systemKlogRead(state),
|
||||||
|
.wall_clock => systemWallClock(state),
|
||||||
_ => fail(state),
|
_ => fail(state),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1336,3 +1338,10 @@ pub fn spawnProcessSupervised(image: []const u8, priority: u3, argv: []const []c
|
|||||||
fn systemClock(state: *architecture.CpuState) void {
|
fn systemClock(state: *architecture.CpuState) void {
|
||||||
architecture.setSystemCallResult(state, architecture.nanos());
|
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());
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const boot_handoff = @import("boot-handoff");
|
|||||||
const abi = @import("abi");
|
const abi = @import("abi");
|
||||||
const device_abi = @import("device-abi");
|
const device_abi = @import("device-abi");
|
||||||
const architecture = @import("architecture");
|
const architecture = @import("architecture");
|
||||||
|
const wall_clock = @import("wall-clock.zig");
|
||||||
const devices_broker = @import("devices-broker.zig");
|
const devices_broker = @import("devices-broker.zig");
|
||||||
const platform = @import("platform");
|
const platform = @import("platform");
|
||||||
const pmm = @import("pmm.zig");
|
const pmm = @import("pmm.zig");
|
||||||
@@ -66,6 +67,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
|
|||||||
timer();
|
timer();
|
||||||
} else if (eql(case, "clock")) {
|
} else if (eql(case, "clock")) {
|
||||||
clock();
|
clock();
|
||||||
|
} else if (eql(case, "wall-clock")) {
|
||||||
|
wallClock();
|
||||||
} else if (eql(case, "vmm")) {
|
} else if (eql(case, "vmm")) {
|
||||||
vmm();
|
vmm();
|
||||||
} else if (eql(case, "heap")) {
|
} else if (eql(case, "heap")) {
|
||||||
@@ -458,6 +461,17 @@ fn heapTest() void {
|
|||||||
/// Verify the calibrated clocks: sane measured frequencies, monotonic uptime that
|
/// Verify the calibrated clocks: sane measured frequencies, monotonic uptime that
|
||||||
/// advances with real ticks, and — the point of the TSC clock — nanosecond
|
/// 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.
|
/// 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 {
|
fn clock() void {
|
||||||
log("DANOS-TEST-BEGIN: clock\n", .{});
|
log("DANOS-TEST-BEGIN: clock\n", .{});
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -108,6 +108,11 @@ CASES = [
|
|||||||
{"name": "clock",
|
{"name": "clock",
|
||||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
"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",
|
{"name": "vmm",
|
||||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
|
|||||||
Reference in New Issue
Block a user