feat(kernel): implement interrupt-driven timer and console with nanosleep support

- Replace syscall-based time functions with CLOCK_MONOTONIC using clock_gettime
- Add 100 Hz PIT timer and COM1 serial input interrupt handling via 8259 PIC
- Implement nanosleep syscall with proper duration calculation and interruptible sleep
- Add blocking console reads that wait for IRQ4 input instead of busy polling
- Update clock resolution to 10 ms (100 Hz) for monotonic clock
- Add proper timer tick tracking and interrupt dispatch mechanism
- Enable hardware interrupts in user mode while maintaining syscall protection
- Add sleep function wrapper around nanosleep in
master
韩天峰 4 weeks ago
parent 875c726810
commit 7726f49cd7
  1. 5
      examples/typephp-os/README.md
  2. 11
      examples/typephp-os/ROADMAP.md
  3. 2
      examples/typephp-os/kernel/core/abi/typephp_os_abi.h
  4. 1
      examples/typephp-os/kernel/core/abi/typephp_os_syscall.h
  5. 41
      examples/typephp-os/kernel/core/kernel.c
  6. 61
      examples/typephp-os/kernel/core/process-entry.S
  7. 147
      examples/typephp-os/kernel/core/process.c
  8. 5
      examples/typephp-os/user/README.md
  9. 14
      examples/typephp-os/user/cmd/systest.c
  10. 1
      examples/typephp-os/user/include/sys/syscall.h
  11. 1
      examples/typephp-os/user/include/time.h
  12. 1
      examples/typephp-os/user/include/unistd.h
  13. 19
      examples/typephp-os/user/runtime/host.cc
  14. 14
      examples/typephp-os/user/runtime/posix.c

@ -224,13 +224,16 @@ segment.
The native x86-64 `SYSCALL` boundary provides synchronous `read`, `write`, `close`,
`lseek`, `openat`, `exit`/`exit_group`, `getcwd`, `chdir`, `mkdir`, `rmdir`,
`unlink`, file stat/access/persistence/truncation families, fixed identity
queries, `time`, `gettimeofday`, `clock_gettime`, `clock_getres`, `brk`,
queries, `time`, `gettimeofday`, `clock_gettime`, `clock_getres`, `nanosleep`, `brk`,
anonymous private `mmap`, `mprotect`, `munmap`, `getdents64`, the initial
`fcntl` flag operations, fixed-console `ioctl(TIOCGWINSZ)`, and private
spawn and same-directory rename operations. The former private directory-list
syscall has been removed; `ls` and PHP Nano use the standard directory ABI. Standard
input and output are backed by QEMU's COM1 serial console. Syscall numbers are
shared by the kernel and userspace through `typephp_os_syscall.h`.
The 8259 PIC routes a 100 Hz PIT timer on IRQ0 and COM1 input on IRQ4. Console
reads and sleeps block with `HLT` until an interrupt arrives, so the resident
shell no longer consumes a host CPU core while it waits at the prompt.
Entry immediately switches from the untrusted user RSP to a dedicated kernel
stack. Return uses `iretq`, allowing synchronous spawn/exit to replace the
complete saved user context.

@ -98,14 +98,15 @@ behavior.
does not yet replace the working userspace commands.
Single-task identity calls report PID/TID 1 for the resident shell, 2 for
its synchronous foreground command, and root UID/GID. Linux-compatible
wall/monotonic clock structures are exposed at the current one-second RTC
resolution. The `systest` ELF exercises this complete syscall slice.
wall-clock time uses the RTC while monotonic time and blocking sleeps use a
100 Hz PIT. The `systest` ELF exercises this complete syscall slice.
Syscall evolution follows the glibc migration rules in `user/README.md`.
7. **Native Class memory.** Exercise Wren GC through Zend MM and verify tracing
of PHPX fields under sustained allocation.
8. **Kernel services.** Interrupt-driven timer and keyboard, higher-level VM
region management, Zend-chunk reclamation, and a capability-oriented native
API.
8. **Kernel services.** The legacy PIC, a 100 Hz PIT clock, interrupt-driven
COM1 input, blocking console reads, and blocking `nanosleep` are implemented.
Higher-level VM region management, Zend-chunk reclamation, and a
capability-oriented native API remain future work.
9. **Packaging and CI.** Automate the two-stage ELF32/ELF64 build and QEMU boot
smoke test in GitHub Actions.

@ -43,6 +43,8 @@ void typephp_os_process_start(void);
long typephp_os_fs_path_type(const char *path);
long typephp_os_fs_getdents64(int fd, void *buffer, size_t capacity);
long typephp_os_console_read(void *buffer, size_t size);
void typephp_os_console_enable_interrupts(void);
void typephp_os_console_interrupt(void);
#ifdef __cplusplus
}

@ -18,6 +18,7 @@ enum typephp_os_syscall_number {
TYPEPHP_SYS_BRK = 12,
TYPEPHP_SYS_IOCTL = 16,
TYPEPHP_SYS_ACCESS = 21,
TYPEPHP_SYS_NANOSLEEP = 35,
TYPEPHP_SYS_GETPID = 39,
TYPEPHP_SYS_EXIT = 60,
TYPEPHP_SYS_UNAME = 63,

@ -8,11 +8,15 @@ enum {
VGA_WIDTH = 80,
VGA_HEIGHT = 25,
COM1 = 0x3f8,
SERIAL_BUFFER_SIZE = 8192,
};
static volatile uint16_t *const vga = (volatile uint16_t *) 0xb8000;
static uint32_t row;
static uint32_t column;
static volatile uint32_t serial_read_head;
static volatile uint32_t serial_read_tail;
static volatile uint8_t serial_read_buffer[SERIAL_BUFFER_SIZE];
static inline void outb(uint16_t port, uint8_t value)
{
@ -37,6 +41,25 @@ static void serial_init(void)
outb(COM1 + 4, 0x0b);
}
void typephp_os_console_enable_interrupts(void)
{
/* Received-data-available interrupt. MCR.OUT2 was enabled by serial_init
* and routes the UART interrupt to the legacy PIC's IRQ4 input. */
outb(COM1 + 1, 0x01);
}
void typephp_os_console_interrupt(void)
{
while ((inb(COM1 + 5) & 0x01u) != 0) {
const uint8_t value = inb(COM1);
const uint32_t next = (serial_read_head + 1u) % SERIAL_BUFFER_SIZE;
if (next != serial_read_tail) {
serial_read_buffer[serial_read_head] = value;
serial_read_head = next;
}
}
}
static void serial_put(uint8_t value)
{
while ((inb(COM1 + 5) & 0x20u) == 0) {
@ -50,14 +73,18 @@ long typephp_os_console_read(void *buffer, unsigned long size)
if (output == 0 || size == 0) {
return 0;
}
/* COM1 is the standard input device for the headless QEMU target. Keep
* this synchronous: the process model intentionally has one foreground
* task and no scheduler yet. */
while ((inb(COM1 + 5) & 0x01u) == 0) {
__asm__ volatile("pause");
/* The system remains single-task, but a blocking read can still halt the
* CPU until IRQ4 supplies input. STI followed immediately by HLT avoids
* the empty-buffer/check-to-sleep lost-wakeup race on x86. */
for (;;) {
__asm__ volatile("cli" : : : "memory");
if (serial_read_tail != serial_read_head) {
output[0] = serial_read_buffer[serial_read_tail];
serial_read_tail = (serial_read_tail + 1u) % SERIAL_BUFFER_SIZE;
return 1;
}
__asm__ volatile("sti; hlt; cli" : : : "memory");
}
output[0] = inb(COM1);
return 1;
}
static void vga_scroll(void)

@ -89,6 +89,60 @@ TYPEPHP_EXCEPTION_WITH_ERROR 12
TYPEPHP_EXCEPTION_WITH_ERROR 13
TYPEPHP_EXCEPTION_WITH_ERROR 14
.macro TYPEPHP_IRQ number vector
.global typephp_os_irq_\number
.type typephp_os_irq_\number, @function
typephp_os_irq_\number:
pushq $0
pushq $\vector
jmp typephp_os_irq_common
.size typephp_os_irq_\number, . - typephp_os_irq_\number
.endm
TYPEPHP_IRQ 0 32
TYPEPHP_IRQ 4 36
.type typephp_os_irq_common, @function
.extern typephp_os_irq_dispatch
typephp_os_irq_common:
push %rbx
push %rcx
push %rdx
push %rsi
push %rdi
push %rbp
push %r8
push %r9
push %r10
push %r11
push %r12
push %r13
push %r14
push %r15
push %rax
mov %rsp, %rdi
call typephp_os_irq_dispatch
pop %rax
pop %r15
pop %r14
pop %r13
pop %r12
pop %r11
pop %r10
pop %r9
pop %r8
pop %rbp
pop %rdi
pop %rsi
pop %rdx
pop %rcx
pop %rbx
add $16, %rsp
iretq
.size typephp_os_irq_common, . - typephp_os_irq_common
.type typephp_os_exception_common, @function
.extern typephp_os_exception_dispatch
typephp_os_exception_common:
@ -133,14 +187,15 @@ typephp_os_exception_common:
.global typephp_os_enter_user
.type typephp_os_enter_user, @function
typephp_os_enter_user:
# RDI = ELF entry, RSI = top of the user stack. Interrupts remain masked;
# SYSCALL is synchronous and the first process manager has no scheduler.
# RDI = ELF entry, RSI = top of the user stack. Hardware interrupts stay
# enabled in Ring 3 so timer and serial input can arrive while a command is
# running. IA32_FMASK masks them again on every SYSCALL entry.
mov $0x1b, %ax
mov %ax, %ds
mov %ax, %es
pushq $0x1b
push %rsi
pushq $0x2
pushq $0x202
pushq $0x23
push %rdi
iretq

@ -35,6 +35,14 @@ enum {
USER_DATA_SELECTOR = 0x1b,
KERNEL_CODE_SELECTOR = 0x08,
TSS_SELECTOR = 0x28,
PIC_MASTER_COMMAND = 0x20,
PIC_MASTER_DATA = 0x21,
PIC_SLAVE_COMMAND = 0xa0,
PIC_SLAVE_DATA = 0xa1,
PIC_END_OF_INTERRUPT = 0x20,
PIT_CHANNEL_0 = 0x40,
PIT_COMMAND = 0x43,
TIMER_FREQUENCY = 100,
MAX_USER_ARGUMENTS = 8,
USER_PATH_MAX = 128,
UTSNAME_LENGTH = 65,
@ -216,6 +224,8 @@ extern void typephp_os_exception_11(void);
extern void typephp_os_exception_12(void);
extern void typephp_os_exception_13(void);
extern void typephp_os_exception_14(void);
extern void typephp_os_irq_0(void);
extern void typephp_os_irq_4(void);
extern int rename(const char *old_path, const char *new_path);
extern uint64_t physical_page_available(void);
extern uint64_t physical_page_total(void);
@ -229,6 +239,7 @@ static task_state_segment tss;
unsigned char typephp_os_syscall_stack[64u * 1024u] __attribute__((aligned(16)));
uint64_t typephp_os_syscall_user_rsp;
static process_state foreground_process = {.pid = 1, .cwd = "/"};
static volatile uint64_t timer_ticks;
enum {
IA32_EFER = 0xc0000080u,
@ -245,6 +256,57 @@ static uint64_t read_msr(uint32_t index)
return ((uint64_t) high << 32u) | low;
}
static void outb(uint16_t port, uint8_t value)
{
__asm__ volatile("outb %0, %1" : : "a"(value), "Nd"(port));
}
static void io_wait(void)
{
outb(0x80, 0);
}
static void install_interrupt_controller(void)
{
const uint16_t divisor = (uint16_t) (1193182u / TIMER_FREQUENCY);
/* Remap the legacy PIC away from the CPU exception vectors. */
outb(PIC_MASTER_COMMAND, 0x11);
io_wait();
outb(PIC_SLAVE_COMMAND, 0x11);
io_wait();
outb(PIC_MASTER_DATA, 0x20);
io_wait();
outb(PIC_SLAVE_DATA, 0x28);
io_wait();
outb(PIC_MASTER_DATA, 0x04);
io_wait();
outb(PIC_SLAVE_DATA, 0x02);
io_wait();
outb(PIC_MASTER_DATA, 0x01);
io_wait();
outb(PIC_SLAVE_DATA, 0x01);
io_wait();
/* A 100 Hz periodic PIT is sufficient for the single-task sleep ABI. */
outb(PIT_COMMAND, 0x36);
outb(PIT_CHANNEL_0, (uint8_t) divisor);
outb(PIT_CHANNEL_0, (uint8_t) (divisor >> 8u));
typephp_os_console_enable_interrupts();
/* IRQ0 (PIT) and IRQ4 (COM1) only. The slave PIC stays fully masked. */
outb(PIC_SLAVE_DATA, 0xff);
outb(PIC_MASTER_DATA, (uint8_t) ~(UINT8_C(1) | UINT8_C(1) << 4u));
}
static void pic_end_of_interrupt(unsigned int irq)
{
if (irq >= 8u) {
outb(PIC_SLAVE_COMMAND, PIC_END_OF_INTERRUPT);
}
outb(PIC_MASTER_COMMAND, PIC_END_OF_INTERRUPT);
}
static void write_msr(uint32_t index, uint64_t value)
{
__asm__ volatile("wrmsr" : : "c"(index), "a"((uint32_t) value),
@ -348,6 +410,8 @@ static void install_descriptor_tables(void)
install_idt_gate(12, typephp_os_exception_12, 0x8e);
install_idt_gate(13, typephp_os_exception_13, 0x8e);
install_idt_gate(14, typephp_os_exception_14, 0x8e);
install_idt_gate(32, typephp_os_irq_0, 0x8e);
install_idt_gate(36, typephp_os_irq_4, 0x8e);
pointer.limit = sizeof(idt) - 1u;
pointer.base = (uint64_t) (uintptr_t) idt;
__asm__ volatile("lidt %0" : : "m"(pointer) : "memory");
@ -362,6 +426,7 @@ static void install_descriptor_tables(void)
write_msr(IA32_LSTAR, (uint64_t) (uintptr_t) typephp_os_syscall_entry);
write_msr(IA32_FMASK, UINT64_C(0x700)); /* TF, IF and DF */
write_msr(IA32_EFER, read_msr(IA32_EFER) | UINT64_C(1));
install_interrupt_controller();
}
static int read_exact(int fd, void *buffer, size_t size)
@ -790,6 +855,7 @@ static long syscall_gettimeofday(user_timeval *result)
static long syscall_clock(int clock_id, user_timespec *result, int resolution)
{
uint64_t ticks;
if (clock_id != 0 && clock_id != 1) {
return -EINVAL;
}
@ -799,8 +865,66 @@ static long syscall_clock(int clock_id, user_timespec *result, int resolution)
if (!user_buffer(result, sizeof(*result))) {
return -EFAULT;
}
result->seconds = resolution ? 1 : typephp_os_time_seconds();
result->nanoseconds = 0;
if (resolution) {
result->seconds = clock_id == 0 ? 1 : 0;
result->nanoseconds = clock_id == 0
? 0
: 1000000000L / TIMER_FREQUENCY;
return 0;
}
if (clock_id == 0) {
result->seconds = typephp_os_time_seconds();
result->nanoseconds = 0;
return 0;
}
ticks = timer_ticks;
result->seconds = (long) (ticks / TIMER_FREQUENCY);
result->nanoseconds = (long) (ticks % TIMER_FREQUENCY)
* (1000000000L / TIMER_FREQUENCY);
return 0;
}
static long syscall_nanosleep(
const user_timespec *requested, user_timespec *remaining)
{
uint64_t seconds;
uint64_t subsecond_ticks;
uint64_t duration_ticks;
uint64_t deadline;
long nanoseconds;
if (!user_buffer(requested, sizeof(*requested))) {
return -EFAULT;
}
if (remaining != 0 && !user_buffer(remaining, sizeof(*remaining))) {
return -EFAULT;
}
if (requested->seconds < 0 || requested->nanoseconds < 0
|| requested->nanoseconds >= 1000000000L) {
return -EINVAL;
}
seconds = (uint64_t) requested->seconds;
nanoseconds = requested->nanoseconds;
subsecond_ticks = ((uint64_t) nanoseconds
+ (1000000000u / TIMER_FREQUENCY) - 1u)
/ (1000000000u / TIMER_FREQUENCY);
if (seconds > (UINT64_MAX - subsecond_ticks) / TIMER_FREQUENCY) {
return -EINVAL;
}
duration_ticks = seconds * TIMER_FREQUENCY + subsecond_ticks;
if (duration_ticks > INT64_MAX) {
return -EINVAL;
}
deadline = timer_ticks + duration_ticks;
while ((int64_t) (timer_ticks - deadline) < 0) {
/* The current single foreground process sleeps inside its syscall.
* IRQ0 advances timer_ticks and wakes HLT; no scheduler is required. */
__asm__ volatile("sti; hlt; cli" : : : "memory");
}
if (remaining != 0) {
remaining->seconds = 0;
remaining->nanoseconds = 0;
}
return 0;
}
@ -1199,6 +1323,22 @@ void typephp_os_exception_dispatch(exception_frame *frame)
restore_shell_after_fault(frame);
}
void typephp_os_irq_dispatch(exception_frame *frame)
{
switch (frame->vector) {
case 32:
++timer_ticks;
pic_end_of_interrupt(0);
return;
case 36:
typephp_os_console_interrupt();
pic_end_of_interrupt(4);
return;
default:
panic("unexpected hardware interrupt\n");
}
}
long typephp_os_syscall_dispatch(syscall_frame *frame)
{
switch (frame->rax) {
@ -1255,6 +1395,9 @@ long typephp_os_syscall_dispatch(syscall_frame *frame)
case TYPEPHP_SYS_ACCESS:
return syscall_access_path(AT_FDCWD, (const char *) frame->rdi,
(int) frame->rsi, 0);
case TYPEPHP_SYS_NANOSLEEP:
return syscall_nanosleep((const user_timespec *) frame->rdi,
(user_timespec *) frame->rsi);
case TYPEPHP_SYS_GETPID:
case TYPEPHP_SYS_GETTID:
return (long) foreground_process.pid;

@ -46,7 +46,7 @@ logic, while C/C++ is limited to the syscall or PHPX boundary.
The current shared userspace runtime provides `syscall`, `read`, `write`, `openat`,
`open`, `close`, `lseek`, `getcwd`, `chdir`, `mkdir`, `rmdir`, `unlink`,
`rename`, `stat`, `lstat`, `fstat`, `access`, `fsync`, `fdatasync`, `truncate`,
`ftruncate`, `time`, `gettimeofday`, `clock_gettime`, `clock_getres`, `uname`,
`ftruncate`, `time`, `gettimeofday`, `clock_gettime`, `clock_getres`, `nanosleep`, `sleep`, `uname`,
`getpid`, `getppid`, `gettid`, the root UID/GID queries, `brk`, `sbrk`, `mmap`,
`mprotect`, `munmap`, `opendir`, `fdopendir`, `readdir`, `rewinddir`,
`closedir`, `dirfd`, the `F_GETFD`/`F_SETFD`/`F_GETFL`/`F_SETFL` subset of
@ -85,7 +85,8 @@ The kernel additionally accepts Linux x86-64 `newfstatat`, `faccessat`,
metadata uses the Linux x86-64 144-byte `struct stat`
layout. FAT16 currently has no owners, ACLs, symlinks, executable file bit, or
sub-second timestamps: UID/GID are always root, ordinary files are `0666`,
directories are `0777`, and clock resolution is one second.
directories are `0777`. Wall-clock values currently retain one-second RTC
resolution; the monotonic clock and sleeping use the 100 Hz PIT.
`rename()` currently accepts only source and destination paths with the same
parent directory and does not replace an existing entry. Its transport uses a

@ -39,7 +39,9 @@ int main(int argc, char **argv)
struct timeval wall;
struct timespec realtime;
struct timespec monotonic;
struct timespec after_sleep;
struct timespec resolution;
const struct timespec sleep_duration = {0, 30000000};
struct winsize window;
DIR *directory;
struct dirent *entry;
@ -113,10 +115,18 @@ int main(int argc, char **argv)
|| clock_gettime(CLOCK_REALTIME, &realtime) != 0
|| clock_gettime(CLOCK_MONOTONIC, &monotonic) != 0
|| clock_getres(CLOCK_MONOTONIC, &resolution) != 0
|| realtime.tv_sec <= 0 || monotonic.tv_sec <= 0
|| resolution.tv_sec != 1 || resolution.tv_nsec != 0) {
|| realtime.tv_sec <= 0 || monotonic.tv_nsec < 0
|| monotonic.tv_nsec >= 1000000000L
|| resolution.tv_sec != 0 || resolution.tv_nsec != 10000000L) {
return fail("clock ABI");
}
if (nanosleep(&sleep_duration, NULL) != 0
|| clock_gettime(CLOCK_MONOTONIC, &after_sleep) != 0
|| after_sleep.tv_sec < monotonic.tv_sec
|| (after_sleep.tv_sec == monotonic.tv_sec
&& after_sleep.tv_nsec - monotonic.tv_nsec < 20000000L)) {
return fail("interruptible nanosleep");
}
(void) write(STDOUT_FILENO, "basic syscalls: OK\n",
sizeof("basic syscalls: OK\n") - 1);

@ -16,6 +16,7 @@
#define SYS_brk TYPEPHP_SYS_BRK
#define SYS_ioctl TYPEPHP_SYS_IOCTL
#define SYS_access TYPEPHP_SYS_ACCESS
#define SYS_nanosleep TYPEPHP_SYS_NANOSLEEP
#define SYS_getpid TYPEPHP_SYS_GETPID
#define SYS_exit TYPEPHP_SYS_EXIT
#define SYS_uname TYPEPHP_SYS_UNAME

@ -14,5 +14,6 @@ struct timespec {
time_t time(time_t *result);
int clock_gettime(clockid_t clock_id, struct timespec *value);
int clock_getres(clockid_t clock_id, struct timespec *value);
int nanosleep(const struct timespec *duration, struct timespec *remaining);
#endif

@ -30,6 +30,7 @@ uid_t getuid(void);
uid_t geteuid(void);
gid_t getgid(void);
gid_t getegid(void);
unsigned int sleep(unsigned int seconds);
int brk(void *address);
void *sbrk(intptr_t increment);
void _exit(int status) __attribute__((noreturn));

@ -4,6 +4,7 @@
#include <cstdint>
#include <cstring>
#include <sys/syscall.h>
#include <time.h>
#include <unistd.h>
extern "C" int typephp_nano_project_main();
@ -54,17 +55,23 @@ extern "C" void php_nano_host_system_time(
extern "C" std::uint64_t php_nano_host_monotonic_nanoseconds()
{
return static_cast<std::uint64_t>(syscall(SYS_time, nullptr))
* UINT64_C(1000000000);
timespec value{};
if (clock_gettime(CLOCK_MONOTONIC, &value) != 0) {
typephp_os_panic("unable to read the monotonic clock");
}
return static_cast<std::uint64_t>(value.tv_sec) * UINT64_C(1000000000)
+ static_cast<std::uint64_t>(value.tv_nsec);
}
extern "C" void php_nano_host_sleep(
std::uint64_t seconds, std::uint32_t nanoseconds)
{
const auto now = static_cast<std::uint64_t>(syscall(SYS_time, nullptr));
const auto deadline = now + seconds + (nanoseconds != 0 ? 1u : 0u);
while (static_cast<std::uint64_t>(syscall(SYS_time, nullptr)) < deadline) {
__asm__ volatile("pause");
const timespec duration{
static_cast<time_t>(seconds),
static_cast<long>(nanoseconds),
};
if (nanosleep(&duration, nullptr) != 0) {
typephp_os_panic("unable to sleep");
}
}

@ -142,18 +142,16 @@ int clock_getres(clockid_t clock_id, struct timespec *value)
unsigned int sleep(unsigned int seconds)
{
const time_t deadline = time(NULL) + seconds;
while (time(NULL) < deadline) {
__asm__ volatile("pause");
}
return 0;
struct timespec duration = {(time_t) seconds, 0};
struct timespec remaining = {0, 0};
return nanosleep(&duration, &remaining) == 0
? 0
: (unsigned int) remaining.tv_sec + (remaining.tv_nsec != 0);
}
int nanosleep(const struct timespec *duration, struct timespec *remaining)
{
(void) remaining;
sleep((unsigned int) duration->tv_sec + (duration->tv_nsec != 0));
return 0;
return (int) syscall(TYPEPHP_SYS_NANOSLEEP, duration, remaining);
}
int uname(struct utsname *value)

Loading…
Cancel
Save