Files
velzieandClaude Opus 5 a4ca7c48ee Sessions, process groups and pid 1: run a real init system
BusyBox init now boots a userspace under gex. It parses an /etc/inittab,
runs a sysinit script that mounts its own /proc and /tmp, starts services
in their own sessions, adopts a daemon whose parent has exited, and powers
the machine off at the end.

Most of what was missing was not hard, only absent. The kernel had no
setsid, setpgid, getpgid or getsid; getppid returned a literal zero; mount,
reboot, sync, prctl and the set*id family were unimplemented; and socket
answered ENOSYS, which sends a caller looking for a fallback rather than
telling it there is no networking.

The structural piece is crates/gex-kernel/src/proc.rs, a process table
shared by every process. Three things need it. Pids have to be unique
system-wide, and each forked kernel used to carry its own counter offset
from its parent's by a thousand -- fine for a shallow tree, not for an init
that respawns for the life of the machine. Orphans have to be reparented to
pid 1, which a parent-owned child list cannot express, because the exiting
process would have to reach into init's kernel behind another thread's
lock. And groups and sessions span processes by definition: kill(-pgid)
asks about processes the caller may not be related to.

Cross-process signals go through the table too. A sender cannot deliver --
the target's signal state is behind its own lock and it may be running,
blocked in the host, or between the two -- so the sender leaves the signal
pending and the target collects it at its next syscall, which is where a
halt is noticed as well. reboot() from any process stops every process,
rather than only the one that called it.

Two bugs surfaced on the way. kill(-pgid) signalled the caller even when
the caller was in another group, because the syscall restated the
membership rule instead of asking the table; that turned the group-kill
test into a SIGTERM to gex itself. And a forked child's /proc still pointed
at its parent's state, because the VFS mount was cloned wholesale, so every
child read its ancestor's pid, maps and command line from /proc/self. The
native host also carried a second, staler copy of ProcSource with PPid
hardcoded to zero; there is one now.

tests/integration/programs/init.c is a pid 1 built for the suite: its own
session, a service in another, an orphan it has to adopt, and a process
group it stops with one signal. Children report through exit status rather
than printing, so the transcript does not depend on scheduling. 89 stages
pass, 0 fail. tests/real-programs/busybox-init.sh does the same with an
init nobody wrote for gex.

Also here: bench/, a CPU-time A/B harness. Wall time on this machine swings
8-15% between rounds, which is wider than most of what is being measured.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01KcidnrZ5aLqKoBJBqdjx9d
2026-08-05 23:58:54 -04:00

80 lines
2.8 KiB
C

/* A small register VM, for the shape an interpreter has.
*
* Dispatch is a computed goto, so the hot loop is an indirect branch that
* gex cannot see the target of -- which is what Lua, CPython and every other
* bytecode interpreter look like from the translator's side, and a shape that
* defeats anything relying on statically known successors.
*/
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
enum { OP_HALT, OP_LOADI, OP_MOV, OP_ADD, OP_SUB, OP_MUL, OP_XOR, OP_SHR,
OP_JMPNZ, OP_JMP, OP_LOAD, OP_STORE, OP_CMPLT, OP_NOPS };
struct insn { uint8_t op, a, b, c; int32_t imm; };
#define MEMSZ 4096
static uint64_t run(const struct insn *code, uint64_t iterations) {
static const void *table[OP_NOPS] = {
&&halt, &&loadi, &&mov, &&add, &&sub, &&mul, &&xor_, &&shr,
&&jmpnz, &&jmp, &&load, &&store, &&cmplt,
};
uint64_t r[16] = {0}, mem[MEMSZ] = {0};
r[15] = iterations;
const struct insn *ip = code;
#define NEXT() goto *table[(++ip)->op]
goto *table[ip->op];
halt:
return r[0] ^ r[1] ^ mem[7] ^ mem[MEMSZ - 1];
loadi:
r[ip->a] = (uint64_t)(int64_t)ip->imm; NEXT();
mov:
r[ip->a] = r[ip->b]; NEXT();
add:
r[ip->a] = r[ip->b] + r[ip->c]; NEXT();
sub:
r[ip->a] = r[ip->b] - r[ip->c]; NEXT();
mul:
r[ip->a] = r[ip->b] * r[ip->c]; NEXT();
xor_:
r[ip->a] = r[ip->b] ^ r[ip->c]; NEXT();
shr:
r[ip->a] = r[ip->b] >> (ip->imm & 63); NEXT();
load:
r[ip->a] = mem[r[ip->b] % MEMSZ]; NEXT();
store:
mem[r[ip->a] % MEMSZ] = r[ip->b]; NEXT();
cmplt:
r[ip->a] = r[ip->b] < r[ip->c]; NEXT();
jmpnz:
if (r[ip->a]) { ip = code + ip->imm; goto *table[ip->op]; }
NEXT();
jmp:
ip = code + ip->imm;
goto *table[ip->op];
}
int main(int argc, char **argv) {
uint64_t n = argc > 1 ? (uint64_t)atol(argv[1]) : 3000000;
/* h = mix(h, i); store; i-- ; loop while i */
static struct insn code[] = {
{OP_LOADI, 0, 0, 0, 1}, /* 0: h = 1 */
{OP_LOADI, 2, 0, 0, 31}, /* 1: k = 31 */
{OP_LOADI, 3, 0, 0, 1}, /* 2: one = 1 */
{OP_MUL, 0, 0, 2, 0}, /* 3: h *= k */
{OP_XOR, 0, 0, 15, 0}, /* 4: h ^= i */
{OP_SHR, 4, 0, 0, 13}, /* 5: t = h >> 13 */
{OP_XOR, 0, 0, 4, 0}, /* 6: h ^= t */
{OP_STORE, 0, 0, 0, 0}, /* 7: mem[h] = h */
{OP_LOAD, 5, 0, 0, 0}, /* 8: u = mem[h] */
{OP_ADD, 1, 1, 5, 0}, /* 9: acc += u */
{OP_SUB, 15, 15, 3, 0}, /* 10: i -= 1 */
{OP_JMPNZ, 15, 0, 0, 3}, /* 11: if i goto 3 */
{OP_HALT, 0, 0, 0, 0},
};
printf("%llu\n", (unsigned long long)run(code, n));
return 0;
}