TaskSwitch/README.md
2026-02-23 20:28:38 -06:00

261 lines
11 KiB
Markdown

# taskswitch -- Cooperative Task Switching Library for DJGPP
A lightweight cooperative multitasking library targeting DJGPP (i386 protected
mode DOS). Tasks voluntarily yield the CPU with `tsYield()`. A credit-based
weighted round-robin scheduler ensures every task runs while giving
higher-priority tasks proportionally more CPU time.
## Files
| File | Description |
|----------------|------------------------------------------|
| `taskswitch.h` | Public API -- types, constants, functions |
| `taskswitch.c` | Implementation |
| `demo.c` | Example program exercising every feature |
| `Makefile` | Build rules for native DJGPP or cross |
## Building
Native DJGPP (from a DOS prompt with DJGPP in the PATH):
```
make
```
Cross-compiling from Linux (adjust the prefix to match your toolchain):
```
make CC=i586-pc-msdosdjgpp-gcc
```
Debug build with no optimisation and symbols:
```
make debug
```
Clean:
```
make clean
```
## 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(4);
tsCreate("alpha", myTask, "alpha", 0, TS_PRIORITY_NORMAL);
tsCreate("beta", myTask, "beta", 0, TS_PRIORITY_HIGH);
for (int i = 0; i < 10; i++) {
tsYield();
}
tsShutdown();
return 0;
}
```
## Lifecycle
1. **`tsInit(maxTasks)`** -- Allocate the task table. The calling context
(typically `main`) becomes task 0 with `TS_PRIORITY_NORMAL`. `maxTasks`
is the total capacity including this main task slot.
2. **`tsCreate(...)`** -- Create tasks. Each gets a name, entry function,
argument pointer, stack size (0 for the 8 KB default), and a priority.
3. **`tsYield()`** -- Call from any task (including main) to hand the CPU to
the next eligible task.
4. **`tsShutdown()`** -- Free all task stacks and the task table.
Tasks terminate by returning from their entry function or by calling
`tsExit()`. The main task (id 0) must never call `tsExit()`.
## API Reference
### Initialisation and Teardown
| Function | Signature | Description |
|----------------|------------------------------|--------------------------------------------------------------------------------------|
| `tsInit` | `int32_t tsInit(uint32_t)` | Initialise 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` (8 KB). |
| `tsExit` | `void tsExit(void)` | Terminate the calling task. Must not be called from the main task. |
### 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. The main task (id 0) cannot be paused. If a task pauses itself, an implicit yield occurs. |
| `tsResume` | `int32_t tsResume(uint32_t id)` | Resume a paused task. Its credits are refilled to `priority + 1` so it is not penalised 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 to `pri + 1` 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. |
### 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 initialised |
| `TS_ERR_PARAM` | -2 | Invalid parameter |
| `TS_ERR_FULL` | -3 | Task table full |
| `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.
### Defaults
| Name | Value | Description |
|-------------------------|-------|-------------------------|
| `TS_DEFAULT_STACK_SIZE` | 8192 | 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 cannot be reused
} TaskStateE;
```
### `TaskEntryT`
```c
typedef void (*TaskEntryT)(void *arg);
```
The signature every task entry function must follow. `arg` is the pointer
passed to `tsCreate`.
## Scheduler Details
The scheduler is a **credit-based weighted round-robin**.
1. Every ready task holds a credit counter initialised 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` 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 is not penalised.
- A task's **priority changes** (`tsSetPriority`) -- reset to `new + 1`.
## Context Switch Internals
Context switching is performed entirely in i386 inline assembly. Six 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)|
The save and restore pointers are passed into the assembly block via GCC
register constraints (`%eax` and `%edx`). Segment registers are not
saved because DJGPP runs in a flat 32-bit protected-mode environment where
CS, DS, ES, and SS share the same base.
New tasks have their initial ESP pointed at a 16-byte-aligned region at the
top of a `malloc`'d stack, with EIP 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.
- **No task slot reuse** -- once a task terminates, its slot in the task table
cannot be reclaimed. The `maxTasks` value passed to `tsInit` is a lifetime
limit.
- **Not interrupt-safe** -- the library uses 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 four 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.
Build and run:
```
make
demo
```