11 KiB
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
#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
-
tsInit(maxTasks)-- Allocate the task table. The calling context (typicallymain) becomes task 0 withTS_PRIORITY_NORMAL.maxTasksis the total capacity including this main task slot. -
tsCreate(...)-- Create tasks. Each gets a name, entry function, argument pointer, stack size (0 for the 8 KB default), and a priority. -
tsYield()-- Call from any task (including main) to hand the CPU to the next eligible task. -
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
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
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.
- Every ready task holds a credit counter initialised to
priority + 1. - 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. - When no ready task has credits remaining, every ready task is
refilled to
priority + 1and 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 withpriority + 1. - A task is resumed (
tsResume) -- refilled so it is not penalised. - A task's priority changes (
tsSetPriority) -- reset tonew + 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()(ortsPause/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
maxTasksvalue passed totsInitis a lifetime limit. - Not interrupt-safe -- the library uses no locking or
volatilemodule 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:
- Priority scheduling -- creates tasks at low, normal, and high priority. All tasks run, but the high-priority task gets significantly more turns.
- Pause -- pauses one task mid-run and shows it stops being scheduled.
- Resume -- resumes the paused task and shows it picks up where it left off.
- Priority boost -- raises the low-priority task above all others and shows it immediately gets more turns.
Build and run:
make
demo