357 lines
15 KiB
Markdown
357 lines
15 KiB
Markdown
# taskswitch -- Cooperative Task Switching Library
|
|
|
|
Cooperative (non-preemptive) multitasking library for DJGPP/DPMI (DOS
|
|
protected mode). Part of the DVX GUI project.
|
|
|
|
Tasks voluntarily yield the CPU by calling `tsYield()`. A credit-based
|
|
weighted round-robin scheduler gives higher-priority tasks proportionally
|
|
more CPU time while guaranteeing that low-priority tasks are never
|
|
starved. A priority-10 task gets 11 turns per scheduling round; a
|
|
priority-0 task gets 1 -- but it always runs eventually.
|
|
|
|
The task array is backed by stb_ds and grows dynamically. Terminated
|
|
task slots are recycled, so there is no fixed upper limit on the number
|
|
of tasks created over the lifetime of the application.
|
|
|
|
|
|
## Why Cooperative?
|
|
|
|
DOS is single-threaded. DPMI provides no timer-based preemption. The
|
|
DVX GUI event model is inherently single-threaded: one compositor, one
|
|
input queue, one window stack. Preemptive switching would require
|
|
locking around every GUI call for no benefit. Cooperative switching
|
|
lets each task yield at safe points, avoiding synchronization entirely.
|
|
|
|
|
|
## Files
|
|
|
|
| File | Description |
|
|
|-----------------------|----------------------------------------------------|
|
|
| `taskswitch.h` | Public API -- types, constants, function prototypes |
|
|
| `taskswitch.c` | Implementation (scheduler, context switch, slots) |
|
|
| `demo.c` | Standalone test harness exercising all features |
|
|
| `thirdparty/stb_ds.h` | stb dynamic array/hashmap library (third-party) |
|
|
| `Makefile` | DJGPP cross-compilation build rules |
|
|
|
|
|
|
## Building
|
|
|
|
Cross-compile from Linux:
|
|
|
|
```
|
|
make # builds ../lib/libtasks.a
|
|
make demo # also builds ../bin/tsdemo.exe
|
|
make clean # removes objects, library, and demo binary
|
|
```
|
|
|
|
Output:
|
|
|
|
| Path | Description |
|
|
|---------------------|----------------------|
|
|
| `../lib/libtasks.a` | Static library |
|
|
| `../obj/tasks/` | Object files |
|
|
| `../bin/tsdemo.exe` | Demo executable |
|
|
|
|
|
|
## Quick Start
|
|
|
|
```c
|
|
#include <stdio.h>
|
|
#include "taskswitch.h"
|
|
|
|
void myTask(void *arg) {
|
|
const char *name = (const char *)arg;
|
|
for (int i = 0; i < 3; i++) {
|
|
printf("[%s] working...\n", name);
|
|
tsYield();
|
|
}
|
|
}
|
|
|
|
int main(void) {
|
|
tsInit();
|
|
tsCreate("alpha", myTask, "alpha", 0, TS_PRIORITY_NORMAL);
|
|
tsCreate("beta", myTask, "beta", 0, TS_PRIORITY_HIGH);
|
|
|
|
while (tsActiveCount() > 1) {
|
|
tsYield();
|
|
}
|
|
|
|
tsShutdown();
|
|
return 0;
|
|
}
|
|
```
|
|
|
|
|
|
## Lifecycle
|
|
|
|
1. `tsInit()` -- Initialize the task system. The calling context
|
|
(typically `main`) becomes task 0 with `TS_PRIORITY_NORMAL`. No
|
|
separate stack is allocated for task 0 -- it uses the process stack.
|
|
|
|
2. `tsCreate(...)` -- Create tasks. Each gets a name, entry function,
|
|
argument pointer, stack size (0 for the default), and a priority.
|
|
Returns the task ID (>= 0) or a negative error code. Terminated
|
|
task slots are reused automatically.
|
|
|
|
3. `tsYield()` -- Call from any task (including main) to hand the CPU
|
|
to the next eligible task. This is the sole mechanism for task
|
|
switching.
|
|
|
|
4. `tsShutdown()` -- Free all task stacks and the task array.
|
|
|
|
Tasks terminate by returning from their entry function or by calling
|
|
`tsExit()`. The main task (id 0) must never call `tsExit()`. When a
|
|
task terminates, its stack is freed immediately and its slot becomes
|
|
available for reuse by the next `tsCreate()` call.
|
|
|
|
|
|
## API Reference
|
|
|
|
### Initialization and Teardown
|
|
|
|
| Function | Signature | Description |
|
|
|--------------|-------------------------|--------------------------------------------------------------------|
|
|
| `tsInit` | `int32_t tsInit(void)` | Initialize the library. Returns `TS_OK` or a negative error code. |
|
|
| `tsShutdown` | `void tsShutdown(void)` | Free all resources. Safe to call even if `tsInit` was never called. |
|
|
|
|
### Task Creation and Termination
|
|
|
|
| Function | Signature | Description |
|
|
|------------|---------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------|
|
|
| `tsCreate` | `int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t ss, int32_t pri)` | Create a ready task. Returns the task ID (>= 0) or a negative error code. Pass 0 for `ss` to use `TS_DEFAULT_STACK_SIZE`. Reuses terminated slots. |
|
|
| `tsExit` | `void tsExit(void)` | Terminate the calling task. Must not be called from the main task. Never returns. |
|
|
| `tsKill` | `int32_t tsKill(uint32_t taskId)` | Forcibly terminate another task. Cannot kill main (id 0) or self (use `tsExit` instead). |
|
|
|
|
### Scheduling
|
|
|
|
| Function | Signature | Description |
|
|
|-----------|----------------------|-----------------------------------------------------------------|
|
|
| `tsYield` | `void tsYield(void)` | Voluntarily relinquish the CPU to the next eligible ready task. |
|
|
|
|
### Pausing and Resuming
|
|
|
|
| Function | Signature | Description |
|
|
|------------|---------------------------------|------------------------------------------------------------------------------------------------------------|
|
|
| `tsPause` | `int32_t tsPause(uint32_t id)` | Pause a task. Main task (id 0) cannot be paused. Self-pause triggers an implicit yield. |
|
|
| `tsResume` | `int32_t tsResume(uint32_t id)` | Resume a paused task. Credits are refilled so it is not penalized for having been paused. |
|
|
|
|
### Priority
|
|
|
|
| Function | Signature | Description |
|
|
|-----------------|---------------------------------------------------|-----------------------------------------------------------------------------------|
|
|
| `tsSetPriority` | `int32_t tsSetPriority(uint32_t id, int32_t pri)` | Change a task's priority. Credits are reset so the change takes effect immediately. |
|
|
| `tsGetPriority` | `int32_t tsGetPriority(uint32_t id)` | Return the task's priority, or `TS_ERR_PARAM` on an invalid ID. |
|
|
|
|
### Crash Recovery
|
|
|
|
| Function | Signature | Description |
|
|
|-------------------|------------------------------|------------------------------------------------------------------------------------------------------------------|
|
|
| `tsRecoverToMain` | `void tsRecoverToMain(void)` | Reset scheduler state to task 0 after a `longjmp` from a signal handler. Call before `tsKill` on the crashed task. The crashed task's slot is NOT freed -- call `tsKill` afterward. |
|
|
|
|
### Query
|
|
|
|
| Function | Signature | Description |
|
|
|-----------------|--------------------------------------|--------------------------------------------------------|
|
|
| `tsGetState` | `TaskStateE tsGetState(uint32_t id)` | Return the task's state enum value. |
|
|
| `tsCurrentId` | `uint32_t tsCurrentId(void)` | Return the ID of the currently running task. |
|
|
| `tsGetName` | `const char *tsGetName(uint32_t id)` | Return the task's name string, or `NULL` on invalid ID. |
|
|
| `tsActiveCount` | `uint32_t tsActiveCount(void)` | Return the number of non-terminated tasks. |
|
|
|
|
|
|
## Constants
|
|
|
|
### Error Codes
|
|
|
|
| Name | Value | Meaning |
|
|
|----------------|-------|--------------------------------------------------|
|
|
| `TS_OK` | 0 | Success |
|
|
| `TS_ERR_INIT` | -1 | Library not initialized |
|
|
| `TS_ERR_PARAM` | -2 | Invalid parameter |
|
|
| `TS_ERR_FULL` | -3 | Task table full (unused, kept for compatibility) |
|
|
| `TS_ERR_NOMEM` | -4 | Memory allocation failed |
|
|
| `TS_ERR_STATE` | -5 | Invalid state transition |
|
|
|
|
### Priority Presets
|
|
|
|
| Name | Value | Credits per Round |
|
|
|----------------------|-------|-------------------|
|
|
| `TS_PRIORITY_LOW` | 0 | 1 |
|
|
| `TS_PRIORITY_NORMAL` | 5 | 6 |
|
|
| `TS_PRIORITY_HIGH` | 10 | 11 |
|
|
|
|
Any non-negative `int32_t` may be used as a priority. The presets are
|
|
provided for convenience. In the DVX Shell, the main task runs at
|
|
`TS_PRIORITY_HIGH` to keep the UI responsive; app tasks default to
|
|
`TS_PRIORITY_NORMAL`.
|
|
|
|
### Defaults
|
|
|
|
| Name | Value | Description |
|
|
|-------------------------|-------|------------------------|
|
|
| `TS_DEFAULT_STACK_SIZE` | 32768 | Default stack per task |
|
|
| `TS_NAME_MAX` | 32 | Max task name length |
|
|
|
|
|
|
## Types
|
|
|
|
### TaskStateE
|
|
|
|
```c
|
|
typedef enum {
|
|
TaskStateReady = 0, // Eligible for scheduling
|
|
TaskStateRunning = 1, // Currently executing
|
|
TaskStatePaused = 2, // Suspended until tsResume()
|
|
TaskStateTerminated = 3 // Finished; slot will be recycled
|
|
} TaskStateE;
|
|
```
|
|
|
|
Only Ready tasks participate in scheduling. Running is cosmetic (marks
|
|
the currently executing task). Paused tasks are skipped until explicitly
|
|
resumed. Terminated slots are recycled by `tsCreate`.
|
|
|
|
### TaskEntryT
|
|
|
|
```c
|
|
typedef void (*TaskEntryT)(void *arg);
|
|
```
|
|
|
|
The signature every task entry function must follow. The `arg` parameter
|
|
is the pointer passed to `tsCreate`.
|
|
|
|
|
|
## Scheduler Details
|
|
|
|
The scheduler is a credit-based weighted round-robin, a variant of the
|
|
Linux 2.4 goodness() scheduler.
|
|
|
|
1. Every ready task holds a credit counter initialized to `priority + 1`.
|
|
2. When `tsYield()` is called, the scheduler scans tasks starting one
|
|
past the current task (wrapping around) looking for a ready task with
|
|
credits > 0. When found, that task's credits are decremented and it
|
|
becomes the running task.
|
|
3. When no ready task has credits remaining, every ready task is
|
|
refilled to `priority + 1` (one "epoch") and the scan repeats.
|
|
|
|
This means a priority-10 task receives 11 turns for every 1 turn a
|
|
priority-0 task receives, but the low-priority task still runs -- it
|
|
is never starved.
|
|
|
|
Credits are also refilled when:
|
|
|
|
- A task is created (`tsCreate`) -- starts with `priority + 1`.
|
|
- A task is resumed (`tsResume`) -- refilled so it runs promptly.
|
|
- A task's priority changes (`tsSetPriority`) -- reset to `new + 1`.
|
|
|
|
|
|
## Task Slot Management
|
|
|
|
The task array is a stb_ds dynamic array that grows automatically.
|
|
Each slot has an `allocated` flag:
|
|
|
|
- `tsCreate()` scans for the first unallocated slot (starting at index
|
|
1, since slot 0 is always the main task). If no free slot exists, the
|
|
array is extended with `arrput()`.
|
|
- `tsExit()` and `tsKill()` free the terminated task's stack immediately
|
|
and mark the slot as unallocated, making it available for the next
|
|
`tsCreate()` call.
|
|
- Task IDs are stable array indices. Slots are never removed or
|
|
reordered, so a task ID remains valid for queries until the slot is
|
|
recycled.
|
|
|
|
This supports long-running applications (like the DVX Shell) that
|
|
create and destroy many tasks over their lifetime without unbounded
|
|
memory growth.
|
|
|
|
|
|
## Context Switch Internals
|
|
|
|
Context switching uses inline assembly with both i386 and x86_64 code
|
|
paths. The `contextSwitch` function is marked `noinline` to preserve
|
|
callee-saved register assumptions.
|
|
|
|
Why inline asm instead of setjmp/longjmp: setjmp/longjmp only save
|
|
callee-saved registers and do not give control over the stack pointer
|
|
in a portable way. New tasks need a fresh stack with the instruction
|
|
pointer set to a trampoline -- setjmp cannot bootstrap that. The asm
|
|
approach also avoids ABI differences in jmp_buf layout across DJGPP
|
|
versions.
|
|
|
|
### i386 (DJGPP target)
|
|
|
|
Six callee-saved values are saved and restored per switch:
|
|
|
|
| Register | Offset | Purpose |
|
|
|----------|--------|------------------------------------------|
|
|
| EBX | 0 | Callee-saved general purpose |
|
|
| ESI | 4 | Callee-saved general purpose |
|
|
| EDI | 8 | Callee-saved general purpose |
|
|
| EBP | 12 | Frame pointer |
|
|
| ESP | 16 | Stack pointer |
|
|
| EIP | 20 | Resume address (captured as local label) |
|
|
|
|
### x86_64 (for native Linux testing)
|
|
|
|
Eight callee-saved values are saved and restored per switch:
|
|
|
|
| Register | Offset | Purpose |
|
|
|----------|--------|------------------------------------------|
|
|
| RBX | 0 | Callee-saved general purpose |
|
|
| R12 | 8 | Callee-saved general purpose |
|
|
| R13 | 16 | Callee-saved general purpose |
|
|
| R14 | 24 | Callee-saved general purpose |
|
|
| R15 | 32 | Callee-saved general purpose |
|
|
| RBP | 40 | Frame pointer |
|
|
| RSP | 48 | Stack pointer |
|
|
| RIP | 56 | Resume address (RIP-relative lea) |
|
|
|
|
Segment registers are not saved because DJGPP runs in a flat
|
|
protected-mode environment where CS, DS, ES, and SS share the same
|
|
base.
|
|
|
|
New tasks have their initial stack pointer set to a 16-byte-aligned
|
|
region at the top of a malloc'd stack, with the instruction pointer
|
|
set to an internal trampoline that calls the user's entry function
|
|
and then `tsExit()`.
|
|
|
|
|
|
## Limitations
|
|
|
|
- **Cooperative only** -- tasks must call `tsYield()` (or
|
|
`tsPause`/`tsExit`) to allow other tasks to run. A task that never
|
|
yields blocks everything.
|
|
- **Not interrupt-safe** -- no locking or volatile module state. Do not
|
|
call library functions from interrupt handlers.
|
|
- **Single-threaded** -- designed for one CPU under DOS protected mode.
|
|
- **Stack overflow is not detected** -- size the stack appropriately for
|
|
each task's needs.
|
|
|
|
|
|
## Demo
|
|
|
|
`demo.c` exercises five phases:
|
|
|
|
1. **Priority scheduling** -- creates tasks at low, normal, and high
|
|
priority. All tasks run, but the high-priority task gets significantly
|
|
more turns.
|
|
2. **Pause** -- pauses one task mid-run and shows it stops being
|
|
scheduled.
|
|
3. **Resume** -- resumes the paused task and shows it picks up where it
|
|
left off.
|
|
4. **Priority boost** -- raises the low-priority task above all others
|
|
and shows it immediately gets more turns.
|
|
5. **Slot reuse** -- creates three waves of short-lived tasks that
|
|
terminate and shows subsequent waves reuse the same task IDs.
|
|
|
|
Build and run:
|
|
|
|
```
|
|
make demo
|
|
tsdemo
|
|
```
|
|
|
|
|
|
## Third-Party Dependencies
|
|
|
|
- **stb_ds.h** (Sean Barrett) -- dynamic array and hashmap library.
|
|
Located in `thirdparty/stb_ds.h`. Used for the task control block
|
|
array. Public domain / MIT licensed.
|