feat: process exit codes, posix_spawn, 4 KiB command lines

Groundwork for the GCC driver: a compiler driver must spawn cc1/as/ld
and know whether each stage succeeded.

Kernel: the scheduler keeps an exit-code ledger (pid -> code; pids are
monotonic so entries never alias), published during teardown right
before waiters wake. SYS_EXIT records main()'s return value, SYS_KILL
records 256+SIGKILL, and the exception handler records 256+signal
mapped from the fault vector (#PF/#GP -> SIGSEGV, #DE/FP -> SIGFPE,
#UD -> SIGILL). SYS_WAITPID now returns the code: 0..255 for a normal
exit, 256+signal for a violent death. Process args grow from 256
bytes to 4 KiB (cc1 invocations do not fit in 256), with crt1 now
parsing up to 255 argv entries from a static buffer.

libc: new spawn.h with posix_spawn/posix_spawnp over SYS_SPAWN -
libiberty's pex layer has a posix_spawn backend, so GCC's driver works
without fork. argv is joined into the kernel args string (spaces in
arguments rejected; no kernel quoting), envp is not transferred, and
non-empty file actions fail loudly with ENOTSUP until the kernel can
redirect stdio on spawn. waitpid() now decodes real POSIX status and
the sys/wait.h macros distinguish exited from signaled children.

Shell: prints [exit code N] after nonzero exits and [terminated by
signal N] for killed or crashed children.

Verified on the OS: cat on a missing file reports exit code 1; a
window-close (clean exit 0) stays silent as it should.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
2026-07-16 16:59:17 +02:00
co-authored by Claude Fable 5
parent 54c0584b85
commit 75184e0651
19 changed files with 326 additions and 23 deletions
+50 -1
View File
@@ -43,6 +43,15 @@ namespace Sched {
static Process processTable[MaxProcesses];
static int nextPid = 0;
// Exit-code ledger. Pids are monotonic and never recycled, so entries
// cannot alias. Written under schedLock during process teardown; read
// by SYS_WAITPID after the waiter unblocks. Codes: 0..255 = normal
// exit status, 256+signal = killed or crashed.
static constexpr int ExitLedgerSize = 128;
static int exitLedgerPid[ExitLedgerSize];
static int exitLedgerCode[ExitLedgerSize];
static int exitLedgerHead = 0;
// The scheduler lock MUST be a Spinlock (interrupt-disabling).
// It is held ACROSS context switches to prevent the race where
// another CPU picks up a process whose RSP hasn't been saved yet.
@@ -212,6 +221,11 @@ namespace Sched {
}
nextPid = 0;
for (int i = 0; i < ExitLedgerSize; i++) {
exitLedgerPid[i] = -1;
exitLedgerCode[i] = 0;
}
exitLedgerHead = 0;
Hal::RegisterIrqHandler(Hal::IRQ_RESCHEDULE, RescheduleIpiHandler);
Kt::KernelLogStream(Kt::OK, "Sched") << "Initialized (" << MaxProcesses
@@ -389,7 +403,7 @@ namespace Sched {
proc.args[0] = '\0';
if (args != nullptr) {
int i = 0;
for (; i < 255 && args[i]; i++) {
for (; i < 4095 && args[i]; i++) {
proc.args[i] = args[i];
}
proc.args[i] = '\0';
@@ -1152,6 +1166,12 @@ namespace Sched {
proc.runningOnCpu = -1;
proc.reapReady = true;
// Publish the exit code before waking waiters so SYS_WAITPID
// observes it the moment it unblocks.
exitLedgerPid[exitLedgerHead] = exitingPid;
exitLedgerCode[exitLedgerHead] = proc.exitCode;
exitLedgerHead = (exitLedgerHead + 1) % ExitLedgerSize;
// Wake any processes blocked on this PID
for (int i = 0; i < MaxProcesses; i++) {
if (processTable[i].state == ProcessState::Blocked &&
@@ -1232,6 +1252,8 @@ namespace Sched {
return -1;
}
processTable[primarySlot_].exitCode = 256 + 9; /* killed (SIGKILL) */
// Flag the main thread so its next tick (or scheduler dispatch)
// routes through ExitProcess, which sweeps every sibling thread
// and tears down the address space. If the main thread is parked
@@ -1263,6 +1285,33 @@ namespace Sched {
return 0;
}
int LookupExitCode(int pid) {
schedLock.Acquire();
int code = 0;
for (int i = 0; i < ExitLedgerSize; i++) {
if (exitLedgerPid[i] == pid) {
code = exitLedgerCode[i];
break;
}
}
schedLock.Release();
return code;
}
void SetProcessExitCode(int pid, int code) {
schedLock.Acquire();
for (int i = 0; i < MaxProcesses; i++) {
if (processTable[i].pid != pid) continue;
// Only a process (primary slot) carries a process exit code;
// sibling-thread exit values go through the join path.
if (processTable[i].primarySlot == i) {
processTable[i].exitCode = code;
}
break;
}
schedLock.Release();
}
void BlockOnPid(int pid) {
// If the target is already dead, return immediately
if (!IsAlive(pid)) return;