# 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. The task array grows dynamically using stb_ds and terminated task slots are recycled, so there is no fixed upper limit on the number of tasks created over the lifetime of the application. ## Files | File | Description | |-----------------------|------------------------------------------| | `taskswitch.h` | Public API -- types, constants, functions | | `taskswitch.c` | Implementation | | `demo.c` | Example program exercising every feature | | `thirdparty/stb_ds.h` | Dynamic array/hashmap library (stb) | | `Makefile` | DJGPP cross-compilation build rules | ## Building Cross-compiling from Linux: ``` make ``` Clean: ``` make clean ``` Output: | Path | Description | |-------------------|----------------------------| | `../lib/libtasks.a` | Static library | | `../obj/tasks/` | Object files | | `../bin/tsdemo.exe` | Demo executable | ## Quick Start ```c #include #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`. 2. **`tsCreate(...)`** -- Create tasks. Each gets a name, entry function, argument pointer, stack size (0 for the 8 KB 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. 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 ### Initialisation and Teardown | Function | Signature | Description | |----------------|------------------------------|--------------------------------------------------------------------------------------| | `tsInit` | `int32_t tsInit(void)` | 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). Reuses terminated task slots when available. | | `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 (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. ### 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 will be recycled } 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`. ## Task Slot Management The task array is a stb_ds dynamic array that grows automatically as needed. 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()`** frees the terminated task's stack immediately and marks 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 design supports long-running applications that create and destroy many tasks over their lifetime without unbounded memory growth. ## Context Switch Internals Context switching is performed entirely in inline assembly with both i386 and x86_64 code paths. ### 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 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) | The save and restore pointers are passed into the assembly block via GCC register constraints. 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** -- 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 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 tsdemo ```