diff --git a/config.mk b/config.mk index 57079ef5..15139ade 100644 --- a/config.mk +++ b/config.mk @@ -66,6 +66,9 @@ MAX_CPUS ?= 64 # Log level (0=debug, 1=info, 2=warn, 3=error, 4=fatal, 5=none) LOG_LEVEL ?= 0 +# Build epoch (Unix timestamp for RTC fallback on platforms without hardware RTC) +STLX_BUILD_EPOCH ?= $(shell date +%s) + # ============================================================================ # Platform Selection # ============================================================================ diff --git a/kernel/Makefile b/kernel/Makefile index 0959487a..0ff4bef9 100644 --- a/kernel/Makefile +++ b/kernel/Makefile @@ -130,6 +130,7 @@ CXXFLAGS_LOG := -DLOG_LEVEL=$(LOG_LEVEL) # Configuration defines (from config.mk) CXXFLAGS_CONFIG := -DMAX_CPUS=$(MAX_CPUS) +CXXFLAGS_CONFIG += -DSTLX_BUILD_EPOCH=$(STLX_BUILD_EPOCH) # Platform define (e.g., PLATFORM=rpi4 → -DSTLX_PLATFORM_RPI4) ifneq ($(PLATFORM),qemu-virt) diff --git a/kernel/arch/aarch64/clock/clock.cpp b/kernel/arch/aarch64/clock/clock.cpp index 19e0d12a..e6dae6fd 100644 --- a/kernel/arch/aarch64/clock/clock.cpp +++ b/kernel/arch/aarch64/clock/clock.cpp @@ -1,5 +1,6 @@ #include "clock/clock.h" #include "hwtimer/hwtimer_arch.h" +#include "hw/rtc.h" #include "common/logging.h" namespace clock { @@ -8,6 +9,7 @@ static uint64_t g_cnt_freq; static uint64_t g_mult; static uint32_t g_shift; static bool g_calibrated; +static uint64_t g_boot_realtime_ns; constexpr uint64_t NS_PER_SEC = 1000000000ULL; @@ -56,6 +58,7 @@ __PRIVILEGED_CODE int32_t init() { } compute_mult_shift(g_cnt_freq, &g_mult, &g_shift); + g_boot_realtime_ns = rtc::boot_unix_ns(); enable_el0_counter_access(); __atomic_store_n(&g_calibrated, true, __ATOMIC_RELEASE); @@ -90,4 +93,8 @@ uint64_t freq_hz() { return g_cnt_freq; } +uint64_t boot_realtime_ns() { + return g_boot_realtime_ns; +} + } // namespace clock diff --git a/kernel/arch/aarch64/hw/rtc.cpp b/kernel/arch/aarch64/hw/rtc.cpp new file mode 100644 index 00000000..5524e0c7 --- /dev/null +++ b/kernel/arch/aarch64/hw/rtc.cpp @@ -0,0 +1,64 @@ +#include "hw/rtc.h" +#include "common/logging.h" + +#if !defined(STLX_PLATFORM_RPI4) +#include "mm/vmm.h" +#include "mm/paging_types.h" +#include "hw/mmio.h" +#endif + +namespace rtc { + +static uint64_t g_boot_unix_ns; + +constexpr uint64_t NS_PER_SEC = 1000000000ULL; + +#if defined(STLX_PLATFORM_RPI4) + +__PRIVILEGED_CODE int32_t init() { +#ifdef STLX_BUILD_EPOCH + g_boot_unix_ns = static_cast(STLX_BUILD_EPOCH) * NS_PER_SEC; + log::warn("rtc: no hardware RTC, using build-time epoch (%lu)", + static_cast(STLX_BUILD_EPOCH)); +#else + g_boot_unix_ns = 0; + log::warn("rtc: no hardware RTC and no build epoch"); +#endif + return OK; +} + +#else // QEMU virt — PL031 RTC + +constexpr uintptr_t PL031_PHYS = 0x09010000; +constexpr size_t PL031_SIZE = 0x1000; +constexpr uintptr_t PL031_RTCDR_OFFSET = 0x00; + +__PRIVILEGED_CODE int32_t init() { + uintptr_t kva_base = 0; + uintptr_t rtc_va = 0; + + int32_t rc = vmm::map_device( + static_cast(PL031_PHYS), + PL031_SIZE, + paging::PAGE_KERNEL_RW, + kva_base, + rtc_va); + if (rc != vmm::OK) { + log::error("rtc: failed to map PL031 at 0x%lx", PL031_PHYS); + return ERR; + } + + uint32_t unix_sec = mmio::read32(rtc_va + PL031_RTCDR_OFFSET); + g_boot_unix_ns = static_cast(unix_sec) * NS_PER_SEC; + + log::info("rtc: PL031 epoch=%u sec", unix_sec); + return OK; +} + +#endif // STLX_PLATFORM_RPI4 + +uint64_t boot_unix_ns() { + return g_boot_unix_ns; +} + +} // namespace rtc diff --git a/kernel/arch/aarch64/sched/sched.cpp b/kernel/arch/aarch64/sched/sched.cpp index 8202e0f9..25df15e4 100644 --- a/kernel/arch/aarch64/sched/sched.cpp +++ b/kernel/arch/aarch64/sched/sched.cpp @@ -61,6 +61,9 @@ __PRIVILEGED_CODE static void save_cpu_context( for (int i = 0; i < 31; i++) { ctx->x[i] = tf->x[i]; } + // ctx->sp tracks the interrupted stack pointer for the return mode in ctx->pstate: + // - EL0t / EL1t: SP_EL0 + // - EL1h: SP_EL1 ctx->sp = tf->sp; ctx->pc = tf->elr; ctx->pstate = tf->spsr; @@ -80,6 +83,29 @@ __PRIVILEGED_CODE static void load_cpu_context( tf->spsr = ctx->pstate; } +/** + * @note Privilege: **required** + */ +__PRIVILEGED_CODE static void prepare_trap_return_stacks( + aarch64::trap_frame* tf, const task* next +) { + // trap_frame.sp is consumed as the return stack for the selected mode: + // - EL0t / EL1t -> SP_EL0 + // - EL1h -> SP_EL1 + // + // trap_frame.sp_el1 is consumed by vector epilogue when returning to + // EL0t/EL1t, establishing the selected task's EL1 exception stack for + // subsequent traps. + tf->sp_el1 = next->exec.system_stack_top; + +#ifdef DEBUG + uint64_t mode = tf->spsr & aarch64::SPSR_MODE_MASK; + if (mode != aarch64::SPSR_EL1H && tf->sp_el1 == 0) { + log::fatal("sched/aarch64: missing system_stack_top for non-EL1h return"); + } +#endif +} + /** * @note Privilege: **required** */ @@ -138,6 +164,7 @@ __PRIVILEGED_CODE void on_yield(aarch64::trap_frame* tf) { fpu::restore(&next->exec.fpu_ctx); load_cpu_context(&next->exec.cpu_ctx, tf); + prepare_trap_return_stacks(tf, next); cpu::write_tls_base(next->exec.tls_base); arch_post_switch(next); // Defer prev->on_cpu clear until switch teardown is complete. @@ -173,6 +200,7 @@ __PRIVILEGED_CODE void on_tick(aarch64::trap_frame* tf) { fpu::restore(&next->exec.fpu_ctx); load_cpu_context(&next->exec.cpu_ctx, tf); + prepare_trap_return_stacks(tf, next); cpu::write_tls_base(next->exec.tls_base); arch_post_switch(next); // Prevent early off-CPU publication while trap exit still depends on prev context. diff --git a/kernel/arch/aarch64/syscall/linux_syscalls.h b/kernel/arch/aarch64/syscall/linux_syscalls.h index a13b86a3..fe81b36e 100644 --- a/kernel/arch/aarch64/syscall/linux_syscalls.h +++ b/kernel/arch/aarch64/syscall/linux_syscalls.h @@ -24,6 +24,9 @@ constexpr uint64_t EXIT = 93; constexpr uint64_t EXIT_GROUP = 94; constexpr uint64_t SET_TID_ADDRESS = 96; constexpr uint64_t NANOSLEEP = 101; +constexpr uint64_t CLOCK_GETTIME = 113; +constexpr uint64_t CLOCK_GETRES = 114; +constexpr uint64_t GETTIMEOFDAY = 169; constexpr uint64_t SOCKET = 198; constexpr uint64_t SOCKETPAIR = 199; constexpr uint64_t BIND = 200; diff --git a/kernel/arch/aarch64/trap/trap_frame.h b/kernel/arch/aarch64/trap/trap_frame.h index a1117d80..62ddf757 100644 --- a/kernel/arch/aarch64/trap/trap_frame.h +++ b/kernel/arch/aarch64/trap/trap_frame.h @@ -8,13 +8,22 @@ namespace aarch64 { struct alignas(16) trap_frame { uint64_t x[31]; // x0-x30 uint64_t sp; // interrupted-context SP (EL0 traps store SP_EL0) + uint64_t sp_el1; // EL1 exception stack pointer to use after trap return uint64_t elr; uint64_t spsr; uint64_t esr; uint64_t far; + uint64_t _reserved0; }; -static_assert(sizeof(trap_frame) == 0x120); +static_assert(__builtin_offsetof(trap_frame, x) == 0x00); +static_assert(__builtin_offsetof(trap_frame, sp) == 0xF8); +static_assert(__builtin_offsetof(trap_frame, sp_el1) == 0x100); +static_assert(__builtin_offsetof(trap_frame, elr) == 0x108); +static_assert(__builtin_offsetof(trap_frame, spsr) == 0x110); +static_assert(__builtin_offsetof(trap_frame, esr) == 0x118); +static_assert(__builtin_offsetof(trap_frame, far) == 0x120); +static_assert(sizeof(trap_frame) == 0x130); inline uint64_t get_ip(const trap_frame* tf) { return tf->elr; diff --git a/kernel/arch/aarch64/trap/vectors.S b/kernel/arch/aarch64/trap/vectors.S index 04b3d356..5a0a27e6 100644 --- a/kernel/arch/aarch64/trap/vectors.S +++ b/kernel/arch/aarch64/trap/vectors.S @@ -20,15 +20,16 @@ /* trap::trap_frame offsets (bytes) */ .set TF_X0, 0x00 .set TF_SP, 0xF8 -.set TF_ELR, 0x100 -.set TF_SPSR, 0x108 -.set TF_ESR, 0x110 -.set TF_FAR, 0x118 -.set TF_SIZE, 0x120 +.set TF_SP_EL1, 0x100 +.set TF_ELR, 0x108 +.set TF_SPSR, 0x110 +.set TF_ESR, 0x118 +.set TF_FAR, 0x120 +.set TF_SIZE, 0x130 .macro STLX_A64_SAVE_GPRS /* ARMv8-A guarantees SP_EL1 is 16-byte aligned on exception entry. - * TF_SIZE (0x120) is 16-byte aligned, so SP remains aligned after subtraction. */ + * TF_SIZE (0x130) is 16-byte aligned, so SP remains aligned after subtraction. */ sub sp, sp, #TF_SIZE stp x0, x1, [sp, #0x00] stp x2, x3, [sp, #0x10] @@ -48,7 +49,7 @@ str x30, [sp, #0xF0] .endm -.macro STLX_A64_RESTORE_GPRS +.macro STLX_A64_RESTORE_GPRS_EXCEPT_X16 ldp x0, x1, [sp, #0x00] ldp x2, x3, [sp, #0x10] ldp x4, x5, [sp, #0x20] @@ -57,7 +58,7 @@ ldp x10, x11, [sp, #0x50] ldp x12, x13, [sp, #0x60] ldp x14, x15, [sp, #0x70] - ldp x16, x17, [sp, #0x80] + ldr x17, [sp, #0x88] ldp x18, x19, [sp, #0x90] ldp x20, x21, [sp, #0xA0] ldp x22, x23, [sp, #0xB0] @@ -65,7 +66,6 @@ ldp x26, x27, [sp, #0xD0] ldp x28, x29, [sp, #0xE0] ldr x30, [sp, #0xF0] - add sp, sp, #TF_SIZE .endm .macro STLX_A64_CAPTURE_SYSREGS @@ -83,7 +83,7 @@ .macro STLX_A64_WRITEBACK_SYSREGS /* Use x16 (IP0, caller-saved scratch) as temporary to avoid clobbering x0. - * x16 will be restored from trap_frame at offset 0x80 in RESTORE_GPRS. */ + * x16 will be restored from trap_frame at offset 0x80 in epilogue. */ ldr x16, [sp, #TF_ELR] msr elr_el1, x16 ldr x16, [sp, #TF_SPSR] @@ -92,12 +92,61 @@ .macro STLX_A64_CALL_C handler /* SP is 16-byte aligned: ARMv8-A guarantees SP_EL1 alignment on exception entry, - * and TF_SIZE (0x120 = 288 = 16×18) preserves alignment. AArch64 PCS requires + * and TF_SIZE (0x130 = 304 = 16×19) preserves alignment. AArch64 PCS requires * 16-byte alignment at function call boundaries. */ mov x0, sp /* arg0 = trap_frame* */ bl \handler .endm +/* + * Mode-aware trap epilogue: + * - Return to EL1h (SPSR.M[0]=1): restore SP_EL1 from trap_frame.sp + * - Return to EL0t/EL1t (SPSR.M[0]=0): + * * restore SP_EL0 from trap_frame.sp + * * switch active EL1 stack to trap_frame.sp_el1 + * + * This is required when scheduler switches away from an in-kernel blocked + * context: the outgoing task's kernel call chain must not remain as the + * active SP_EL1 for unrelated tasks. + */ +.macro STLX_A64_RETURN_FROM_FRAME + ldr x17, [sp, #TF_SPSR] + tst x17, #1 + b.ne 1f + + /* Return to EL0t/EL1t: publish selected task SP_EL0. */ + ldr x17, [sp, #TF_SP] + msr sp_el0, x17 + + STLX_A64_WRITEBACK_SYSREGS + isb + mov x16, sp + STLX_A64_RESTORE_GPRS_EXCEPT_X16 + + /* Install selected task's exception stack for the next trap. */ + ldr x17, [x16, #TF_SP_EL1] + mov sp, x17 + + /* Restore scratch regs last (x16 held frame base). */ + ldr x17, [x16, #0x88] + ldr x16, [x16, #0x80] + eret + +1: + /* Return to EL1h: resume at exact interrupted kernel SP. */ + STLX_A64_WRITEBACK_SYSREGS + isb + mov x16, sp + STLX_A64_RESTORE_GPRS_EXCEPT_X16 + ldr x17, [x16, #TF_SP] + mov sp, x17 + + /* Restore scratch regs last (x16 held frame base). */ + ldr x17, [x16, #0x88] + ldr x16, [x16, #0x80] + eret +.endm + /* EL1-origin prologue/epilogue */ .macro STLX_A64_EL1_ENTRY handler STLX_A64_SAVE_GPRS @@ -106,25 +155,11 @@ /* Use x16 (IP0, caller-saved scratch) as temporary. x16 already saved above. */ add x16, sp, #TF_SIZE str x16, [sp, #TF_SP] + str x16, [sp, #TF_SP_EL1] STLX_A64_CAPTURE_SYSREGS STLX_A64_CALL_C \handler - - /* If returning to a mode that uses SP_EL0 (EL0t/EL1t), honor trap_frame.sp. - * This is required when the scheduler switches tasks from an EL1h context: - * load_cpu_context() updates trap_frame.sp for the selected task, and we - * must publish it to SP_EL0 before eret. */ - ldr x16, [sp, #TF_SPSR] - tst x16, #1 - b.ne 1f - ldr x16, [sp, #TF_SP] - msr sp_el0, x16 -1: - - STLX_A64_WRITEBACK_SYSREGS - isb - STLX_A64_RESTORE_GPRS - eret + STLX_A64_RETURN_FROM_FRAME .endm /* EL0-origin prologue/epilogue */ @@ -135,19 +170,12 @@ /* Use x16 (IP0, caller-saved scratch) as temporary. x16 already saved above. */ mrs x16, sp_el0 str x16, [sp, #TF_SP] + add x16, sp, #TF_SIZE + str x16, [sp, #TF_SP_EL1] STLX_A64_CAPTURE_SYSREGS STLX_A64_CALL_C \handler - - /* Allow handler to change return SP_EL0 via trap_frame.sp */ - /* Use x16 (IP0, caller-saved scratch) as temporary. x16 will be restored below. */ - ldr x16, [sp, #TF_SP] - msr sp_el0, x16 - - STLX_A64_WRITEBACK_SYSREGS - isb - STLX_A64_RESTORE_GPRS - eret + STLX_A64_RETURN_FROM_FRAME .endm /* @@ -175,19 +203,12 @@ /* Use x16 (IP0, caller-saved scratch) as temporary. x16 already saved above. */ mrs x16, sp_el0 str x16, [sp, #TF_SP] + add x16, sp, #TF_SIZE + str x16, [sp, #TF_SP_EL1] STLX_A64_CAPTURE_SYSREGS STLX_A64_CALL_C \handler - - /* Allow handler to change return SP_EL0 via trap_frame.sp */ - /* Use x16 (IP0, caller-saved scratch) as temporary. x16 will be restored below. */ - ldr x16, [sp, #TF_SP] - msr sp_el0, x16 - - STLX_A64_WRITEBACK_SYSREGS - isb - STLX_A64_RESTORE_GPRS - eret + STLX_A64_RETURN_FROM_FRAME .endm /* 2KB-aligned vector table */ diff --git a/kernel/arch/x86_64/clock/clock.cpp b/kernel/arch/x86_64/clock/clock.cpp index dd038e12..cba52703 100644 --- a/kernel/arch/x86_64/clock/clock.cpp +++ b/kernel/arch/x86_64/clock/clock.cpp @@ -3,6 +3,7 @@ #include "hw/portio.h" #include "hw/mmio.h" #include "hw/cpu.h" +#include "hw/rtc.h" #include "cpu/features.h" #include "common/logging.h" @@ -12,6 +13,7 @@ static uint64_t g_tsc_freq; static uint64_t g_mult; static uint32_t g_shift; static bool g_calibrated; +static uint64_t g_boot_realtime_ns; constexpr uint64_t NS_PER_SEC = 1000000000ULL; @@ -89,6 +91,7 @@ __PRIVILEGED_CODE int32_t init() { } compute_mult_shift(g_tsc_freq, &g_mult, &g_shift); + g_boot_realtime_ns = rtc::boot_unix_ns(); __atomic_store_n(&g_calibrated, true, __ATOMIC_RELEASE); log::info("clock: TSC freq=%lu Hz, mult=%lu shift=%u%s", @@ -122,4 +125,8 @@ uint64_t freq_hz() { return g_tsc_freq; } +uint64_t boot_realtime_ns() { + return g_boot_realtime_ns; +} + } // namespace clock diff --git a/kernel/arch/x86_64/hw/rtc.cpp b/kernel/arch/x86_64/hw/rtc.cpp new file mode 100644 index 00000000..d74ac1d8 --- /dev/null +++ b/kernel/arch/x86_64/hw/rtc.cpp @@ -0,0 +1,159 @@ +#include "hw/rtc.h" +#include "hw/portio.h" +#include "common/time_util.h" +#include "common/logging.h" +#include "acpi/acpi.h" +#include "common/string.h" + +namespace rtc { + +static uint64_t g_boot_unix_ns; + +constexpr uint64_t NS_PER_SEC = 1000000000ULL; + +constexpr uint16_t CMOS_ADDR = 0x70; +constexpr uint16_t CMOS_DATA = 0x71; +constexpr uint8_t NMI_DISABLE = 0x80; + +constexpr uint8_t REG_SECONDS = 0x00; +constexpr uint8_t REG_MINUTES = 0x02; +constexpr uint8_t REG_HOURS = 0x04; +constexpr uint8_t REG_DAY = 0x07; +constexpr uint8_t REG_MONTH = 0x08; +constexpr uint8_t REG_YEAR = 0x09; +constexpr uint8_t REG_STATUS_A = 0x0A; +constexpr uint8_t REG_STATUS_B = 0x0B; + +constexpr uint8_t STATUS_A_UIP = 0x80; +constexpr uint8_t STATUS_B_24HR = 0x02; +constexpr uint8_t STATUS_B_BINARY = 0x04; + +constexpr size_t FADT_CENTURY_OFFSET = 108; + +/** @note Privilege: **required** */ +__PRIVILEGED_CODE static uint8_t cmos_read(uint8_t reg) { + portio::out8(CMOS_ADDR, reg | NMI_DISABLE); + return portio::in8(CMOS_DATA); +} + +static uint8_t bcd_to_bin(uint8_t val) { + return static_cast((val & 0x0F) + (val >> 4) * 10); +} + +/** + * Read the FADT century register index (ACPI 2.0+, offset 108). + * Returns 0 if FADT is unavailable or field is zero. + * @note Privilege: **required** + */ +__PRIVILEGED_CODE static uint8_t get_fadt_century_register() { + const auto* fadt = acpi::find_table("FACP"); + if (!fadt) { + return 0; + } + + uint32_t length; + string::memcpy(&length, &fadt->length, sizeof(length)); + if (length < FADT_CENTURY_OFFSET + sizeof(uint8_t)) { + return 0; + } + + uint8_t century_reg; + string::memcpy(¢ury_reg, + reinterpret_cast(fadt) + FADT_CENTURY_OFFSET, + sizeof(century_reg)); + return century_reg; +} + +struct rtc_time { + uint8_t sec, min, hr, day, mon, year, century; +}; + +/** + * Read all RTC time registers in one pass. + * @note Privilege: **required** + */ +__PRIVILEGED_CODE static void read_rtc_raw(rtc_time& t, uint8_t century_reg) { + t.sec = cmos_read(REG_SECONDS); + t.min = cmos_read(REG_MINUTES); + t.hr = cmos_read(REG_HOURS); + t.day = cmos_read(REG_DAY); + t.mon = cmos_read(REG_MONTH); + t.year = cmos_read(REG_YEAR); + t.century = century_reg ? cmos_read(century_reg) : 0; +} + +__PRIVILEGED_CODE int32_t init() { + uint8_t century_reg = get_fadt_century_register(); + + // Double-read for consistency: two consecutive reads must match to ensure + // we didn't read across an RTC update cycle (~244 us window). + rtc_time t1, t2; + while (cmos_read(REG_STATUS_A) & STATUS_A_UIP) {} + read_rtc_raw(t1, century_reg); + for (;;) { + while (cmos_read(REG_STATUS_A) & STATUS_A_UIP) {} + read_rtc_raw(t2, century_reg); + if (t1.sec == t2.sec && t1.min == t2.min && t1.hr == t2.hr && + t1.day == t2.day && t1.mon == t2.mon && t1.year == t2.year && + t1.century == t2.century) + break; + t1 = t2; + } + + uint8_t status_b = cmos_read(REG_STATUS_B); + bool is_binary = (status_b & STATUS_B_BINARY) != 0; + bool is_24hr = (status_b & STATUS_B_24HR) != 0; + + uint8_t sec = t2.sec; + uint8_t min = t2.min; + uint8_t hr = t2.hr; + uint8_t day = t2.day; + uint8_t mon = t2.mon; + uint8_t year = t2.year; + uint8_t cent = t2.century; + + if (!is_binary) { + sec = bcd_to_bin(sec); + min = bcd_to_bin(min); + hr = bcd_to_bin(static_cast(hr & 0x7F)); + day = bcd_to_bin(day); + mon = bcd_to_bin(mon); + year = bcd_to_bin(year); + if (cent) { + cent = bcd_to_bin(cent); + } + } else { + hr = static_cast(hr & 0x7F); + } + + if (!is_24hr) { + bool pm = (t2.hr & 0x80) != 0; + hr = static_cast(hr % 12); + if (pm) { + hr = static_cast(hr + 12); + } + } + + uint32_t full_year; + if (cent) { + full_year = static_cast(cent) * 100 + year; + } else { + full_year = (year < 70) ? 2000u + year : 1900u + year; + } + + uint64_t epoch_sec = time_util::date_to_unix(full_year, mon, day, hr, min, sec); + g_boot_unix_ns = epoch_sec * NS_PER_SEC; + + log::info("rtc: %u-%02u-%02u %02u:%02u:%02u UTC (epoch=%lu)", + full_year, static_cast(mon), static_cast(day), + static_cast(hr), static_cast(min), + static_cast(sec), epoch_sec); + + return OK; +} + +uint64_t boot_unix_ns() { + return g_boot_unix_ns; +} + +} // namespace rtc diff --git a/kernel/arch/x86_64/syscall/linux_syscalls.h b/kernel/arch/x86_64/syscall/linux_syscalls.h index 84f88c07..53c4cf9c 100644 --- a/kernel/arch/x86_64/syscall/linux_syscalls.h +++ b/kernel/arch/x86_64/syscall/linux_syscalls.h @@ -32,9 +32,12 @@ constexpr uint64_t CHDIR = 80; constexpr uint64_t FCHDIR = 81; constexpr uint64_t RMDIR = 84; constexpr uint64_t UNLINK = 87; +constexpr uint64_t GETTIMEOFDAY = 96; constexpr uint64_t ARCH_PRCTL = 158; constexpr uint64_t GETDENTS64 = 217; constexpr uint64_t SET_TID_ADDRESS = 218; +constexpr uint64_t CLOCK_GETTIME = 228; +constexpr uint64_t CLOCK_GETRES = 229; constexpr uint64_t EXIT_GROUP = 231; constexpr uint64_t OPENAT = 257; constexpr uint64_t NEWFSTATAT = 262; diff --git a/kernel/boot/boot.cpp b/kernel/boot/boot.cpp index 970e5e1b..8803a5b3 100644 --- a/kernel/boot/boot.cpp +++ b/kernel/boot/boot.cpp @@ -17,6 +17,7 @@ #include "exec/elf.h" #include "syscall/syscall_table.h" #include "terminal/terminal.h" +#include "hw/rtc.h" #ifdef STLX_UNIT_TESTS_ENABLED #include "runner.h" @@ -57,6 +58,10 @@ extern "C" __PRIVILEGED_CODE void stlx_init() { log::fatal("acpi::init failed"); } + if (rtc::init() != rtc::OK) { + log::warn("rtc::init failed, wall-clock time unavailable"); + } + if (irq::init() != irq::OK) { log::fatal("irq::init failed"); } diff --git a/kernel/clock/clock.h b/kernel/clock/clock.h index 5a908d62..20afefa2 100644 --- a/kernel/clock/clock.h +++ b/kernel/clock/clock.h @@ -41,6 +41,13 @@ uint64_t now_ns(); */ uint64_t freq_hz(); +/** + * @brief Unix epoch in nanoseconds at boot time. + * Returns 0 if no RTC was available or rtc::init() was not called. + * Unprivileged: reads a cached value from regular .bss. + */ +uint64_t boot_realtime_ns(); + } // namespace clock #endif // STELLUX_CLOCK_CLOCK_H diff --git a/kernel/common/time_util.cpp b/kernel/common/time_util.cpp new file mode 100644 index 00000000..d77d3228 --- /dev/null +++ b/kernel/common/time_util.cpp @@ -0,0 +1,26 @@ +#include "common/time_util.h" + +namespace time_util { + +uint64_t date_to_unix(uint32_t year, uint32_t mon, uint32_t day, + uint32_t hour, uint32_t min, uint32_t sec) { + uint32_t y = year; + uint32_t m = mon; + + // Shift Jan/Feb to months 11/12 of the previous year (puts Feb last + // so leap day doesn't split the year boundary in the formula). + if (m <= 2) { + m += 10; + y -= 1; + } else { + m -= 2; + } + + int64_t days = static_cast(y / 4 - y / 100 + y / 400 + + 367 * m / 12 + day) + + static_cast(y) * 365 - 719499; + + return static_cast(((days * 24 + hour) * 60 + min) * 60 + sec); +} + +} // namespace time_util diff --git a/kernel/common/time_util.h b/kernel/common/time_util.h new file mode 100644 index 00000000..8fe9a560 --- /dev/null +++ b/kernel/common/time_util.h @@ -0,0 +1,25 @@ +#ifndef STELLUX_COMMON_TIME_UTIL_H +#define STELLUX_COMMON_TIME_UTIL_H + +#include "common/types.h" + +namespace time_util { + +/** + * @brief Convert a Gregorian date to Unix epoch seconds. + * Uses the Linux mktime64 algorithm. + * Unprivileged: pure arithmetic, no hardware access. + * @param year Full year (e.g. 2026) + * @param mon Month 1-12 + * @param day Day of month 1-31 + * @param hour Hour 0-23 + * @param min Minute 0-59 + * @param sec Second 0-59 + * @return Seconds since 1970-01-01 00:00:00 UTC. + */ +uint64_t date_to_unix(uint32_t year, uint32_t mon, uint32_t day, + uint32_t hour, uint32_t min, uint32_t sec); + +} // namespace time_util + +#endif // STELLUX_COMMON_TIME_UTIL_H diff --git a/kernel/hw/rtc.h b/kernel/hw/rtc.h new file mode 100644 index 00000000..faaa24ec --- /dev/null +++ b/kernel/hw/rtc.h @@ -0,0 +1,31 @@ +#ifndef STELLUX_HW_RTC_H +#define STELLUX_HW_RTC_H + +#include "common/types.h" + +namespace rtc { + +constexpr int32_t OK = 0; +constexpr int32_t ERR = -1; + +/** + * @brief Read the hardware RTC and cache the boot-time Unix epoch. + * x86_64: reads CMOS RTC via port I/O. + * AArch64/QEMU: reads PL031 RTC via MMIO. + * AArch64/RPi4: uses build-time epoch fallback. + * Must be called after acpi::init() and mm::init(). + * @return OK on success, ERR on failure. + * @note Privilege: **required** + */ +__PRIVILEGED_CODE int32_t init(); + +/** + * @brief Unix epoch in nanoseconds captured at boot. + * Returns 0 if init() has not been called. + * Unprivileged: reads a cached value from regular .bss. + */ +uint64_t boot_unix_ns(); + +} // namespace rtc + +#endif // STELLUX_HW_RTC_H diff --git a/kernel/sched/sched_internal.h b/kernel/sched/sched_internal.h index fd0b438d..9408db8a 100644 --- a/kernel/sched/sched_internal.h +++ b/kernel/sched/sched_internal.h @@ -14,7 +14,8 @@ __PRIVILEGED_CODE void arch_init_task_context( /** * Arch-specific: called after picking the next task, before returning - * to trap exit. Updates TSS.RSP0 on x86 (no-op on aarch64). + * to trap exit. Updates architecture-specific post-switch state + * (e.g. TSS.RSP0 on x86, translation roots on aarch64). * @note Privilege: **required** */ __PRIVILEGED_CODE void arch_post_switch(task* next); diff --git a/kernel/syscall/handlers/sys_clock.cpp b/kernel/syscall/handlers/sys_clock.cpp new file mode 100644 index 00000000..72f2bdf6 --- /dev/null +++ b/kernel/syscall/handlers/sys_clock.cpp @@ -0,0 +1,145 @@ +#include "syscall/handlers/sys_clock.h" +#include "syscall/syscall_table.h" +#include "clock/clock.h" +#include "mm/uaccess.h" + +namespace { + +struct kernel_timespec { + int64_t tv_sec; + int64_t tv_nsec; +}; + +constexpr uint64_t CLOCK_REALTIME = 0; +constexpr uint64_t CLOCK_MONOTONIC = 1; +constexpr uint64_t CLOCK_PROCESS_CPUTIME_ID = 2; +constexpr uint64_t CLOCK_THREAD_CPUTIME_ID = 3; +constexpr uint64_t CLOCK_MONOTONIC_RAW = 4; +constexpr uint64_t CLOCK_REALTIME_COARSE = 5; +constexpr uint64_t CLOCK_MONOTONIC_COARSE = 6; +constexpr uint64_t CLOCK_BOOTTIME = 7; + +constexpr uint64_t NS_PER_SEC = 1000000000ULL; +constexpr int64_t COARSE_RES_NS = 10000000; // 10 ms at 100 Hz tick + +struct kernel_timeval { + int64_t tv_sec; + int64_t tv_usec; +}; + +struct kernel_timezone { + int32_t tz_minuteswest; + int32_t tz_dsttime; +}; + +uint64_t get_monotonic_ns() { + return clock::now_ns(); +} + +uint64_t get_realtime_ns() { + return clock::boot_realtime_ns() + clock::now_ns(); +} + +} // anonymous namespace + +DEFINE_SYSCALL2(clock_gettime, clock_id, u_tp) { + if (u_tp == 0) { + return syscall::EFAULT; + } + + uint64_t ns; + switch (clock_id) { + case CLOCK_MONOTONIC: + case CLOCK_MONOTONIC_RAW: + case CLOCK_MONOTONIC_COARSE: + case CLOCK_BOOTTIME: + case CLOCK_PROCESS_CPUTIME_ID: + ns = get_monotonic_ns(); + break; + case CLOCK_REALTIME: + case CLOCK_REALTIME_COARSE: + if (clock::boot_realtime_ns() == 0) { + return syscall::EINVAL; + } + ns = get_realtime_ns(); + break; + case CLOCK_THREAD_CPUTIME_ID: + return syscall::EINVAL; + default: + return syscall::EINVAL; + } + + kernel_timespec ts; + ts.tv_sec = static_cast(ns / NS_PER_SEC); + ts.tv_nsec = static_cast(ns % NS_PER_SEC); + + int32_t rc = mm::uaccess::copy_to_user( + reinterpret_cast(u_tp), &ts, sizeof(ts)); + if (rc != mm::uaccess::OK) { + return syscall::EFAULT; + } + return 0; +} + +DEFINE_SYSCALL2(clock_getres, clock_id, u_tp) { + int64_t res_ns; + switch (clock_id) { + case CLOCK_MONOTONIC: + case CLOCK_MONOTONIC_RAW: + case CLOCK_BOOTTIME: + case CLOCK_REALTIME: + case CLOCK_PROCESS_CPUTIME_ID: + res_ns = 1; + break; + case CLOCK_MONOTONIC_COARSE: + case CLOCK_REALTIME_COARSE: + res_ns = COARSE_RES_NS; + break; + case CLOCK_THREAD_CPUTIME_ID: + return syscall::EINVAL; + default: + return syscall::EINVAL; + } + + if (u_tp != 0) { + kernel_timespec res; + res.tv_sec = 0; + res.tv_nsec = res_ns; + + int32_t rc = mm::uaccess::copy_to_user( + reinterpret_cast(u_tp), &res, sizeof(res)); + if (rc != mm::uaccess::OK) { + return syscall::EFAULT; + } + } + return 0; +} + +DEFINE_SYSCALL2(gettimeofday, u_tv, u_tz) { + if (u_tv != 0) { + if (clock::boot_realtime_ns() == 0) { + return syscall::EINVAL; + } + uint64_t ns = get_realtime_ns(); + kernel_timeval tv; + tv.tv_sec = static_cast(ns / NS_PER_SEC); + tv.tv_usec = static_cast((ns % NS_PER_SEC) / 1000); + + int32_t rc = mm::uaccess::copy_to_user( + reinterpret_cast(u_tv), &tv, sizeof(tv)); + if (rc != mm::uaccess::OK) { + return syscall::EFAULT; + } + } + + if (u_tz != 0) { + kernel_timezone tz = {0, 0}; + int32_t rc = mm::uaccess::copy_to_user( + reinterpret_cast(u_tz), &tz, sizeof(tz)); + if (rc != mm::uaccess::OK) { + return syscall::EFAULT; + } + } + + return 0; +} diff --git a/kernel/syscall/handlers/sys_clock.h b/kernel/syscall/handlers/sys_clock.h new file mode 100644 index 00000000..d42bcde7 --- /dev/null +++ b/kernel/syscall/handlers/sys_clock.h @@ -0,0 +1,10 @@ +#ifndef STELLUX_SYSCALL_HANDLERS_SYS_CLOCK_H +#define STELLUX_SYSCALL_HANDLERS_SYS_CLOCK_H + +#include "syscall/syscall_table.h" + +DECLARE_SYSCALL(clock_gettime); +DECLARE_SYSCALL(clock_getres); +DECLARE_SYSCALL(gettimeofday); + +#endif // STELLUX_SYSCALL_HANDLERS_SYS_CLOCK_H diff --git a/kernel/syscall/syscall_table.cpp b/kernel/syscall/syscall_table.cpp index 7f70ec1c..b2d61598 100644 --- a/kernel/syscall/syscall_table.cpp +++ b/kernel/syscall/syscall_table.cpp @@ -10,6 +10,7 @@ #include "syscall/handlers/sys_memfd.h" #include "syscall/handlers/sys_proc.h" #include "syscall/handlers/sys_pty.h" +#include "syscall/handlers/sys_clock.h" namespace syscall { @@ -42,6 +43,9 @@ __PRIVILEGED_CODE void init_syscall_table() { REGISTER_SYSCALL(linux_nr::EXIT_GROUP, exit_group); REGISTER_SYSCALL(linux_nr::SET_TID_ADDRESS, set_tid_address); REGISTER_SYSCALL(linux_nr::NANOSLEEP, nanosleep); + REGISTER_SYSCALL(linux_nr::CLOCK_GETTIME, clock_gettime); + REGISTER_SYSCALL(linux_nr::CLOCK_GETRES, clock_getres); + REGISTER_SYSCALL(linux_nr::GETTIMEOFDAY, gettimeofday); REGISTER_SYSCALL(linux_nr::SOCKET, socket); REGISTER_SYSCALL(linux_nr::SOCKETPAIR, socketpair); diff --git a/userland/apps/Makefile b/userland/apps/Makefile index 29eeac2a..a76215ff 100644 --- a/userland/apps/Makefile +++ b/userland/apps/Makefile @@ -2,7 +2,7 @@ # Stellux Userland - Applications # -APP_DIRS := init hello shell ls cat rm stat touch sleep true false clear ptytest +APP_DIRS := init hello shell ls cat rm stat touch sleep true false clear ptytest date clockbench APP_COUNT := $(words $(APP_DIRS)) all: diff --git a/userland/apps/clockbench/Makefile b/userland/apps/clockbench/Makefile new file mode 100644 index 00000000..84f7cb13 --- /dev/null +++ b/userland/apps/clockbench/Makefile @@ -0,0 +1,2 @@ +APP_NAME := clockbench +include ../../mk/app.mk diff --git a/userland/apps/clockbench/src/clockbench.c b/userland/apps/clockbench/src/clockbench.c new file mode 100644 index 00000000..55f7bc7e --- /dev/null +++ b/userland/apps/clockbench/src/clockbench.c @@ -0,0 +1,39 @@ +#define _POSIX_C_SOURCE 199309L +#include +#include +#include + +int main(int argc, char* argv[]) { + setvbuf(stdout, NULL, _IONBF, 0); + + int n = 100000; + if (argc > 1) { + n = atoi(argv[1]); + if (n <= 0) n = 100000; + } + + struct timespec start, end, ts; + + if (clock_gettime(CLOCK_MONOTONIC, &start) != 0) { + printf("clockbench: clock_gettime failed\r\n"); + return 1; + } + + for (int i = 0; i < n; i++) { + clock_gettime(CLOCK_MONOTONIC, &ts); + } + + if (clock_gettime(CLOCK_MONOTONIC, &end) != 0) { + printf("clockbench: clock_gettime failed\r\n"); + return 1; + } + + long long elapsed_ns = (long long)(end.tv_sec - start.tv_sec) * 1000000000LL + + (end.tv_nsec - start.tv_nsec); + long long avg_ns = elapsed_ns / n; + long long elapsed_us = elapsed_ns / 1000; + + printf("%d calls in %lld.%03lld ms (avg %lld ns/call)\r\n", + n, elapsed_us / 1000, elapsed_us % 1000, avg_ns); + return 0; +} diff --git a/userland/apps/date/Makefile b/userland/apps/date/Makefile new file mode 100644 index 00000000..2cc9a849 --- /dev/null +++ b/userland/apps/date/Makefile @@ -0,0 +1,2 @@ +APP_NAME := date +include ../../mk/app.mk diff --git a/userland/apps/date/src/date.c b/userland/apps/date/src/date.c new file mode 100644 index 00000000..b71d7bf7 --- /dev/null +++ b/userland/apps/date/src/date.c @@ -0,0 +1,24 @@ +#define _POSIX_C_SOURCE 199309L +#include +#include + +int main(void) { + setvbuf(stdout, NULL, _IONBF, 0); + + struct timespec ts; + if (clock_gettime(CLOCK_REALTIME, &ts) != 0) { + printf("date: clock_gettime failed\r\n"); + return 1; + } + + struct tm* t = gmtime(&ts.tv_sec); + if (!t) { + printf("date: gmtime failed\r\n"); + return 1; + } + + char buf[64]; + strftime(buf, sizeof(buf), "%a %b %e %H:%M:%S UTC %Y", t); + printf("%s\r\n", buf); + return 0; +}