In this blog I share my observations, thoughts and experience about computers, linguistics, philosophy and many other things that interest me.

Saturday, April 04, 2026

QRV progress: Interrupt-Driven Console, Fault Handling, kill()

We're progressing towards v0.21 steadily. Today's commits are about three things worth writing up properly because each one marks a qualitative step in how the system behaves.


Interrupt-Driven Console

This is the headline. Up to now, the shell's console I/O ran through devtext — a simple in-kernel device that bypassed the full driver infrastructure. v0.20.1 is the first version where the shell runs on devc-ser8250, the real UART driver, receiving input through PLIC-delivered hardware interrupts.

That sentence is easy to write and took most of the day to make true.

There were three separate bugs in the PLIC interrupt path, all of which had to be fixed before a single interrupt arrived:

plic_inithart() was never called. The PLIC's interrupt priority threshold was left at QEMU's default of 7, which blocks everything at priority 1 — which is where UART interrupts live. No call to plic_inithart(), no interrupts, ever. The kind of bug that is invisible until you know to look for it.

Wrong interrupt level mapping. The PLIC handler was using irq + 32 as a raw index into the interrupt level table. But get_interrupt_level() maps vector 42 to internal level 11 — there is a translation step, and bypassing it meant the interrupt was routed to the wrong handler slot.

IRQ unmasked on one hart only. The PLIC unmask was enabling the IRQ only on the calling hart, which works on uniprocessor but breaks on SMP when the UART interrupt ends up routed to a different hart. Fixed to unmask on all harts, matching Linux's plic_irq_toggle behavior.

On the devc-ser8250 side: the MCR OUT2 bit — which gates the UART's interrupt output signal to the PLIC bus — was not being set. Without it, the UART asserts interrupts internally but never signals the interrupt controller. Also: stale interrupt conditions in LSR/RHR/IIR/MSR were not being cleared before enabling IER, which can cause a spurious interrupt storm on startup.

After all of that: the shell prompt appears on the serial console, keyboard input arrives via interrupt, and the system behaves like a system.

The console switching mechanism is worth a note. /dev/console is now a pathmgr symlink, initially pointing to /dev/text (devtext). When the init script starts devc-ser8250 and the shell runs reopen /dev/ser1, it calls sysmgr_console_set() to update the symlink and redirects its own file descriptors. Properly QNX-style: just path management.


User-Mode Fault Handling

Previously, if a user-space process triggered a fault — null pointer dereference, stack overflow, misaligned access — the kernel would either crash or hang.

v0.20.1 handles U-mode faults properly. The trap handler detects that the faulting instruction was in U-mode (scause 0–15), maps the RISC-V exception code to a POSIX signal number and fault code, and calls usr_fault() to tear the process down cleanly. nano_fault.c calls thread_destroyall and force_ready on all non-active threads (including the system thread blocked in MsgReceivePulse), then thread_destroy to bring num_active_threads to zero. The process exits, control returns to the shell, and the shell's waitpid checks WIFSIGNALED and prints something descriptive — "Memory fault", "Bus error", the signal number.

The kernel no longer crashes when a user process misbehaves. That is a meaningful threshold to cross.


Pulse-Based kill()

kill() now works, implemented in a way that fits the QNX architecture naturally: kill() in libc sends a _PROC_KILL message to taskman, taskman delivers a _PULSE_CODE_SIGNAL pulse to the target process's system channel, and the system thread's default handler acts on it. No special kernel machinery — just the message-passing infrastructure that was already there.

The shell has a kill built-in supporting kill -<signal> <pid> syntax.


Next

The immediate target for v0.21 is devb-virtio + a writable filesystem — moving from a read-only CPIO root to something you can actually write to. The infrastructure is largely in place.

No comments: