diff options
| author | user <user@clank> | 2026-07-16 00:12:43 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-07-16 00:12:43 +0200 |
| commit | 19a2b63bab71b796ee456b3ae8bec1a2ab297158 (patch) | |
| tree | 49b1effd335d47e859002903caea337fea27e1b9 | |
| parent | kernel: implement exec() (diff) | |
| download | gbos-19a2b63bab71b796ee456b3ae8bec1a2ab297158.tar.gz gbos-19a2b63bab71b796ee456b3ae8bec1a2ab297158.tar.xz gbos-19a2b63bab71b796ee456b3ae8bec1a2ab297158.zip | |
kernel: complete the process lifecycle (exit/wait/reap + bank free list)
- exit(): zombie + status, reparent orphans to init, wake blocked parent,
schedule away forever (resources reclaimed by the reaper)
- wait(): scan for a zombie child; reap (free cart-RAM bank + PCB slot) and
return pid/code; block PS_BLOCKED + yield if children still live; $FF if none
- cart-RAM banks are now a free list (wBankUsed bitmap): AllocRamBank/FreeRamBank
- scheduler: FindNextReady returns carry when nothing runnable; yield idles
instead of blindly picking slot 0
- helpers: FindPcbByPid, ReparentToInit
- demo: init forks 3 workers -> exec -> exit(pid) -> wait/reap => wwwR2R3R4!
verified bank recycling with 6 workers over 3 banks (w2w3w4w5w6w7)
- README: document the lifecycle + syscall status
Diffstat (limited to '')
| -rw-r--r-- | README.md | 28 | ||||
| -rw-r--r-- | include/gbos.inc | 4 | ||||
| -rw-r--r-- | src/kdata.asm | 11 | ||||
| -rw-r--r-- | src/proc.asm | 110 | ||||
| -rw-r--r-- | src/programs.asm | 54 | ||||
| -rw-r--r-- | src/sched.asm | 12 | ||||
| -rw-r--r-- | src/syscall.asm | 150 | ||||
| -rw-r--r-- | src/tasks.asm | 64 |
8 files changed, 353 insertions, 80 deletions
@@ -45,13 +45,35 @@ User loads `C` = syscall number, args in `DE`/`B`, then `rst $30`. Return in `A` | # | name | status | |---|---------|--------| -| 0 | exit | ✅ marks zombie, reschedules | +| 0 | exit | ✅ zombie + reparent orphans + wake waiter | | 1 | fork | ✅ copies the 8 KiB bank, child returns 0 | | 3 | write | ✅ `DE`=buf `B`=len → serial console | | 6 | exec | ✅ `B`=program id → maps ROM text bank, jumps in | +| 7 | wait | ✅ blocks; reaps a zombie child → `A`=pid `B`=code | | 8 | getpid | ✅ | | 11| yield | ✅ cooperative switch | -| 2,4,5,7,9,10 | read/open/close/wait/kill/brk | ⛔ `ENOSYS` | +| 2,4,5,9,10 | read/open/close/kill/brk | ⛔ `ENOSYS` | + +### Process lifecycle (exit / wait / reaping) + +The classic Unix zombie/reap dance, adapted to banked memory: + +- **`exit(code)`** sets `PS_ZOMBIE` + status, **reparents** any children to init + (pid 1), **wakes** the parent if it's blocked in `wait()`, then schedules + away forever. Its resources are freed by the reaper, not here. +- **`wait()`** scans for a `PS_ZOMBIE` child. Found → **reap**: free its + cart-RAM bank (back to the allocator) and its PCB slot, return pid + code. + Children exist but none dead → set self `PS_BLOCKED` and yield, retry on wake. + No children → return `$FF` (`ECHILD`). +- The scheduler skips non-`READY` procs; `FindNextReady` returns carry when + nothing is runnable so `yield` just keeps the caller running (idle). +- **Cart-RAM banks are a free list** (`wBankUsed` bitmap): `AllocRamBank` / + `FreeRamBank`. Verified by running 6 workers through only 3 child banks + (`w2w3w4w5w6w7`) — each reaped bank is recycled by the next `fork`. + +Lifecycle demo (`tasks.asm`): init forks 3 workers, each `exec`s PROG_WORKER +(prints `w`, exits with its pid), init `wait`s and reaps all three → +`wwwR2R3R4!`. ### exec (`sys_exec`, src/proc.asm + programs.asm) @@ -131,7 +153,9 @@ A real gbos would render to VRAM; serial is the zero-VRAM debug channel. - [x] `fork`: copy parent's 8 KiB RAM bank → free bank, child returns 0 - [x] `exec`: point `PROC_ROMB` at a program in a ROM bank, reset stack, enter +- [x] `exit` / `wait` / zombie reaping + cart-RAM bank recycling (free list) - [ ] `exec` refinement: copy `.data` from ROM + zero `.bss` for RW globals +- [ ] a real shell program: `fork`+`exec`+`wait` driven from the serial console - [ ] Preemptive scheduling: real context save in `TimerISR` → `hSwitchTo` - [ ] Use the `$D000-$DFFF` u-area (SVBK) for per-process kernel state / kstack - [ ] `brk`/heap allocator inside the task bank (heap up, stack down, collision = ENOMEM) diff --git a/include/gbos.inc b/include/gbos.inc index e15f7cb..7919bd6 100644 --- a/include/gbos.inc +++ b/include/gbos.inc @@ -48,6 +48,7 @@ DEF KSTACK_TOP EQU $D000 ; kernel stack top (grows down into WRAM0) DEF MAX_PROCS EQU 8 DEF NUM_RAM_BANKS EQU 4 ; MBC5 cart RAM banks available (-r 3 = 32 KiB) DEF KSTACK_TOP2 EQU $D000 ; kernel stack top reused for syscalls (fork) +DEF INIT_PID EQU 1 ; pid of the init task (orphan reaper) ; process states DEF PS_FREE EQU 0 @@ -88,7 +89,6 @@ DEF SYS_MAX EQU 12 ; ----------------------------------------------------------------------------- ; Program table indices (see programs.asm). exec() takes one of these in B. ; ----------------------------------------------------------------------------- -DEF PROG_PING EQU 0 -DEF PROG_PONG EQU 1 +DEF PROG_WORKER EQU 0 ENDC diff --git a/src/kdata.asm b/src/kdata.asm index ba395f1..5b91030 100644 --- a/src/kdata.asm +++ b/src/kdata.asm @@ -21,8 +21,8 @@ wTmpSlot:: DS 1 wTmpPid:: DS 1 wTmpSP:: DS 2 -; cart-RAM bank allocator (bump; bank 0 taken by the init task) -wNextRamBank:: DS 1 +; cart-RAM bank allocator: 0 = free, 1 = in use. bank 0 is the init task's. +wBankUsed:: DS NUM_RAM_BANKS ; scratch used by sys_fork wForkUserSP:: DS 2 ; parent user SP at fork entry (= Uafter) @@ -31,3 +31,10 @@ wForkChildPid:: DS 1 wForkParentBank::DS 1 wForkChildBank:: DS 1 wCopyBuf:: DS 256 ; bank-copy bounce buffer (WRAM0, always mapped) + +; scratch used by sys_exit / sys_wait +wExitMyPid:: DS 1 +wExitParentPid:: DS 1 +wWaitMyPid:: DS 1 +wWaitRetPid:: DS 1 +wWaitRetCode:: DS 1 diff --git a/src/proc.asm b/src/proc.asm index 114cc9c..a2b1368 100644 --- a/src/proc.asm +++ b/src/proc.asm @@ -11,26 +11,116 @@ SECTION "proc", ROM0 ProcInit:: ld a, 1 ld [wNextPid], a - ld a, 1 ; bank 0 is the init task's; allocate from 1 up - ld [wNextRamBank], a + ; bank 0 belongs to the init task; the rest start free + ld a, 1 + ld [wBankUsed + 0], a + ld hl, wBankUsed + 1 + ld b, NUM_RAM_BANKS - 1 + xor a +.bclr + ld [hl+], a + dec b + jr nz, .bclr ret ; ----------------------------------------------------------------------------- -; AllocRamBank - bump-allocate a cart-RAM bank. out: A=bank, CF set if none. +; AllocRamBank - claim a free cart-RAM bank. out: A=bank, CF set if none. ; ----------------------------------------------------------------------------- AllocRamBank:: - ld a, [wNextRamBank] + ld hl, wBankUsed + ld c, 0 +.scan + ld a, [hl] + or a + jr z, .free + inc hl + inc c + ld a, c cp NUM_RAM_BANKS - jr nc, .none - ld b, a - inc a - ld [wNextRamBank], a - ld a, b + jr c, .scan + scf + ret +.free + ld a, 1 + ld [hl], a ; mark used + ld a, c and a ; clear carry ret -.none + +; ----------------------------------------------------------------------------- +; FreeRamBank - release a cart-RAM bank. in: A=bank. +; ----------------------------------------------------------------------------- +FreeRamBank:: + ld hl, wBankUsed + add l + ld l, a + ld a, h + adc 0 + ld h, a + xor a + ld [hl], a + ret + +; ----------------------------------------------------------------------------- +; FindPcbByPid - locate a live PCB by pid. +; in: A=pid out: DE=&PCB, CF set if not found. +; ----------------------------------------------------------------------------- +FindPcbByPid:: + ld b, a ; target pid + ld c, 0 +.l + ld a, c + call PcbPtr ; DE=&PCB[c] (preserves BC) + ld a, [de] + cp PS_FREE + jr z, .n + ld h, d + ld l, e + inc hl ; ->PROC_PID + ld a, [hl] + cp b + jr z, .found +.n + inc c + ld a, c + cp MAX_PROCS + jr c, .l scf ret +.found + and a ; clear carry (pids are >= 1) + ret + +; ----------------------------------------------------------------------------- +; ReparentToInit - give every live child of wExitMyPid to pid INIT_PID. +; ----------------------------------------------------------------------------- +ReparentToInit:: + ld c, 0 +.l + ld a, c + call PcbPtr ; DE=&PCB (preserves BC) + ld a, [de] + cp PS_FREE + jr z, .n + ld h, d + ld l, e + ld a, l + add PROC_PARENT + ld l, a + ld a, h + adc 0 + ld h, a ; HL=&PROC_PARENT + ld a, [wExitMyPid] + cp [hl] + jr nz, .n + ld a, INIT_PID + ld [hl], a +.n + inc c + ld a, c + cp MAX_PROCS + jr c, .l + ret ; ----------------------------------------------------------------------------- ; FindFreeSlot - scan the proc table for a PS_FREE slot. diff --git a/src/programs.asm b/src/programs.asm index a8d8b63..b5e4d8e 100644 --- a/src/programs.asm +++ b/src/programs.asm @@ -1,52 +1,36 @@ ; ============================================================================= ; programs.asm - userland program images (text-in-ROM model). ; -; Each program is linked to execute from the ROMX window ($4000-$7FFF) and -; lives in its own ROM bank. exec() maps the bank and jumps to the entry. -; -; To prove ROM banking really drives execution, both demo programs are ORG'd at -; the SAME address ($4000) in DIFFERENT banks - the only thing selecting which -; code runs is PROC_ROMB. They also read their message string out of the mapped -; bank, so the correct bank must be live when sys_write runs. -; -; Programs only ever talk to the kernel via `rst $30` (the trap + all kernel -; code live in ROM0, which is always mapped), so they never call kernel labels. +; Programs are linked to run from the ROMX window ($4000-$7FFF) and live in +; their own ROM banks. exec() maps the bank and jumps to the entry. Programs +; only talk to the kernel via `rst $30` (the trap + kernel live in ROM0, always +; mapped), so they never reference kernel labels. ; ============================================================================= INCLUDE "include/gbos.inc" -SECTION "prog_ping", ROMX[$4000], BANK[2] -ProgPing: -.loop - ld de, .msg - ld b, 1 - ld c, SYS_WRITE - rst $30 - ld c, SYS_YIELD - rst $30 - jr .loop -.msg - db "1" - -SECTION "prog_pong", ROMX[$4000], BANK[3] -ProgPong: -.loop +; ----------------------------------------------------------------------------- +; PROG_WORKER: announce with 'w', then exit() with a status equal to our pid. +; Exercises exec (runs from a ROM bank) + the exit/wait lifecycle. +; ----------------------------------------------------------------------------- +SECTION "prog_worker", ROMX[$4000], BANK[2] +ProgWorker: ld de, .msg ld b, 1 ld c, SYS_WRITE rst $30 - ld c, SYS_YIELD + ld c, SYS_GETPID rst $30 - jr .loop + ld b, a ; exit status = pid (so reaped codes are distinct) + ld c, SYS_EXIT + rst $30 ; never returns .msg - db "2" + db "w" ; ----------------------------------------------------------------------------- -; Program table (in ROM0, always mapped). Indexed by exec()'s program id. -; Entry layout: bank (2 bytes, MBC5 16-bit) then entry address (2 bytes). +; Program table (ROM0). Indexed by exec()'s program id. +; Entry: bank (2 bytes, MBC5 16-bit) then entry address (2 bytes). ; ----------------------------------------------------------------------------- SECTION "progtab", ROM0 ProgramTable:: - db LOW(BANK(ProgPing)), HIGH(BANK(ProgPing)) - dw ProgPing - db LOW(BANK(ProgPong)), HIGH(BANK(ProgPong)) - dw ProgPong + db LOW(BANK(ProgWorker)), HIGH(BANK(ProgWorker)) + dw ProgWorker diff --git a/src/sched.asm b/src/sched.asm index c871f7a..a540c49 100644 --- a/src/sched.asm +++ b/src/sched.asm @@ -31,13 +31,14 @@ SchedRunFirst:: ; caller when this task is eventually rescheduled. ; ----------------------------------------------------------------------------- SchedYield:: - call FindNextReady ; DE = &next PCB + call FindNextReady ; DE = &next PCB, CF if none runnable + ret c ; nobody else ready: keep running current call hSwitchTo ret ; ----------------------------------------------------------------------------- -; FindNextReady - round-robin from wSchedNext. out: DE = &PCB (updates cursor) -; Scaffold: assumes at least one PS_READY task exists. +; FindNextReady - round-robin from wSchedNext. +; out: DE = &PCB and CF clear on success; CF set if nothing is PS_READY. ; ----------------------------------------------------------------------------- FindNextReady:: ld a, [wSchedNext] @@ -56,14 +57,13 @@ FindNextReady:: pop af ; restore candidate slot dec b jr nz, .scan - ; nothing ready: fall back to slot 0 (idle behavior) - xor a - call PcbPtr + scf ; nothing runnable ret .found pop af ; A = chosen slot ld [wSchedNext], a call PcbPtr ; DE = &PCB + and a ; clear carry ret ; ----------------------------------------------------------------------------- diff --git a/src/syscall.asm b/src/syscall.asm index fd1b213..4b8771c 100644 --- a/src/syscall.asm +++ b/src/syscall.asm @@ -41,7 +41,7 @@ SyscallTable: dw sys_nosys ; 4 OPEN (TODO) dw sys_nosys ; 5 CLOSE (TODO) dw sys_exec ; 6 EXEC - dw sys_nosys ; 7 WAIT (TODO) + dw sys_wait ; 7 WAIT dw sys_getpid ; 8 GETPID dw sys_nosys ; 9 KILL (TODO) dw sys_nosys ; 10 BRK (TODO) @@ -91,17 +91,21 @@ sys_yield: ret ; ----------------------------------------------------------------------------- -; sys_exit(code=B) -- mark zombie and schedule away forever. +; sys_exit(code=B) -- become a zombie awaiting reap; never returns. +; - store exit code, set PS_ZOMBIE +; - reparent any children to init (so they can still be waited on) +; - wake our parent if it is blocked in wait() +; - schedule away forever (RAM bank + PCB slot are freed by the reaper) ; ----------------------------------------------------------------------------- sys_exit: - ; mark current PCB as zombie, store exit code + ; PROC_STATE = PS_ZOMBIE ld a, [wCurProc] ld l, a ld a, [wCurProc+1] - ld h, a ; HL = &PCB + ld h, a ld a, PS_ZOMBIE - ld [hl], a ; PROC_STATE - ; PROC_EXIT is at offset PROC_EXIT + ld [hl], a + ; PROC_EXIT = B ld a, [wCurProc] add PROC_EXIT ld l, a @@ -109,9 +113,133 @@ sys_exit: adc 0 ld h, a ld a, b - ld [hl], a ; exit code - ; never return to this task + ld [hl], a + ; stash my pid + parent pid + ld a, [wCurProc] + add PROC_PID + ld l, a + ld a, [wCurProc+1] + adc 0 + ld h, a + ld a, [hl] + ld [wExitMyPid], a + ld a, [wCurProc] + add PROC_PARENT + ld l, a + ld a, [wCurProc+1] + adc 0 + ld h, a + ld a, [hl] + ld [wExitParentPid], a + ; hand any children to init + call ReparentToInit + ; wake our parent if it's blocked in wait() + ld a, [wExitParentPid] + call FindPcbByPid ; DE=&parent PCB, CF if gone + jr c, .gone + ld a, [de] + cp PS_BLOCKED + jr nz, .gone + ld a, PS_READY + ld [de], a +.gone + ; never run this process again +.dead call SchedYield - ; if we somehow come back (no other ready task), spin -.halt - jr .halt + jr .dead + +; ----------------------------------------------------------------------------- +; sys_wait() -- reap a zombie child. +; out: A = child pid, B = exit code; or A = $FF if we have no children. +; Blocks (PS_BLOCKED) until a child becomes a zombie. +; ----------------------------------------------------------------------------- +sys_wait: + ld a, [wCurProc] + add PROC_PID + ld l, a + ld a, [wCurProc+1] + adc 0 + ld h, a + ld a, [hl] + ld [wWaitMyPid], a +.retry + ld c, 0 ; slot cursor + ld b, 0 ; "any child exists" flag +.scan + ld a, c + call PcbPtr ; DE=&PCB[c] (preserves BC) + ld a, [de] + cp PS_FREE + jr z, .next + ; parent == my pid ? + ld h, d + ld l, e + ld a, l + add PROC_PARENT + ld l, a + ld a, h + adc 0 + ld h, a + ld a, [wWaitMyPid] + cp [hl] + jr nz, .next + ld b, 1 ; we have at least one child + ld a, [de] ; state (DE still = base) + cp PS_ZOMBIE + jr z, .reap +.next + inc c + ld a, c + cp MAX_PROCS + jr c, .scan + ; finished scan: any children? + ld a, b + or a + jr z, .nochild + ; children exist but none are zombies: block and yield, then retry + ld a, [wCurProc] + ld l, a + ld a, [wCurProc+1] + ld h, a + ld a, PS_BLOCKED + ld [hl], a + call SchedYield + ld a, [wCurProc] + ld l, a + ld a, [wCurProc+1] + ld h, a + ld a, PS_READY + ld [hl], a + jr .retry +.reap + ; DE = &zombie child PCB + ld h, d + ld l, e + inc hl ; ->PROC_PID + ld a, [hl] + ld [wWaitRetPid], a + ld a, e + add PROC_EXIT + ld l, a + ld a, d + adc 0 + ld h, a + ld a, [hl] + ld [wWaitRetCode], a + ld a, e + add PROC_RAMB + ld l, a + ld a, d + adc 0 + ld h, a + ld a, [hl] + call FreeRamBank ; recycle the child's cart-RAM bank + ld a, PS_FREE + ld [de], a ; free the PCB slot + ld a, [wWaitRetCode] + ld b, a + ld a, [wWaitRetPid] + ret +.nochild + ld a, $FF + ret diff --git a/src/tasks.asm b/src/tasks.asm index 0b5276d..028d074 100644 --- a/src/tasks.asm +++ b/src/tasks.asm @@ -1,27 +1,67 @@ ; ============================================================================= -; tasks.asm - the init task demonstrates fork() + exec(). +; tasks.asm - init exercises the full process lifecycle. ; -; init forks; the parent exec()s PROG_PING (ROM bank 2), the child exec()s -; PROG_PONG (ROM bank 3). Both program images are ORG'd at $4000, so the output -; "1212..." proves each process runs from its own ROM bank via PROC_ROMB - not -; from shared ROM0. +; init fork()s three children; each child exec()s PROG_WORKER, which prints 'w' +; and exit()s with its pid as the status. init then wait()s in a loop, printing +; 'R' + the reaped pid digit for each child, until wait() returns $FF (no more +; children), then prints '!' and idles. This drives fork + exec + exit + wait + +; zombie reaping + cart-RAM bank recycling. +; +; The fork counter lives at $A000 (bottom of init's own RAM bank) because the +; register file does not survive a fork() call. ; ============================================================================= INCLUDE "include/gbos.inc" +DEF ASCII0 EQU $30 ; '0', added to a pid to print it as a digit +DEF CH_R EQU $52 ; 'R' +DEF CH_BANG EQU $21 ; '!' + SECTION "tasks", ROM0 TaskInit:: + ld a, 3 + ld [$A000], a ; children left to spawn + +.forkloop ld c, SYS_FORK - rst $30 ; A = child pid (parent) or 0 (child) + rst $30 or a - jr z, .child + jr z, .child ; A==0 -> we are the child + ; parent: one more child spawned + ld a, [$A000] + dec a + ld [$A000], a + jr nz, .forkloop -.parent - ld b, PROG_PING - ld c, SYS_EXEC - rst $30 ; never returns +.waitloop + ld c, SYS_WAIT + rst $30 ; A = reaped pid (B = code), or $FF if none left + cp $FF + jr z, .done + ; print 'R' then the reaped pid as a digit + push af + ld a, CH_R + ld [$FF01], a + ld a, $81 + ld [$FF02], a + pop af + add ASCII0 + ld [$FF01], a + ld a, $81 + ld [$FF02], a + jr .waitloop + +.done + ld a, CH_BANG + ld [$FF01], a + ld a, $81 + ld [$FF02], a +.idle + ld c, SYS_YIELD + rst $30 + jr .idle .child - ld b, PROG_PONG + ld b, PROG_WORKER ld c, SYS_EXEC rst $30 ; never returns |
