aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md28
-rw-r--r--include/gbos.inc4
-rw-r--r--src/kdata.asm11
-rw-r--r--src/proc.asm110
-rw-r--r--src/programs.asm54
-rw-r--r--src/sched.asm12
-rw-r--r--src/syscall.asm150
-rw-r--r--src/tasks.asm64
8 files changed, 353 insertions, 80 deletions
diff --git a/README.md b/README.md
index 3f6aab0..929cafe 100644
--- a/README.md
+++ b/README.md
@@ -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