diff options
| author | user <user@clank> | 2026-07-15 23:54:13 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-07-15 23:54:13 +0200 |
| commit | ab3ebbe227c72b745a46f9233fbfbb27477586b8 (patch) | |
| tree | d71f87605d17c5653f34ebcdef3941f898cd5b91 | |
| parent | gbos scaffold: working cooperative microkernel (verified ABAB in emulator) (diff) | |
| download | gbos-ab3ebbe227c72b745a46f9233fbfbb27477586b8.tar.gz gbos-ab3ebbe227c72b745a46f9233fbfbb27477586b8.tar.xz gbos-ab3ebbe227c72b745a46f9233fbfbb27477586b8.zip | |
kernel: implement fork()
- sys_fork: switch to a kernel stack, bump-allocate a cart-RAM bank, copy the
parent's 8 KiB bank via a WRAM bounce buffer, plant a switch-in frame so the
child returns 0 to the post-fork PC, fill the child PCB (shared ROM text)
- add AllocRamBank (bump allocator) + FindFreeSlot helpers
- demo: init forks; parent prints P, child prints C (PCPC...); nested fork
gives three distinct pids (123...)
- README: document the fork mechanism, mark syscall status
Diffstat (limited to '')
| -rw-r--r-- | README.md | 24 | ||||
| -rw-r--r-- | include/gbos.inc | 2 | ||||
| -rw-r--r-- | src/boot.asm | 12 | ||||
| -rw-r--r-- | src/kdata.asm | 11 | ||||
| -rw-r--r-- | src/proc.asm | 211 | ||||
| -rw-r--r-- | src/syscall.asm | 2 | ||||
| -rw-r--r-- | src/tasks.asm | 40 |
7 files changed, 273 insertions, 29 deletions
@@ -46,10 +46,30 @@ User loads `C` = syscall number, args in `DE`/`B`, then `rst $30`. Return in `A` | # | name | status | |---|---------|--------| | 0 | exit | ✅ marks zombie, reschedules | +| 1 | fork | ✅ copies the 8 KiB bank, child returns 0 | | 3 | write | ✅ `DE`=buf `B`=len → serial console | | 8 | getpid | ✅ | | 11| yield | ✅ cooperative switch | -| 1,2,4,5,6,7,9,10 | fork/read/open/close/exec/wait/kill/brk | ⛔ `ENOSYS` | +| 2,4,5,6,7,9,10 | read/open/close/exec/wait/kill/brk | ⛔ `ENOSYS` | + +### fork (`sys_fork`, src/proc.asm) + +The interesting syscall. It enters on the parent's user stack (which lives in +the `$A000-$BFFF` cart-RAM window), so before it can swap that window it +**switches to a kernel stack in WRAM0**. Then it: + +1. allocates a free PCB slot + a fresh cart-RAM bank (bump allocator), +2. copies the parent's whole 8 KiB bank → the child bank, 256 bytes at a time + through a WRAM bounce buffer (only one cart-RAM bank is visible at once), +3. plants a **switch-in frame** in the child at `Uafter-8` (`Uafter` = parent + SP at entry): the copied return address is already there, so it just zeroes + the saved `hl/de/bc/af` slots — the child resumes at the post-`fork` PC with + `A=0`, +4. fills the child PCB (shared ROM text bank, new RAM bank, parent as PPID), +5. restores the parent stack and returns the child pid. + +Verified: `init` forks a child (P/C alternate on serial); nested forks yield +three independent processes with distinct pids (`123123...`). ## The context switch (`src/hram.asm`) @@ -91,7 +111,7 @@ A real gbos would render to VRAM; serial is the zero-VRAM debug channel. ## Roadmap -- [ ] `fork`: copy parent's 8 KiB RAM bank → free bank, dup u-area, child returns 0 +- [x] `fork`: copy parent's 8 KiB RAM bank → free bank, child returns 0 - [ ] `exec`: point `PROC_ROMB` at a flashed program, reset RAM-bank layout - [ ] Preemptive scheduling: real context save in `TimerISR` → `hSwitchTo` - [ ] Use the `$D000-$DFFF` u-area (SVBK) for per-process kernel state / kstack diff --git a/include/gbos.inc b/include/gbos.inc index c22344a..0727b02 100644 --- a/include/gbos.inc +++ b/include/gbos.inc @@ -46,6 +46,8 @@ DEF KSTACK_TOP EQU $D000 ; kernel stack top (grows down into WRAM0) ; Process table ; ----------------------------------------------------------------------------- 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) ; process states DEF PS_FREE EQU 0 diff --git a/src/boot.asm b/src/boot.asm index 513c392..73ea860 100644 --- a/src/boot.asm +++ b/src/boot.asm @@ -50,18 +50,12 @@ KernelInit: call CopyHramCode ; move context-switch trampoline into HRAM call ProcInit ; wipe process table - ; --- spawn the two demo tasks (text-in-ROM model) --- - ; task A: entry TaskA, RAM bank 0, u-area WRAM bank 1 - ld hl, TaskA - ld b, 0 ; RAM bank + ; --- spawn the init task (pid 1); it fork()s the rest --- + ld hl, TaskInit + ld b, 0 ; cart-RAM bank 0 ld c, 1 ; WRAM u-area bank call ProcCreate - ld hl, TaskB - ld b, 1 - ld c, 2 - call ProcCreate - call SchedInit ; pick first task, set wCurProc ; interrupts: enable timer for preemption diff --git a/src/kdata.asm b/src/kdata.asm index efc780a..ba395f1 100644 --- a/src/kdata.asm +++ b/src/kdata.asm @@ -20,3 +20,14 @@ wTmpWramB:: DS 1 wTmpSlot:: DS 1 wTmpPid:: DS 1 wTmpSP:: DS 2 + +; cart-RAM bank allocator (bump; bank 0 taken by the init task) +wNextRamBank:: DS 1 + +; scratch used by sys_fork +wForkUserSP:: DS 2 ; parent user SP at fork entry (= Uafter) +wForkChildSlot:: DS 1 +wForkChildPid:: DS 1 +wForkParentBank::DS 1 +wForkChildBank:: DS 1 +wCopyBuf:: DS 256 ; bank-copy bounce buffer (WRAM0, always mapped) diff --git a/src/proc.asm b/src/proc.asm index 762b30e..058881f 100644 --- a/src/proc.asm +++ b/src/proc.asm @@ -11,6 +11,48 @@ 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 + ret + +; ----------------------------------------------------------------------------- +; AllocRamBank - bump-allocate a cart-RAM bank. out: A=bank, CF set if none. +; ----------------------------------------------------------------------------- +AllocRamBank:: + ld a, [wNextRamBank] + cp NUM_RAM_BANKS + jr nc, .none + ld b, a + inc a + ld [wNextRamBank], a + ld a, b + and a ; clear carry + ret +.none + scf + ret + +; ----------------------------------------------------------------------------- +; FindFreeSlot - scan the proc table for a PS_FREE slot. +; out: A=slot, DE=&PCB, CF set if the table is full. +; ----------------------------------------------------------------------------- +FindFreeSlot:: + ld c, 0 +.l + ld a, c + call PcbPtr ; DE = &PCB[c] + ld a, [de] + cp PS_FREE + jr z, .ok + inc c + ld a, c + cp MAX_PROCS + jr c, .l + scf + ret +.ok + ld a, c + and a ; clear carry ret ; ----------------------------------------------------------------------------- @@ -157,3 +199,172 @@ ProcSetupStack:: ; exec: point PROC_ROMB at a flashed program image, reset RAM-bank layout ; (copy .data from ROM, zero .bss, fresh heap/stack), longjmp in. ; ----------------------------------------------------------------------------- + +; ============================================================================= +; sys_fork() - duplicate the current process. +; out: A = child pid in the parent, 0 in the child, $FF on failure. +; +; Runs on the parent's user stack at entry (SP = Uafter, pointing at the rst +; return address). We switch to a kernel stack in WRAM0 so we're free to swap +; the $A000 cart-RAM window (the parent's user stack lives there), copy the +; parent's 8 KiB bank into a freshly allocated child bank via a WRAM bounce +; buffer, then plant a switch-in frame in the child so it "returns from fork" +; with A=0 to the same user PC. +; ============================================================================= +sys_fork:: + ; capture parent user SP (= Uafter) + ld hl, sp+0 + ld a, l + ld [wForkUserSP], a + ld a, h + ld [wForkUserSP+1], a + ld sp, KSTACK_TOP2 ; move off the cart-RAM stack + + call FindFreeSlot ; A=slot, DE=&child PCB + jp c, .fail + ld [wForkChildSlot], a + call AllocRamBank ; A=child cart-RAM bank + jp c, .fail + ld [wForkChildBank], a + + ; remember the parent's bank so we can restore the window afterwards + ld a, [wCurProc] + add PROC_RAMB + ld l, a + ld a, [wCurProc+1] + adc 0 + ld h, a + ld a, [hl] + ld [wForkParentBank], a + + ; ---- copy parent bank -> child bank, 32 pages of 256 bytes ---- + ld c, 0 ; page index 0..31 +.page + ld a, [wForkParentBank] + ld [MBC5_RAMB], a ; map parent bank + ld a, $A0 + add c + ld h, a + ld l, 0 ; HL = $A000 + c*256 + ld de, wCopyBuf + ld b, 0 ; 256 iterations (b wraps 0->255..->0) +.rd + ld a, [hl+] + ld [de], a + inc de + dec b + jr nz, .rd + ld a, [wForkChildBank] + ld [MBC5_RAMB], a ; map child bank + ld a, $A0 + add c + ld h, a + ld l, 0 + ld de, wCopyBuf + ld b, 0 +.wr + ld a, [de] + ld [hl+], a + inc de + dec b + jr nz, .wr + inc c + ld a, c + cp 32 + jr c, .page + ; child bank is currently mapped + + ; ---- plant the child's switch-in frame at Uafter-8 (in child bank) ---- + ; hSwitchTo restore pops hl,de,bc,af then rets; we zero those 8 bytes and + ; leave the copied return address (retU) at Uafter untouched, so the child + ; resumes at the post-fork PC with A=0. + ld a, [wForkUserSP] + sub 8 + ld l, a + ld a, [wForkUserSP+1] + sbc 0 + ld h, a ; HL = Uafter-8 + xor a + ld b, 8 +.zframe + ld [hl+], a + dec b + jr nz, .zframe + + ; restore the parent's bank into the window (parent stack valid again) + ld a, [wForkParentBank] + ld [MBC5_RAMB], a + + ; ---- fill the child PCB ---- + ld a, [wForkChildSlot] + call PcbPtr ; DE = &child PCB + ld h, d + ld l, e ; HL walks child PCB + ld a, PS_READY + ld [hl+], a ; STATE + ld a, [wNextPid] + ld [hl+], a ; PID + ld [wForkChildPid], a + inc a + ld [wNextPid], a + ld a, [wForkUserSP] ; PROC_SP = Uafter-8 + sub 8 + ld [hl+], a + ld a, [wForkUserSP+1] + sbc 0 + ld [hl+], a + ; PROC_ROMB = parent ROMB (shared read-only text) + ld a, [wCurProc] + add PROC_ROMB + ld e, a + ld a, [wCurProc+1] + adc 0 + ld d, a ; DE = &parent.PROC_ROMB + ld a, [de] + ld [hl+], a + inc de + ld a, [de] + ld [hl+], a + ; PROC_RAMB = child bank + ld a, [wForkChildBank] + ld [hl+], a + ; PROC_WRAMB = parent WRAMB (u-area shared for now) + ld a, [wCurProc] + add PROC_WRAMB + ld e, a + ld a, [wCurProc+1] + adc 0 + ld d, a + ld a, [de] + ld [hl+], a + ; PROC_PARENT = parent pid + ld a, [wCurProc] + add PROC_PID + ld e, a + ld a, [wCurProc+1] + adc 0 + ld d, a + ld a, [de] + ld [hl+], a + ; PROC_EXIT = 0 + xor a + ld [hl], a + + ; ---- return to the parent with child pid ---- + ld a, [wForkUserSP] + ld l, a + ld a, [wForkUserSP+1] + ld h, a + ld sp, hl ; back on the parent's user stack + ld a, [wForkChildPid] + ret + +.fail + ; restore parent user stack and report failure + ld a, [wForkUserSP] + ld l, a + ld a, [wForkUserSP+1] + ld h, a + ld sp, hl + ld a, $FF + ret diff --git a/src/syscall.asm b/src/syscall.asm index 71e1f2b..ca2857c 100644 --- a/src/syscall.asm +++ b/src/syscall.asm @@ -35,7 +35,7 @@ SyscallTrap:: ; ----------------------------------------------------------------------------- SyscallTable: dw sys_exit ; 0 - dw sys_nosys ; 1 FORK (TODO) + dw sys_fork ; 1 FORK dw sys_nosys ; 2 READ (TODO) dw sys_write ; 3 WRITE dw sys_nosys ; 4 OPEN (TODO) diff --git a/src/tasks.asm b/src/tasks.asm index 1f84142..459ccb6 100644 --- a/src/tasks.asm +++ b/src/tasks.asm @@ -1,33 +1,39 @@ ; ============================================================================= -; tasks.asm - two demo userland tasks (text-in-ROM model). -; Each loops: write its letter to the console, then yield. This exercises the -; whole pipeline: syscall trap, console, and the HRAM context switch swapping -; RAM banks / SVBK between tasks. +; tasks.asm - the init task demonstrates fork(). +; init forks once; the parent loops printing 'P', the child loops printing 'C'. +; Both run the same ROM text, diverging on fork()'s return value (0 in child). +; Exercises: syscall trap, fork (8 KiB bank copy + child frame), scheduler, +; and the HRAM context switch swapping cart-RAM banks per task. ; ============================================================================= INCLUDE "include/gbos.inc" SECTION "tasks", ROM0 -TaskA:: -.loop - ld de, .msg - ld b, 1 ; len +TaskInit:: + ld c, SYS_FORK + rst $30 ; A = child pid (parent) or 0 (child) + or a + jr z, .child + +.parent + ld de, .pmsg + ld b, 1 ld c, SYS_WRITE rst $30 ld c, SYS_YIELD rst $30 - jr .loop -.msg - db "A" + jr .parent -TaskB:: -.loop - ld de, .msg +.child + ld de, .cmsg ld b, 1 ld c, SYS_WRITE rst $30 ld c, SYS_YIELD rst $30 - jr .loop -.msg - db "B" + jr .child + +.pmsg + db "P" +.cmsg + db "C" |
