mk90.system

Multiprogramming

Four fixed context slots with cooperative scheduling and explicit ownership.

Execution model

MK90/Unix 0.6 has one CPU and four task-control blocks. At most one context executes at a time. Other active tasks make progress when the running context calls a yield or a blocking operation that yields. There is no timer-driven preemption.

This is real context switching: registers, stack pointer, return program counter and supported processor status are saved and restored. It is deliberately smaller than a general Unix process model: there is no fork, address-space duplication, memory protection, arbitrary background command execution or dynamically allocated process table.

Context slots and ownership

PIDRoleStackResources and lifetime
0Shell/parser/dispatcherShared 512-byte foreground stackWaits while PID 3 borrows the stack; CD runs here.
1Background worker or pipe consumer160 bytes, plus 32 private data bytesIndependent registers and stack; slot can be reused.
2Background worker or pipe consumer160 bytes, plus 32 private data bytesIndependent registers and stack; slot can be reused.
3One foreground childSame stack as PID 0Runs an ordinary command or the single BASIC instance; parent is suspended.

One simple foreground command can coexist with both background workers. A pipeline needs one of slots 1/2 for its consumer and can coexist with at most one other worker. PIDs identify slots, not permanent process histories.

Foreground command lifecycle

The shell resolves and loads a command before starting its child. It saves its continuation, marks PID 0 WAIT_CHILD, initializes PID 3 and calls the entry point on the existing foreground stack. PID 0 is not runnable while PID 3 uses that stack.

The child can yield to PID 1/2. Returning normally or through task_exit restores the saved parent continuation, records the child result and changes its state to DONE. The kernel closes the child’s pipe writer, releases its readers and aborts uncommitted file writers. PID 0 becomes READY again.

This mechanism neither forks the shell nor gives the child a second 512-byte stack. The parent’s suspended frames and the child’s frames share the one bounded region. The next command reuses PID 3.

Scheduler algorithm

task_yield saves the context while interrupts are masked, then scans cyclically from the following TCB. A sleeping task whose deadline is due becomes READY. READY contexts with valid stacks can be selected; FREE, DONE, FAULT and blocked contexts are skipped.

If no context is ready, the scheduler enables interrupts and executes WAIT, then scans again. RTC and keyboard interrupts can wake the CPU; their handlers do not select a different task themselves.

The selection order is round-robin among eligible slots at yield points. It is not a fixed time slice and does not promise equal CPU time. A task that does not cooperate can delay every other task, even while timer interrupts continue.

Yield is suppressed during a shared-controller transaction or when the saved interrupt priority masks interrupts. The sleep service rejects those conditions before marking a task asleep. Native blocking APIs must not be called from interrupt handlers.

Task states

ValuePS letterStateMeaning / transition
0FREENot allocated; omitted from PS.
1RREADYEligible for selection; the current running task uses this state too.
2SSLEEPDeadline in RTC ticks; made READY by a later scheduler scan.
3DDONEFinished; exit result retained until the slot is reused.
4FFAULTStack check failed in a background slot; exit result 1.
5IWAIT_READPipe reader blocked until data or writer closure.
6OWAIT_WRITEPipe writer blocked until space or reader closure.
7WWAIT_CHILDShell cannot run while its foreground child borrows the stack.

RTC and timing

The kernel uses a nominal 32 Hz RTC tick, held in an unsigned 16-bit counter. The counter wraps every 65,536 ticks: 2,048 seconds. Deadlines are compared with modular signed differences, so a single sleep accepts at most 32,767 ticks.

The shell SLEEP command accepts 0–1023 whole seconds. BASIC WAIT accepts 0–32767 ticks. Zero means yield. Neither the conversion nor a deadline guarantees when a task actually resumes: controller work and other tasks can add latency.

COUNT and PULSE sleep for 32 ticks before each step, then do their work. Their counters are application progress, not elapsed-time measurements. PS does not show CPU percentages or accumulated CPU time.

Built-in background workers

RUN creates only COUNT or PULSE. An optional 1–9999 step limit gives a finite run; omission means continuous operation. PULSE calls the guarded buzzer API after incrementing its counter. On a tone I/O failure it exits with the returned error.

KILL accepts only PID 1 or 2 and marks the selected allocated slot DONE with result 130. It is not a signal facility. WAIT prints a target’s exit result when it becomes DONE/FAULT, or allows SU+C to abandon the wait without stopping the worker. A later RUN may overwrite the old slot history.

I/O and memory ownership

Only PID 0 and PID 3 may use the regular-file services; they never execute simultaneously. Background workers must use their own registers, stack and 32-byte data area and cannot touch shell buffers, load application modules or directly use filesystem, keyboard, LCD or shared-controller registers.

The registered pipe consumer is a controlled exception: its verified pager code remains loaded while it owns the keyboard/display, and it reads only the FIFO. It does not open files or overwrite a suspended producer’s sector buffer.

The application loader accepts the shell and the foreground child’s registered BASIC module family. It rejects ordinary background loads, and all replacement loads while a pipe is active. These restrictions preserve return addresses and code across yield points.

There is no MMU or protection boundary between native tasks. A trusted native task with an invalid pointer can still corrupt another subsystem. These contracts govern the built-in code; they are not a security sandbox.

Context frames and stack checks

A saved context, from its saved SP upward, contains R5, R4, R3, R2, R1, R0, PSW and PC. The supported PSW state is its low byte, including condition flags and interrupt priority. Trace mode and privileged modes of other PDP-11 models are not promised.

Each stack region has an A55A guard word. Checks require an aligned saved SP, at least 32 bytes above the lower bound, and room for the saved frame below the upper bound. The declared stack sizes include kernel and IRQ frames, not just application locals.

A bad background stack marks that slot FAULT. If it is the pipe consumer, its read endpoint closes and wakes the producer. A bad shared shell/foreground stack is fatal: the system displays a stack error and requires restart. Guard checks detect some corruption after it occurs; they do not prevent every invalid write.

BASIC participation

MBC compiles in PID 3 and yields between source lines. It reuses console text/shadow memory for the scanner and target table, so it does not render or poll keyboard during compilation. COUNT/PULSE remain compatible because they do not touch those buffers.

The MBX runtime also uses PID 3. Every 32 opcodes it calls the keyboard poll path, which renders, rescans input and yields. WAIT yields directly; keyboard input also cooperates. One BASIC instance may coexist with the native workers, but cannot be started with &, RUN or a pipe.

SU+C cancellation is checked at runtime polling/input points. A long WAIT or controller transaction must return before the next cancellation check; compilation has no SU+C polling loop.

Related reference

Native task API · Pipe lifecycle · Memory layout · ps