Process Management — Architecture Plan¶
Overview¶
A preemptive process management system for the Asuro kernel. Processes execute on the kernel's main thread (replacing the idle HLT loop). The 1024 Hz timer interrupt (IRQ0) preempts the running process, dispatches all existing kernel hooks (graphics, USB, network, BIOS tick, etc.) as normal, then the scheduler decides whether to resume the same process or context-switch to another. When no process is runnable, the CPU idles via HLT. This lays the groundwork for running WASM VM instances as first-class processes.
Key Principle: Timer Hooks Are Untouched¶
All existing TMR_0_ISR hooks (bios_data_area.tick_update, graphicsrefresh, usbhotplug, TCP timers, etc.) continue to be dispatched identically on every IRQ0. The context switch happens after hooks have run, not instead of them.
Design Decisions¶
| Decision | Choice | Rationale |
|---|---|---|
| Scheduling model | Preemptive (timer-driven context switch) | Main thread is wasted on HLT; preemption gives true concurrency; processes can run real code between interrupts |
| Context switch mechanism | IRQ0 saves/restores per-process kernel stack (ESP swap) | Single-stack-swap is the simplest correct approach for ring-0; no TSS task-switching needed |
| Per-process stack | 8 KB kalloc'd kernel stack per process |
Large enough for nested calls + interrupt frames; small enough for many processes |
| Idle task | The original kernel.yield() HLT loop |
When no process is runnable, scheduler returns the idle ESP; IRET resumes the HLT loop |
| Timer hooks | Unchanged — dispatched every IRQ0 before scheduling | Zero impact on existing hook-based subsystems (graphics, USB, network, BIOS) |
| Timer source | 1024 Hz TMR_0_ISR via IRQ0 (ISR_32) | Proven; all hooks already registered here |
| Process table | DList of PProcessContext pointers |
Dynamic, contiguous; matches TCP/TCB pattern; avoids static array bloat |
| Process IDs | rand32() |
Already used for TCP ISN; simple, collision-unlikely for ≤65535 processes |
| StdIO model | Per-process POutBuf triple |
Extends existing stdio pattern; vterminal already knows how to render POutBuf |
| Resource bindings | Per-process hook DList | Allows sockets, timers, etc. to be bound to a process and cleaned up on destroy |
| Backwards compat | Legacy commands wrapped as processes | Existing TCommandMethod callbacks run as a process entry point, finish in one quantum |
| Re-entrancy guard | CLI/STI spinlocks on kalloc/kfree and shared state |
Prevents heap corruption when process is preempted mid-allocation |
| Unit location | src/processmanager.pas + src/contextswitcher.pas |
processmanager owns the table & API; contextswitcher owns the assembly stub |
Data Structures (new src/include/proctypes.pas)¶
Process State Enum¶
TProcessState = (
psCreated, { Allocated, stack prepared, not yet scheduled }
psRunning, { Currently executing on the main thread }
psReady, { Runnable but not the current process }
psSuspended, { Paused — will not be scheduled }
psAwaiting, { Blocked on I/O or event — will not be scheduled }
psFinishing, { Marked for cleanup — destroy runs next schedule }
psFinished, { Terminal state — safe to reap }
psError { Unrecoverable error — safe to reap }
);
System Message Enum¶
TProcessSysMsg = (
smNone, { No message }
smTerminate, { Graceful shutdown requested }
smKill, { Immediate forced shutdown }
smSuspend, { Pause execution }
smResume, { Resume from suspended }
smInput, { New data available on stdin }
smChildExited, { A child process has exited }
smCustom { User-defined message (payload in MsgData) }
);
Process Entry Point¶
{ Every process is a procedure that receives its own context pointer.
When this procedure returns, the process is automatically moved to psFinished. }
TProcessEntryPoint = procedure(ctx : PProcessContext);
Resource Binding¶
TResourceKind = (
rkSocket, { TCP/UDP socket }
rkTimer, { A timer hook }
rkFileHandle, { VFS file descriptor }
rkCustom { Arbitrary pointer }
);
PResourceBinding = ^TResourceBinding;
TResourceBinding = record
Kind : TResourceKind;
Handle : void; { The resource pointer (PTCPSocket, etc.) }
Cleanup : pointer; { Optional cleanup procedure(handle: void) }
end;
Process Context¶
const
PROCESS_STACK_SIZE = 8192; { 8 KB per-process kernel stack }
PProcessContext = ^TProcessContext;
TProcessContext = record
{ Identity }
ProcessID : uint32; { Unique ID via rand32() }
Name : array[0..31] of char; { Human-readable name }
ParentID : uint32; { PID of parent (0 = kernel) }
{ State }
State : TProcessState;
ExitCode : uint32;
{ StdIO — per-process I/O buffers }
StdIn : POutBuf; { Input buffer (written to by terminal/parent) }
StdOut : POutBuf; { Output buffer (read by terminal for display) }
StdErr : POutBuf; { Error buffer (read by terminal for display) }
{ Entry point }
EntryPoint : TProcessEntryPoint; { The procedure that IS the process }
{ Context switch state }
SavedESP : uint32; { Saved stack pointer (set by context switch) }
StackBase : pointer; { Base of kalloc'd stack (for kfree on reap) }
StackTop : uint32; { Top of stack (StackBase + PROCESS_STACK_SIZE) }
{ Scheduling }
Priority : uint8; { 1 = low, 255 = high; affects quantum length }
Quantum : uint16; { Ticks before preemption (derived from Priority) }
TicksUsed : uint16; { Ticks consumed this quantum }
{ System messages }
PendingMsg : TProcessSysMsg; { Next message to deliver }
MsgData : void; { Optional message payload }
{ Resource bindings }
Resources : void; { PDList of TResourceBinding }
{ User-defined state }
Local : void; { Arbitrary pointer (PWASMContext, etc.) }
end;
Process Lifecycle¶
processmanager.create()
│
▼
┌─────────────┐
│ psCreated │ Context + stack allocated, fake interrupt frame pushed,
└──────┬───────┘ added to process table as psReady
│
│ Scheduler selects this process → context switch
│ IRET "returns" into process_trampoline → calls EntryPoint(ctx)
▼
┌─────────────┐
│ psRunning │◄──────────────────────┐
└──────┬───────┘ │
│ Process code executes on │
│ the main thread; preempted │
│ by IRQ0 every quantum │
│ │
├─ process calls proc_yield() │
│ → state set to psAwaiting ────┤ (resumed when smResume/smInput received)
│ │
├─ process calls proc_suspend() │
│ → state set to psSuspended ───┘ (resumed via smResume)
│
│ Process returns from EntryPoint (or calls proc_exit)
│ → trampoline sets state to psFinished
▼
┌─────────────┐
│ psFinished │ Reaped by processmanager.reap():
└─────────────┘ unbindAllResources, kfree stack, kfree context
Lifecycle Rules¶
- A process is a procedure.
EntryPoint(ctx)runs as actual code on the main thread. When it returns, the process is done. - Preemption is transparent. The process does not need to yield or tick — IRQ0 preempts it automatically and the scheduler may switch to another process.
- The process can cooperate by calling
proc_yield()(release remaining quantum),proc_await()(block until event), orproc_suspend()(pause until explicit resume). - External actors send system messages via
processmanager.sendMessage(). Messages are delivered as a flag check — the process can pollctx^.PendingMsgor ignore it. - smKill is the exception: the process manager forcibly moves state to
psFinishedand reaps on the next scheduling pass (the process code is abandoned, stack freed). - smTerminate sets a flag the process should poll. Well-behaved processes check
ctx^.PendingMsgperiodically and return cleanly.
Context Switcher (contextswitcher.pas)¶
The heart of the preemptive model. This unit replaces the generic ISR_32 (IRQ0) handler with a custom assembly stub that performs the context switch.
How It Works¶
Current flow (unchanged for hooks):
IRQ0 fires → CPU pushes EFLAGS, CS, EIP onto current stack
→ Enters ISR_32 handler
→ TMR_0_ISR hooks are dispatched (graphics, USB, BIOS tick, TCP, etc.)
New flow (added after hooks):
→ Save current ESP into CurrentProcess^.SavedESP
→ Call scheduler.pickNext() → returns PProcessContext (or nil for idle)
→ Load new process's SavedESP into ESP
→ EOI ($20 → port $20)
→ POPAD + POP segment regs + IRET
→ CPU resumes executing the new (or same) process
Assembly Stub (pseudocode)¶
context_switch_isr:
; --- Save interrupted process's registers ---
pushad ; EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI
push ds
push es
push fs
push gs
mov ax, $10 ; Kernel data segment
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
; --- Save current ESP ---
mov eax, [current_process] ; PProcessContext pointer (or nil if idle)
test eax, eax
jz .skip_save
mov [eax + SavedESP_offset], esp
.skip_save:
; --- Dispatch all TMR_0_ISR hooks (unchanged) ---
call tmr0_dispatch_hooks
; --- Ask scheduler for next process ---
call scheduler_pick_next ; Returns PProcessContext in EAX (nil = idle)
mov [current_process], eax
test eax, eax
jz .load_idle
mov esp, [eax + SavedESP_offset] ; Switch to next process's stack
jmp .restore
.load_idle:
mov esp, [idle_esp] ; Switch to idle task stack
.restore:
; --- EOI ---
mov al, $20
out $20, al
; --- Restore new process's registers ---
pop gs
pop fs
pop es
pop ds
popad
iret ; Resume (potentially different) process
Idle Task¶
The idle task is the original kernel.yield() HLT loop. Its ESP is captured once during init before any process is created. When the scheduler returns nil (no runnable process), the context switcher loads the idle ESP. The IRET jumps back into the HLT loop. The next IRQ0 will check again.
Stack Frame Layout (per process)¶
When a new process is created, its stack is initialized with a fake interrupt frame so that the first context switch "returns" into it:
StackBase + PROCESS_STACK_SIZE (high addresses)
┌───────────────────────────┐
│ EFLAGS ($0202 = IF set) │ ← IRET pops this
│ CS ($08) │ ← IRET pops this
│ EIP (→ trampoline) │ ← IRET pops this
│ EAX (0) │ ← POPAD restores
│ ECX (0) │
│ EDX (0) │
│ EBX (0) │
│ ESP (ignored by POPAD)│
│ EBP (0) │
│ ESI (0) │
│ EDI (0) │
│ DS ($10) │ ← POP DS restores
│ ES ($10) │
│ FS ($10) │
│ GS ($10) │
└───────────────────────────┘
← SavedESP points here
The trampoline is a small Pascal procedure:
procedure process_trampoline;
{ This is the EIP that IRET jumps to on first schedule }
begin
STI; { Re-enable interrupts (IRET would have set IF from EFLAGS, but be safe) }
CurrentProcess^.State := psRunning;
CurrentProcess^.EntryPoint(CurrentProcess); { Run the process }
{ If EntryPoint returns, process is done }
CurrentProcess^.State := psFinished;
{ Yield to scheduler — we can't just return, we need to wait for next IRQ0 }
while true do HLT;
end;
Process Manager Unit (processmanager.pas)¶
Interface¶
unit processmanager;
interface
uses
proctypes,
stdio;
{ Core API }
function create(name : pchar;
entry : TProcessEntryPoint;
localData : void;
priority : uint8) : PProcessContext;
function findByID(pid : uint32) : PProcessContext;
function findByName(name : pchar) : PProcessContext;
procedure sendMessage(pid : uint32; msg : TProcessSysMsg; data : void);
procedure terminate(pid : uint32); { Sends smTerminate }
procedure kill(pid : uint32); { Forces immediate destroy + reap }
{ Process self-management (called by the running process) }
procedure proc_yield; { Voluntarily release remaining quantum }
procedure proc_await; { Block until an event/message arrives }
procedure proc_suspend; { Pause self until explicit resume }
procedure proc_exit(exitCode : uint32); { Terminate self }
{ Resource binding }
procedure bindResource(ctx : PProcessContext; kind : TResourceKind; handle : void; cleanup : pointer);
procedure unbindResource(ctx : PProcessContext; handle : void);
procedure unbindAllResources(ctx : PProcessContext);
{ Legacy command wrapper }
function runCommand(method : TCommandMethod; params : PParamList;
stdin_buf, stdout_buf, stderr_buf : POutBuf) : PProcessContext;
{ Query }
function processCount : uint32;
function getProcessByIndex(idx : uint32) : PProcessContext;
{ Init (called from kernel.pas) }
procedure init;
{ Terminal commands: PS, KILL, TERMINATE }
Internal Design¶
Process Table: Processes : PDList — stores uint32 pointers to TProcessContext (same pattern as TCP connections DList).
Scheduler (scheduler_pick_next):
Called from the context switch ISR (in contextswitcher.pas) after all timer hooks have run.
function scheduler_pick_next : PProcessContext:
{ Reap dead processes first }
Reap all psFinished/psError (unbind resources, kfree stack, kfree context, remove from table)
{ Increment TicksUsed on current process }
if CurrentProcess <> nil and CurrentProcess^.State = psRunning then
CurrentProcess^.TicksUsed += 1
if CurrentProcess^.TicksUsed < CurrentProcess^.Quantum then
return CurrentProcess { Same process, still has quantum left }
else
CurrentProcess^.State := psReady
CurrentProcess^.TicksUsed := 0
{ Round-robin through process table looking for psReady or psCreated }
next := pick next psReady/psCreated process (round-robin from current index)
if next <> nil then
next^.State := psRunning (or psCreated → first schedule via trampoline)
return next
else
return nil { No runnable process → idle }
Priority: Quantum = Priority * BASE_QUANTUM. Higher priority = more ticks before preemption. BASE_QUANTUM = 8 (~8ms at 1024 Hz). A priority-1 process gets 8 ticks; priority-10 gets 80 ticks (~78ms).
Reaping: Dead processes are reaped at the start of the scheduling pass (inside the ISR, interrupts disabled). This is safe because no process code is running at that point.
VTerminal Refactor — Multi-Instance Terminals¶
The vterminal is currently a singleton with ~12 global variables (scrollback buffer, input line, history, LVGL widget pointers, focus state). This prevents opening multiple terminals and tightly couples the UI to command execution.
Goal¶
Refactor vterminal into a multi-instance design where each launch allocates its own TVTermState and each terminal instance runs commands as preemptive processes.
TVTermState Record¶
All former globals move into a heap-allocated record:
PVTermState = ^TVTermState;
TVTermState = record
{ LVGL widgets }
win_id : uint32;
content : Plv_obj;
text_label : Plv_obj;
focused : boolean;
{ Scrollback text buffer }
text_buf : array[0..MAX_TEXT-1] of char;
text_len : uint32;
{ Current input line }
line_buf : TCommandBuffer;
line_len : uint32;
{ Command history ring }
hist : array[0..HIST_SIZE-1] of TCommandBuffer;
hist_count : uint32;
hist_head : uint32;
hist_pos : sint32;
saved_line : TCommandBuffer;
saved_len : uint32;
{ Process integration }
ForegroundPID : uint32; { PID of the running command process, or 0 }
fg_stdout : POutBuf; { Shared stdout buffer for the foreground process }
fg_stderr : POutBuf; { Shared stderr buffer for the foreground process }
last_drain : uint32; { stdout.len at last drain — for incremental rendering }
last_drain_err: uint32; { stderr.len at last drain }
fg_params : PParamList; { Saved params pointer for cleanup after process finishes }
end;
Instance Lifecycle¶
launch—kalloc(SizeOf(TVTermState)), initialise all fields, create LVGL window + widgets. Passstateasuser_datato all LVGL event callbacks.- Event callbacks — retrieve state via
lv_event_get_user_data(e). No globals referenced. onClose— ifForegroundPID <> 0, kill foreground process. Destroy LVGL widgets.kfree(state).- Desktop registers
launchonce — each click creates a new independent terminal.
Command Execution Flow (per-instance)¶
- User types command, presses Enter.
processCommand(state)looks up the command viastdio.findCommand.- Creates per-process
POutBufbuffers:state^.fg_stdout := createOutBuf(1024), etc. - Calls
processmanager.runCommand(cmd^.method, params, stdin_buf, fg_stdout, fg_stderr). - Stores the returned
PProcessContext^.ProcessIDinstate^.ForegroundPID. - Terminal enters busy state — keyboard input is buffered but Enter is ignored while a foreground process is active.
Incremental Output Rendering¶
The graphics refresh tick (~120 FPS) already calls desktop.update → LVGL handler. We add a lightweight drain check in the vterminal's update path:
procedure drainOutput(state : PVTermState);
var
ctx : PProcessContext;
begin
if state^.ForegroundPID = 0 then exit;
ctx := processmanager.findByID(state^.ForegroundPID);
{ Drain any new stdout content since last check }
if (state^.fg_stdout <> nil) and (state^.fg_stdout^.len > state^.last_drain) then begin
appendTextRange(state, state^.fg_stdout^.buf, state^.last_drain, state^.fg_stdout^.len);
state^.last_drain := state^.fg_stdout^.len;
end;
{ Same for stderr }
if (state^.fg_stderr <> nil) and (state^.fg_stderr^.len > state^.last_drain_err) then begin
appendTextRange(state, state^.fg_stderr^.buf, state^.last_drain_err, state^.fg_stderr^.len);
state^.last_drain_err := state^.fg_stderr^.len;
end;
{ Check if process finished }
if (ctx = nil) or (ctx^.State = psFinished) or (ctx^.State = psError) then begin
{ Final drain already done above }
freeOutBuf(state^.fg_stdout); state^.fg_stdout := nil;
freeOutBuf(state^.fg_stderr); state^.fg_stderr := nil;
freeParams(state^.fg_params); state^.fg_params := nil;
state^.ForegroundPID := 0;
state^.last_drain := 0;
state^.last_drain_err := 0;
showPrompt(state);
end;
refreshDisplay(state);
end;
This is called from a lightweight LVGL timer callback registered per-instance (e.g., every 100ms). No global hooks needed — each terminal polls its own foreground process.
Foreground / Background¶
- Each terminal instance tracks its own
ForegroundPID. Only that process's output is displayed in that terminal. - Future:
&suffix to run in background;FG/BG/JOBScommands (Phase 5). - Multiple terminals can each have their own foreground process running simultaneously — true multitasking UI.
Benefits¶
- Multiple terminals — each
launchcreates a fully independent instance. - No global state — all callbacks use
user_datato find their instance. - Per-terminal foreground process — each terminal independently tracks and renders its command's output.
- Clean teardown — closing a terminal kills its foreground process and frees all per-instance memory.
- Unchanged command procedures —
TCommandMethodcallbacks write toPOutBufas before; they don't know (or care) which terminal instance owns them.
Resource Binding System¶
Processes need access to shared resources (TCP sockets, VFS handles, etc.). When a process dies, its resources must be cleaned up.
Binding Flow¶
1. Process creates a TCP socket:
sock := tcp.connect(@ctx);
processmanager.bindResource(myProcess, rkSocket, void(sock), @tcp.abort_connection);
2. Process runs normally, using sock.
3. On process destroy (normal or kill):
processmanager.unbindAllResources(myProcess)
→ For each binding:
if binding.Cleanup <> nil then
TCleanupProc(binding.Cleanup)(binding.Handle)
kfree(binding)
This ensures sockets are RST'd, files are closed, timers unhooked, etc. even on abnormal termination.
Legacy Command Wrapper¶
To maintain backwards compatibility, existing TCommandMethod procedures are wrapped as true preemptive processes:
procedure legacy_entry(ctx : PProcessContext);
var
method : TCommandMethod;
params : PParamList;
begin
method := TCommandMethod(ctx^.EntryPoint); { Overloaded — see note }
params := PParamList(ctx^.MsgData);
method(params, ctx^.StdIn, ctx^.StdOut, ctx^.StdErr);
{ Procedure returns → trampoline sets psFinished }
end;
How it works: The legacy command procedure runs as real code on the process's own 8 KB stack. It calls bufWriteStr, bufWriteInt, etc. as normal. It may take microseconds (MEMINFO) or milliseconds (ARP table dump). Either way, when it returns, the process is done.
For halt-based async commands (PING, etc.): These currently set stdio.Halted := true and return immediately, with a timer hook doing work. Under the preemptive model, these can be refactored to loop inside the entry point:
procedure ping_entry(ctx : PProcessContext);
begin
while ctx^.PendingMsg <> smTerminate do begin
icmp.send_echo(...);
proc_await; { Block until reply or timeout }
{ Display result }
end;
end;
This means zero changes to existing command procedures. They just work.
Terminal Commands¶
PS — List Processes¶
KILL <pid> — Force-kill a Process¶
Calls processmanager.kill(pid).
TERMINATE <pid> — Graceful Shutdown¶
Calls processmanager.terminate(pid).
Re-entrancy & Shared State¶
Preemptive scheduling means a process can be interrupted at any point — including mid-kalloc, mid-DList-mutation, etc. Since all processes share the kernel address space, we need guards.
Strategy: CLI/STI Critical Sections¶
These are placed around:
- kalloc() / kfree() — heap metadata must not be corrupted
- DList mutations (DL_Add, DL_Delete) on shared lists
- Any global variable write that another process or a timer hook might read
Timer hooks already run with interrupts disabled (TMR_0_ISR calls CLI first), so they are inherently safe.
Cost: ~2 instructions per critical section. At 1024 Hz interrupt rate, the window where we might preempt mid-critical-section is tiny. The guards just prevent the rare unlucky case.
What Does NOT Need Guards¶
- Per-process
StdOut/StdErrwrites — only the owning process writes, terminal reads asynchronously (read-only is safe) - Process-local data (
ctx^.Local) — owned by one process TProcessContextfields modified only by the scheduler (which runs with interrupts disabled in the ISR)
Memory Management Strategy¶
TProcessContextallocated viakalloc(SizeOf(TProcessContext)), freed on reap.- Per-process stack:
kalloc(PROCESS_STACK_SIZE), freed on reap. Stack grows downward fromStackBase + PROCESS_STACK_SIZE. - Per-process
StdIn/StdOut/StdErrallocated viastdio.createOutBuf(), freed by the caller (vterminal or parent process). - Resource bindings stored in a
PDListper-process, each entrykalloc'd, freed on unbind. - Process table is a single
PDListstoringuint32pointers to contexts. Localdata is owned by the process — must bekfree'd before the entry point returns (or in a cleanup wrapper).
Implementation Phases¶
Phase 1: Core Infrastructure — Types & Process Manager¶
- [x] Create
src/include/proctypes.paswith all type definitions (TProcessState,TProcessSysMsg,TProcessEntryPoint,TProcessContext,TResourceBinding, etc.) - [x] Create
src/processmanager.paswith: process table (DList),create,findByID,findByName,sendMessage,terminate,kill,proc_yield,proc_await,proc_suspend,proc_exit,reap - [x]
PSterminal command - [x]
KILL/TERMINATEcommands - [x] Register in
kernel.pasinit sequence (after stdio, before progmanager) - [x] Compilation verification (no context switching yet — processes created but not run)
Phase 2: Context Switcher & Preemption¶
- [x] Implement
contextswitcher.paswith the assembly ISR stub - [x] Capture idle ESP from
kernel.yield()before entering the HLT loop - [x] Replace ISR_32's IDT entry with the custom context switch stub
- [x] Implement
scheduler_pick_next(round-robin with priority quantum) - [x] Implement
process_trampoline(entry/exit wrapper) - [x] Fake interrupt frame construction in
processmanager.create - [x] PUSHF/CLI...POPF guards on
kalloc/kfreeinlmemorymanager.pas - [x] Create two trivial test processes (print "Hello from program 1/2" once per second)
- [x] Boot-test: verify timer hooks (graphics, USB, BIOS tick) still fire
- [x] Boot-test: verify test processes run, interleave correctly, ~1 msg/sec each
Phase 3: VTerminal Multi-Instance + Legacy Command Integration¶
- [x]
runCommandwrapper for existingTCommandMethodcommands (aslegacy_entryprocess) — already implemented inprocessmanager.pas - [x] Define
PVTermState/TVTermStaterecord containing all current vterminal globals - [x] Refactor
launchtokalloca newTVTermStateper invocation - [x] Pass
stateasuser_datato all LVGL event callbacks (content_click_cb,content_key_cb,content_defocus_cb) - [x] Refactor all internal procedures (
appendText,refreshDisplay,showPrompt,processCommand) to takestate : PVTermStateparameter - [x] Refactor
onCloseto kill foreground process (if any) andkfree(state) - [x] Modify
processCommandto callprocessmanager.runCommandinstead of callingcmd^.methoddirectly - [x] Implement
drainOutput(state)— incremental stdout/stderr rendering via LVGL timer callback per-instance - [x] Per-instance
ForegroundPIDtracking: block Enter while process is running, restore prompt on finish - [x] Build + boot-test: zero panics, clean boot, test processes still interleaving at ~1 msg/sec
- [x] CTRL+C terminal interrupt —
LV_KEY_CTRLCvia keyboard hook,killForegroundin vterminal - [x] Per-terminal CWD and dir stack — CD/LS/PUSHD/POPD as vterminal builtins
- [ ] Verify all existing commands still work identically (MEMINFO, CPU, ARP, LS, etc.) — requires manual UI testing
Phase 4: Resource Binding & Cleanup¶
- [x]
bindResource/unbindResource/unbindAllResources— implemented in processmanager.pas (Phase 1) - [x]
unbindResourceNoCleanup— removes binding without invoking cleanup callback (prevents re-entrancy) - [x] Re-entrancy safety —
unbindAllResourcesdetaches resource list before iterating - [x]
OwnerPIDfield onTTCPSocket— tracks owning process - [x]
socket_cleanupwrapper in tcp.pas — callsabort_connectionto RST and destroy - [x] TCP
connect/listenauto-bind socket toCurrentProcess - [x] TCP
DestroySocketauto-unbinds from owning process (no cleanup, prevents infinite recursion) - [x] Build + boot-test: clean boot, no crashes
- [ ] Integrate with VFS (file handles auto-bound) — N/A, VFS file operations are stubs
- [ ] Verify cleanup on kill (RST sockets, close files) — requires manual network testing
Phase 5: Process-Aware Async Commands & Command Consolidation¶
- [x] Refactor PING to be a proper looping process —
src/prog/ping.pas, heap-allocated state, 5s timeout - [x]
proc_sleep_msuniversal sleep in processmanager.pas - [x] Parent-child process relationship —
reapFinishedsendssmChildExitedto parent before reaping - [x] Centralize all scattered
registerCommandcalls intoprogmanager.pas— 14 commands moved from 9 units (kernel, cpu, drivermanagement, processmanager, arp, ipv4, tcp, storagemanagement, usbcore) - [x] Export command procedures in host unit interfaces — procedures remain with their data but registration is centralized
- [x] Add
&background suffix support in vterminal —TBackgroundJobrecord,bg_jobsPDList per terminal - [x]
JOBScommand — lists background jobs with status - [x]
FGcommand — brings first background job to foreground - [ ] Refactor dhclient to be a background process (can already be launched with
&suffix)
Kept as-is (not moved):
- E1000/MAC commands — conditionally registered in load() callback (hardware-dependent)
- TRACER command — circular dependency prevents export (stdio↔tracer)
- stdio builtins (VERSION, CLEAR, HELP, ECHO, TIME, REBOOT) — intrinsic shell commands
Phase 6: WASM Integration (future)¶
- [ ]
PWASMContextasLocaldata - [ ] EntryPoint loads WASM module, enters execution loop
- [ ] Loop calls
wasm.vm.execute(N_INSTRUCTIONS)per iteration, checksPendingMsg - [ ]
smTerminatebreaks the loop → returns → psFinished - [ ] Terminal command:
WASM <file.wasm>→ creates WASM process
Files Modified/Created¶
| File | Changes |
|---|---|
src/include/proctypes.pas |
New — all process type definitions |
src/processmanager.pas |
New — core process management unit, scheduler, reaper, resource binding, proc_sleep_ms |
src/contextswitcher.pas |
Rewritten — custom ISR_32 assembly stub, idle ESP capture, trampoline |
src/kernel.pas |
Add processmanager.init, contextswitcher.init; modify yield() to capture idle ESP |
src/lmemorymanager.pas |
Add CLI/STI guards around kalloc/kfree |
src/prog/vterminal.pas |
Major refactor — multi-instance, CTRL+C (killForeground), per-terminal CWD/dir stack, FS builtins (CD/LS/PUSHD/POPD) |
src/prog/ping.pas |
New — standalone ICMP ping process with heap-allocated state, 5s timeout, concurrency-safe |
src/prog/testcmd.pas |
New — TEST command (5 lines with 1s delays via proc_sleep_ms) |
src/driver/video/lvgl.pas |
LV_KEY_CTRLC constant + Ctrl+C interception in keyboard hook |
src/driver/storage/vfs.pas |
Per-terminal VFS helpers (makeAbsolutePathFrom, changeDirectoryFrom, GetDirectoryListingFrom); removed FS command registrations |
src/driver/net/include/nettypes.pas |
OwnerPID field on TTCPSocket (Phase 4) |
src/driver/net/l4/tcp.pas |
Auto-bind sockets to calling process; socket_cleanup + DestroySocket unbind (Phase 4) |
src/driver/net/l4/icmp.pas |
UserData parameter on ICMP callbacks for ping concurrency |
src/progmanager.pas |
Central command registration + register testcmd, ping, and 14 commands from provider units |
src/stdio.pas |
Minor: expose halt state for legacy wrapper (Phase 3) |
src/isr/isrmanager.pas |
Minor: allow ISR_32 IDT gate to be overridden (Phase 2) |
src/vmemorymanager.pas |
Bugfix: vtop mask $FFFFFF → $3FFFFF (22-bit offset for 4MB pages) |
Risk Considerations¶
| Risk | Mitigation |
|---|---|
| Triple-fault on bad ESP swap | Extensive serial logging in context switch; test with single trivial process first |
Re-entrancy in kalloc/kfree |
CLI/STI guards around all allocator calls |
| Timer hooks disrupted | Hooks dispatched before context switch — identical to today; test first |
| Process crash = kernel crash | Ring-0 limitation — processes share address space. Future: per-process fault handler |
| DList mutation during ISR reap | Reaping runs in ISR with interrupts disabled — no concurrent mutation possible |
| Memory leaks on process death | unbindAllResources + kfree(stack) + kfree(ctx) in reap |
| Legacy command compatibility | Legacy wrapper is a real process — runs the exact same code on its own stack |
| FPC runtime hidden globals | Avoid Str(), dynamic strings etc. in process code; use bufWrite* helpers |
| Stack overflow (8 KB limit) | Sufficient for typical commands; future: guard page at stack base (unmapped page → #PF → kill process) |
| WASM VM infinite loop | Preemption handles this naturally — VM process is preempted like any other |
| VTerminal multi-instance memory | Each instance is ~10 KB (4 KB scrollback + 1 KB input + 5 KB history). 10 terminals = ~100 KB — acceptable |
| LVGL user_data lifetime | State pointer must outlive the LVGL objects — guaranteed by kfree only in onClose after widget destruction |