Moved task switcher to this project.
This commit is contained in:
parent
8f03559dc7
commit
d041f93268
6 changed files with 2967 additions and 0 deletions
55
tasks/Makefile
Normal file
55
tasks/Makefile
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# Cooperative Task Switching Library Makefile for DJGPP cross-compilation
|
||||
|
||||
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
|
||||
DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib
|
||||
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
|
||||
AR = LD_LIBRARY_PATH=$(DJGPP_LIBPATH) $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-ar
|
||||
RANLIB = LD_LIBRARY_PATH=$(DJGPP_LIBPATH) $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-ranlib
|
||||
EXE2COFF = $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/exe2coff
|
||||
CWSDSTUB = $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/CWSDSTUB.EXE
|
||||
CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586
|
||||
|
||||
OBJDIR = ../obj/tasks
|
||||
LIBDIR = ../lib
|
||||
BINDIR = ../bin
|
||||
|
||||
SRCS = taskswitch.c
|
||||
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
|
||||
TARGET = $(LIBDIR)/libtasks.a
|
||||
|
||||
DEMO_SRCS = demo.c
|
||||
DEMO_OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(DEMO_SRCS))
|
||||
DEMO_TARGET = $(BINDIR)/tsdemo.exe
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET) $(DEMO_TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) | $(LIBDIR)
|
||||
$(AR) rcs $@ $(OBJS)
|
||||
$(RANLIB) $@
|
||||
|
||||
$(DEMO_TARGET): $(DEMO_OBJS) $(TARGET) | $(BINDIR)
|
||||
$(CC) $(CFLAGS) -o $@ $(DEMO_OBJS) -L$(LIBDIR) -ltasks
|
||||
$(EXE2COFF) $@
|
||||
cat $(CWSDSTUB) $(BINDIR)/tsdemo > $@
|
||||
rm -f $(BINDIR)/tsdemo
|
||||
|
||||
$(OBJDIR)/%.o: %.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR):
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
$(LIBDIR):
|
||||
mkdir -p $(LIBDIR)
|
||||
|
||||
$(BINDIR):
|
||||
mkdir -p $(BINDIR)
|
||||
|
||||
# Dependencies
|
||||
$(OBJDIR)/taskswitch.o: taskswitch.c taskswitch.h thirdparty/stb_ds.h
|
||||
$(OBJDIR)/demo.o: demo.c taskswitch.h
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET) $(DEMO_TARGET)
|
||||
299
tasks/README.md
Normal file
299
tasks/README.md
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
# 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 <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`.
|
||||
|
||||
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
|
||||
```
|
||||
133
tasks/demo.c
Normal file
133
tasks/demo.c
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
// demo.c -- Demonstration of the cooperative task switching library
|
||||
//
|
||||
// Shows priority scheduling, round-robin, pausing/resuming, dynamic
|
||||
// priority changes, and slot reuse after task termination.
|
||||
|
||||
#include <stdio.h>
|
||||
#include "taskswitch.h"
|
||||
|
||||
// Forward declarations
|
||||
static void pauseTarget(void *arg);
|
||||
static void shortLived(void *arg);
|
||||
static void worker(void *arg);
|
||||
|
||||
|
||||
static void pauseTarget(void *arg) {
|
||||
(void)arg;
|
||||
for (int32_t i = 0; i < 10; i++) {
|
||||
printf(" [pauser] iteration %d\n", (int)i);
|
||||
tsYield();
|
||||
}
|
||||
printf(" [pauser] finished\n");
|
||||
}
|
||||
|
||||
|
||||
static void shortLived(void *arg) {
|
||||
int32_t id = (int32_t)(intptr_t)arg;
|
||||
printf(" [short-%d] running (id=%d)\n", (int)id, (int)tsCurrentId());
|
||||
tsYield();
|
||||
printf(" [short-%d] done\n", (int)id);
|
||||
}
|
||||
|
||||
|
||||
static void worker(void *arg) {
|
||||
const char *name = (const char *)arg;
|
||||
int32_t pri = tsGetPriority(tsCurrentId());
|
||||
|
||||
for (int32_t i = 0; i < 5; i++) {
|
||||
printf(" [%s] step %d (priority %d)\n", name, (int)i, (int)pri);
|
||||
tsYield();
|
||||
}
|
||||
printf(" [%s] finished\n", name);
|
||||
}
|
||||
|
||||
|
||||
int main(void) {
|
||||
printf("=== Cooperative Task Switching Demo ===\n\n");
|
||||
|
||||
if (tsInit() != TS_OK) {
|
||||
printf("ERROR: failed to initialize task system\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
int32_t tHigh = tsCreate("high", worker, "high", 0, TS_PRIORITY_HIGH);
|
||||
int32_t tNorm = tsCreate("normal", worker, "normal", 0, TS_PRIORITY_NORMAL);
|
||||
int32_t tLow = tsCreate("low", worker, "low", 0, TS_PRIORITY_LOW);
|
||||
int32_t tPauser = tsCreate("pauser", pauseTarget, NULL, 0, TS_PRIORITY_NORMAL);
|
||||
|
||||
if (tHigh < 0 || tNorm < 0 || tLow < 0 || tPauser < 0) {
|
||||
printf("ERROR: failed to create tasks\n");
|
||||
tsShutdown();
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("Created: high=%d normal=%d low=%d pauser=%d\n\n", (int)tHigh, (int)tNorm, (int)tLow, (int)tPauser);
|
||||
|
||||
// Phase 1: all tasks run, but high-priority tasks get more turns per round.
|
||||
// Low (priority 0) gets 1 turn, normal (5) gets 6, high (10) gets 11.
|
||||
printf("--- Phase 1: Priority scheduling ---\n");
|
||||
for (int32_t i = 0; i < 6; i++) {
|
||||
printf("[main] yield %d\n", (int)i);
|
||||
tsYield();
|
||||
}
|
||||
|
||||
// Phase 2: pause the pauser task so it stops being scheduled
|
||||
printf("\n--- Phase 2: Pause 'pauser' ---\n");
|
||||
tsPause((uint32_t)tPauser);
|
||||
printf("[main] pauser state: %d (expect %d=paused)\n", tsGetState((uint32_t)tPauser), TaskStatePaused);
|
||||
for (int32_t i = 0; i < 4; i++) {
|
||||
printf("[main] yield %d (pauser is paused)\n", (int)i);
|
||||
tsYield();
|
||||
}
|
||||
|
||||
// Phase 3: resume pauser
|
||||
printf("\n--- Phase 3: Resume 'pauser' ---\n");
|
||||
tsResume((uint32_t)tPauser);
|
||||
for (int32_t i = 0; i < 4; i++) {
|
||||
printf("[main] yield %d\n", (int)i);
|
||||
tsYield();
|
||||
}
|
||||
|
||||
// Phase 4: boost low to the highest priority so it gets more turns
|
||||
printf("\n--- Phase 4: Boost 'low' to highest ---\n");
|
||||
tsSetPriority((uint32_t)tLow, TS_PRIORITY_HIGH + 5);
|
||||
for (int32_t i = 0; i < 6; i++) {
|
||||
printf("[main] yield %d\n", (int)i);
|
||||
tsYield();
|
||||
}
|
||||
|
||||
// Let remaining tasks finish
|
||||
printf("\n--- Draining remaining tasks ---\n");
|
||||
while (tsActiveCount() > 1) {
|
||||
tsYield();
|
||||
}
|
||||
|
||||
// Phase 5: dynamic creation with slot reuse
|
||||
printf("\n--- Phase 5: Slot reuse ---\n");
|
||||
printf("[main] active before: %u\n", (unsigned)tsActiveCount());
|
||||
|
||||
// Create short-lived tasks that will terminate and free their slots
|
||||
for (int32_t wave = 0; wave < 3; wave++) {
|
||||
printf("[main] wave %d: creating 3 tasks\n", (int)wave);
|
||||
int32_t ids[3];
|
||||
for (int32_t j = 0; j < 3; j++) {
|
||||
int32_t tag = wave * 3 + j;
|
||||
char name[TS_NAME_MAX];
|
||||
snprintf(name, sizeof(name), "short-%d", (int)tag);
|
||||
ids[j] = tsCreate(name, shortLived, (void *)(intptr_t)tag, 0, TS_PRIORITY_NORMAL);
|
||||
printf("[main] created id=%d\n", (int)ids[j]);
|
||||
}
|
||||
|
||||
// Let them all finish
|
||||
while (tsActiveCount() > 1) {
|
||||
tsYield();
|
||||
}
|
||||
printf("[main] wave %d complete, active=%u\n", (int)wave, (unsigned)tsActiveCount());
|
||||
}
|
||||
|
||||
printf("\nActive tasks: %u\n", (unsigned)tsActiveCount());
|
||||
printf("=== Demo complete ===\n");
|
||||
|
||||
tsShutdown();
|
||||
return 0;
|
||||
}
|
||||
500
tasks/taskswitch.c
Normal file
500
tasks/taskswitch.c
Normal file
|
|
@ -0,0 +1,500 @@
|
|||
// taskswitch.c -- Cooperative task switching library for DJGPP
|
||||
//
|
||||
// Uses inline assembly for context switching (i386 and x86_64). The
|
||||
// scheduler uses credit-based weighted round-robin so all tasks run,
|
||||
// but higher-priority tasks run proportionally more often.
|
||||
//
|
||||
// Task storage is a stb_ds dynamic array that grows as needed.
|
||||
// Terminated task slots are recycled by tsCreate().
|
||||
|
||||
#define STB_DS_IMPLEMENTATION
|
||||
#include "thirdparty/stb_ds.h"
|
||||
|
||||
#include "taskswitch.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
// ============================================================================
|
||||
// Internal types
|
||||
// ============================================================================
|
||||
|
||||
#if defined(__x86_64__)
|
||||
// Saved CPU context for x86_64 (field order matches asm byte offsets)
|
||||
typedef struct {
|
||||
uintptr_t rbx; // offset 0
|
||||
uintptr_t r12; // offset 8
|
||||
uintptr_t r13; // offset 16
|
||||
uintptr_t r14; // offset 24
|
||||
uintptr_t r15; // offset 32
|
||||
uintptr_t rbp; // offset 40
|
||||
uintptr_t rsp; // offset 48
|
||||
uintptr_t rip; // offset 56
|
||||
} TaskContextT;
|
||||
#else
|
||||
// Saved CPU context for i386 (field order matches asm byte offsets)
|
||||
typedef struct {
|
||||
uintptr_t ebx; // offset 0
|
||||
uintptr_t esi; // offset 4
|
||||
uintptr_t edi; // offset 8
|
||||
uintptr_t ebp; // offset 12
|
||||
uintptr_t esp; // offset 16
|
||||
uintptr_t eip; // offset 20
|
||||
} TaskContextT;
|
||||
#endif
|
||||
|
||||
// Task control block
|
||||
typedef struct {
|
||||
char name[TS_NAME_MAX];
|
||||
TaskContextT context;
|
||||
uint8_t *stack;
|
||||
uint32_t stackSize;
|
||||
TaskStateE state;
|
||||
int32_t priority;
|
||||
int32_t credits;
|
||||
TaskEntryT entry;
|
||||
void *arg;
|
||||
bool isMain;
|
||||
bool allocated; // true if slot is in use, false if free for reuse
|
||||
} TaskBlockT;
|
||||
|
||||
// ============================================================================
|
||||
// Module state
|
||||
// ============================================================================
|
||||
|
||||
static TaskBlockT *tasks = NULL; // stb_ds dynamic array
|
||||
static uint32_t currentIdx = 0;
|
||||
static bool initialized = false;
|
||||
|
||||
// ============================================================================
|
||||
// Forward declarations
|
||||
// ============================================================================
|
||||
|
||||
// Static helpers
|
||||
static void contextSwitch(TaskContextT *save, TaskContextT *restore);
|
||||
static int32_t findFreeSlot(void);
|
||||
static uint32_t scheduleNext(void);
|
||||
static void taskTrampoline(void);
|
||||
|
||||
// Public API prototypes are provided by taskswitch.h via #include.
|
||||
// Explicit prototypes repeated here per project convention:
|
||||
uint32_t tsActiveCount(void);
|
||||
int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t stackSize, int32_t priority);
|
||||
uint32_t tsCurrentId(void);
|
||||
void tsExit(void);
|
||||
const char *tsGetName(uint32_t taskId);
|
||||
int32_t tsGetPriority(uint32_t taskId);
|
||||
TaskStateE tsGetState(uint32_t taskId);
|
||||
int32_t tsInit(void);
|
||||
int32_t tsPause(uint32_t taskId);
|
||||
int32_t tsResume(uint32_t taskId);
|
||||
int32_t tsSetPriority(uint32_t taskId, int32_t priority);
|
||||
void tsShutdown(void);
|
||||
void tsYield(void);
|
||||
|
||||
// ============================================================================
|
||||
// Static functions (alphabetical)
|
||||
// ============================================================================
|
||||
|
||||
// Switch execution from the current task to another by saving and restoring
|
||||
// callee-saved registers and the stack pointer. The return address is
|
||||
// captured as a local label so that when another task switches back to us,
|
||||
// execution resumes right after the save point.
|
||||
#if defined(__x86_64__)
|
||||
// x86_64: save rbx, r12-r15, rbp, rsp, rip.
|
||||
// Inputs via GCC constraints: %rdi = save ptr, %rsi = restore ptr.
|
||||
static void __attribute__((noinline)) contextSwitch(TaskContextT *save, TaskContextT *restore) {
|
||||
__asm__ __volatile__(
|
||||
// Save current context
|
||||
"movq %%rbx, 0(%%rdi)\n\t"
|
||||
"movq %%r12, 8(%%rdi)\n\t"
|
||||
"movq %%r13, 16(%%rdi)\n\t"
|
||||
"movq %%r14, 24(%%rdi)\n\t"
|
||||
"movq %%r15, 32(%%rdi)\n\t"
|
||||
"movq %%rbp, 40(%%rdi)\n\t"
|
||||
"movq %%rsp, 48(%%rdi)\n\t"
|
||||
"leaq 1f(%%rip), %%rax\n\t"
|
||||
"movq %%rax, 56(%%rdi)\n\t"
|
||||
// Restore new context
|
||||
"movq 0(%%rsi), %%rbx\n\t"
|
||||
"movq 8(%%rsi), %%r12\n\t"
|
||||
"movq 16(%%rsi), %%r13\n\t"
|
||||
"movq 24(%%rsi), %%r14\n\t"
|
||||
"movq 32(%%rsi), %%r15\n\t"
|
||||
"movq 40(%%rsi), %%rbp\n\t"
|
||||
"movq 48(%%rsi), %%rsp\n\t"
|
||||
"movq 56(%%rsi), %%rax\n\t"
|
||||
"jmp *%%rax\n\t"
|
||||
"1:\n\t"
|
||||
:
|
||||
: "D" (save), "S" (restore)
|
||||
: "rax", "rcx", "rdx", "r8", "r9", "r10", "r11", "memory", "cc"
|
||||
);
|
||||
}
|
||||
#else
|
||||
// i386: save ebx, esi, edi, ebp, esp, eip.
|
||||
// Inputs via GCC constraints: %eax = save ptr, %edx = restore ptr.
|
||||
static void __attribute__((noinline)) contextSwitch(TaskContextT *save, TaskContextT *restore) {
|
||||
__asm__ __volatile__(
|
||||
// Save current context
|
||||
"movl %%ebx, 0(%%eax)\n\t"
|
||||
"movl %%esi, 4(%%eax)\n\t"
|
||||
"movl %%edi, 8(%%eax)\n\t"
|
||||
"movl %%ebp, 12(%%eax)\n\t"
|
||||
"movl %%esp, 16(%%eax)\n\t"
|
||||
"movl $1f, 20(%%eax)\n\t"
|
||||
// Restore new context
|
||||
"movl 0(%%edx), %%ebx\n\t"
|
||||
"movl 4(%%edx), %%esi\n\t"
|
||||
"movl 8(%%edx), %%edi\n\t"
|
||||
"movl 12(%%edx), %%ebp\n\t"
|
||||
"movl 16(%%edx), %%esp\n\t"
|
||||
"movl 20(%%edx), %%eax\n\t"
|
||||
"jmp *%%eax\n\t"
|
||||
"1:\n\t"
|
||||
:
|
||||
: "a" (save), "d" (restore)
|
||||
: "ecx", "memory", "cc"
|
||||
);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
// Find a free (terminated or unallocated) slot in the task array.
|
||||
// Returns the index, or -1 if no free slot exists.
|
||||
static int32_t findFreeSlot(void) {
|
||||
ptrdiff_t count = arrlen(tasks);
|
||||
for (ptrdiff_t i = 1; i < count; i++) {
|
||||
if (!tasks[i].allocated) {
|
||||
return (int32_t)i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
// Find the next task to run using credit-based weighted round-robin.
|
||||
// Each ready task holds (priority + 1) credits. One credit is consumed
|
||||
// per scheduling turn. When no ready task has credits left, every
|
||||
// ready task is refilled. This guarantees all tasks run while giving
|
||||
// higher-priority tasks proportionally more turns.
|
||||
static uint32_t scheduleNext(void) {
|
||||
uint32_t count = (uint32_t)arrlen(tasks);
|
||||
|
||||
// First pass: look for a ready task with remaining credits
|
||||
for (uint32_t i = 1; i <= count; i++) {
|
||||
uint32_t idx = (currentIdx + i) % count;
|
||||
if (tasks[idx].allocated && tasks[idx].state == TaskStateReady && tasks[idx].credits > 0) {
|
||||
tasks[idx].credits--;
|
||||
return idx;
|
||||
}
|
||||
}
|
||||
|
||||
// All credits exhausted -- refill every ready task
|
||||
bool anyReady = false;
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
if (tasks[i].allocated && tasks[i].state == TaskStateReady) {
|
||||
tasks[i].credits = tasks[i].priority + 1;
|
||||
anyReady = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!anyReady) {
|
||||
return currentIdx;
|
||||
}
|
||||
|
||||
// Pick the first ready task after refill
|
||||
for (uint32_t i = 1; i <= count; i++) {
|
||||
uint32_t idx = (currentIdx + i) % count;
|
||||
if (tasks[idx].allocated && tasks[idx].state == TaskStateReady && tasks[idx].credits > 0) {
|
||||
tasks[idx].credits--;
|
||||
return idx;
|
||||
}
|
||||
}
|
||||
|
||||
return currentIdx;
|
||||
}
|
||||
|
||||
|
||||
// Entry point for every new task. Calls the user-supplied function and
|
||||
// then terminates the task when it returns.
|
||||
static void taskTrampoline(void) {
|
||||
TaskBlockT *task = &tasks[currentIdx];
|
||||
task->entry(task->arg);
|
||||
tsExit();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Public API (alphabetical, main-equivalent functions last if applicable)
|
||||
// ============================================================================
|
||||
|
||||
uint32_t tsActiveCount(void) {
|
||||
if (!initialized) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t active = 0;
|
||||
ptrdiff_t count = arrlen(tasks);
|
||||
for (ptrdiff_t i = 0; i < count; i++) {
|
||||
if (tasks[i].allocated && tasks[i].state != TaskStateTerminated) {
|
||||
active++;
|
||||
}
|
||||
}
|
||||
return active;
|
||||
}
|
||||
|
||||
|
||||
int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t stackSize, int32_t priority) {
|
||||
if (!initialized || !entry) {
|
||||
return TS_ERR_PARAM;
|
||||
}
|
||||
if (stackSize == 0) {
|
||||
stackSize = TS_DEFAULT_STACK_SIZE;
|
||||
}
|
||||
|
||||
// Reuse a terminated/free slot, or append a new one
|
||||
int32_t id = findFreeSlot();
|
||||
if (id < 0) {
|
||||
TaskBlockT blank = {0};
|
||||
arrput(tasks, blank);
|
||||
id = (int32_t)(arrlen(tasks) - 1);
|
||||
}
|
||||
|
||||
TaskBlockT *task = &tasks[id];
|
||||
memset(task, 0, sizeof(*task));
|
||||
|
||||
task->stack = (uint8_t *)malloc(stackSize);
|
||||
if (!task->stack) {
|
||||
return TS_ERR_NOMEM;
|
||||
}
|
||||
|
||||
if (name) {
|
||||
strncpy(task->name, name, TS_NAME_MAX - 1);
|
||||
task->name[TS_NAME_MAX - 1] = '\0';
|
||||
}
|
||||
|
||||
task->stackSize = stackSize;
|
||||
task->state = TaskStateReady;
|
||||
task->priority = priority;
|
||||
task->credits = priority + 1;
|
||||
task->entry = entry;
|
||||
task->arg = arg;
|
||||
task->isMain = false;
|
||||
task->allocated = true;
|
||||
|
||||
// Set up initial stack (grows downward, 16-byte aligned)
|
||||
uintptr_t top = (uintptr_t)(task->stack + stackSize);
|
||||
top &= ~(uintptr_t)0xF;
|
||||
top -= sizeof(uintptr_t);
|
||||
*(uintptr_t *)top = 0; // dummy return address; trampoline never returns
|
||||
|
||||
#if defined(__x86_64__)
|
||||
task->context.rsp = top;
|
||||
task->context.rbp = 0;
|
||||
task->context.rbx = 0;
|
||||
task->context.r12 = 0;
|
||||
task->context.r13 = 0;
|
||||
task->context.r14 = 0;
|
||||
task->context.r15 = 0;
|
||||
task->context.rip = (uintptr_t)taskTrampoline;
|
||||
#else
|
||||
task->context.esp = top;
|
||||
task->context.ebp = 0;
|
||||
task->context.ebx = 0;
|
||||
task->context.esi = 0;
|
||||
task->context.edi = 0;
|
||||
task->context.eip = (uintptr_t)taskTrampoline;
|
||||
#endif
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
|
||||
uint32_t tsCurrentId(void) {
|
||||
return currentIdx;
|
||||
}
|
||||
|
||||
|
||||
void tsExit(void) {
|
||||
if (!initialized || tasks[currentIdx].isMain) {
|
||||
return;
|
||||
}
|
||||
|
||||
tasks[currentIdx].state = TaskStateTerminated;
|
||||
|
||||
// Free the stack immediately
|
||||
free(tasks[currentIdx].stack);
|
||||
tasks[currentIdx].stack = NULL;
|
||||
tasks[currentIdx].allocated = false;
|
||||
|
||||
uint32_t next = scheduleNext();
|
||||
uint32_t prev = currentIdx;
|
||||
|
||||
currentIdx = next;
|
||||
tasks[next].state = TaskStateRunning;
|
||||
|
||||
contextSwitch(&tasks[prev].context, &tasks[next].context);
|
||||
// Terminated task never resumes here
|
||||
}
|
||||
|
||||
|
||||
const char *tsGetName(uint32_t taskId) {
|
||||
if (!initialized || taskId >= (uint32_t)arrlen(tasks)) {
|
||||
return NULL;
|
||||
}
|
||||
if (!tasks[taskId].allocated) {
|
||||
return NULL;
|
||||
}
|
||||
return tasks[taskId].name;
|
||||
}
|
||||
|
||||
|
||||
int32_t tsGetPriority(uint32_t taskId) {
|
||||
if (!initialized || taskId >= (uint32_t)arrlen(tasks)) {
|
||||
return TS_ERR_PARAM;
|
||||
}
|
||||
if (!tasks[taskId].allocated) {
|
||||
return TS_ERR_PARAM;
|
||||
}
|
||||
return tasks[taskId].priority;
|
||||
}
|
||||
|
||||
|
||||
TaskStateE tsGetState(uint32_t taskId) {
|
||||
if (!initialized || taskId >= (uint32_t)arrlen(tasks)) {
|
||||
return TaskStateTerminated;
|
||||
}
|
||||
if (!tasks[taskId].allocated) {
|
||||
return TaskStateTerminated;
|
||||
}
|
||||
return tasks[taskId].state;
|
||||
}
|
||||
|
||||
|
||||
int32_t tsInit(void) {
|
||||
if (initialized) {
|
||||
return TS_ERR_PARAM;
|
||||
}
|
||||
|
||||
// Start with the main task at slot 0
|
||||
TaskBlockT main = {0};
|
||||
strncpy(main.name, "main", TS_NAME_MAX - 1);
|
||||
main.state = TaskStateRunning;
|
||||
main.priority = TS_PRIORITY_NORMAL;
|
||||
main.credits = TS_PRIORITY_NORMAL + 1;
|
||||
main.isMain = true;
|
||||
main.stack = NULL;
|
||||
main.allocated = true;
|
||||
|
||||
arrput(tasks, main);
|
||||
|
||||
currentIdx = 0;
|
||||
initialized = true;
|
||||
return TS_OK;
|
||||
}
|
||||
|
||||
|
||||
int32_t tsPause(uint32_t taskId) {
|
||||
if (!initialized || taskId >= (uint32_t)arrlen(tasks)) {
|
||||
return TS_ERR_PARAM;
|
||||
}
|
||||
if (!tasks[taskId].allocated) {
|
||||
return TS_ERR_PARAM;
|
||||
}
|
||||
if (tasks[taskId].isMain) {
|
||||
return TS_ERR_STATE;
|
||||
}
|
||||
if (tasks[taskId].state != TaskStateRunning && tasks[taskId].state != TaskStateReady) {
|
||||
return TS_ERR_STATE;
|
||||
}
|
||||
|
||||
tasks[taskId].state = TaskStatePaused;
|
||||
|
||||
// If we paused ourselves, yield immediately
|
||||
if (taskId == currentIdx) {
|
||||
uint32_t next = scheduleNext();
|
||||
if (next != currentIdx) {
|
||||
uint32_t prev = currentIdx;
|
||||
|
||||
currentIdx = next;
|
||||
tasks[next].state = TaskStateRunning;
|
||||
|
||||
contextSwitch(&tasks[prev].context, &tasks[next].context);
|
||||
}
|
||||
}
|
||||
|
||||
return TS_OK;
|
||||
}
|
||||
|
||||
|
||||
int32_t tsResume(uint32_t taskId) {
|
||||
if (!initialized || taskId >= (uint32_t)arrlen(tasks)) {
|
||||
return TS_ERR_PARAM;
|
||||
}
|
||||
if (!tasks[taskId].allocated) {
|
||||
return TS_ERR_PARAM;
|
||||
}
|
||||
if (tasks[taskId].state != TaskStatePaused) {
|
||||
return TS_ERR_STATE;
|
||||
}
|
||||
|
||||
tasks[taskId].state = TaskStateReady;
|
||||
tasks[taskId].credits = tasks[taskId].priority + 1;
|
||||
return TS_OK;
|
||||
}
|
||||
|
||||
|
||||
int32_t tsSetPriority(uint32_t taskId, int32_t priority) {
|
||||
if (!initialized || taskId >= (uint32_t)arrlen(tasks)) {
|
||||
return TS_ERR_PARAM;
|
||||
}
|
||||
if (!tasks[taskId].allocated) {
|
||||
return TS_ERR_PARAM;
|
||||
}
|
||||
if (tasks[taskId].state == TaskStateTerminated) {
|
||||
return TS_ERR_STATE;
|
||||
}
|
||||
|
||||
tasks[taskId].priority = priority;
|
||||
tasks[taskId].credits = priority + 1;
|
||||
return TS_OK;
|
||||
}
|
||||
|
||||
|
||||
void tsShutdown(void) {
|
||||
if (!initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
ptrdiff_t count = arrlen(tasks);
|
||||
for (ptrdiff_t i = 0; i < count; i++) {
|
||||
free(tasks[i].stack);
|
||||
}
|
||||
|
||||
arrfree(tasks);
|
||||
tasks = NULL;
|
||||
currentIdx = 0;
|
||||
initialized = false;
|
||||
}
|
||||
|
||||
|
||||
void tsYield(void) {
|
||||
if (!initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t next = scheduleNext();
|
||||
if (next == currentIdx) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t prev = currentIdx;
|
||||
|
||||
if (tasks[prev].state == TaskStateRunning) {
|
||||
tasks[prev].state = TaskStateReady;
|
||||
}
|
||||
|
||||
currentIdx = next;
|
||||
tasks[next].state = TaskStateRunning;
|
||||
|
||||
contextSwitch(&tasks[prev].context, &tasks[next].context);
|
||||
}
|
||||
85
tasks/taskswitch.h
Normal file
85
tasks/taskswitch.h
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
// taskswitch.h -- Cooperative task switching library for DJGPP
|
||||
//
|
||||
// Credit-based cooperative multitasking with task pausing.
|
||||
// Each task receives (priority + 1) credits per scheduling round.
|
||||
// Tasks run round-robin, consuming one credit per turn. When all
|
||||
// credits are spent, every ready task is refilled. Higher-priority
|
||||
// tasks run proportionally more often but never starve lower ones.
|
||||
|
||||
#ifndef TASKSWITCH_H
|
||||
#define TASKSWITCH_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
// Error codes
|
||||
#define TS_OK 0
|
||||
#define TS_ERR_INIT (-1)
|
||||
#define TS_ERR_PARAM (-2)
|
||||
#define TS_ERR_FULL (-3)
|
||||
#define TS_ERR_NOMEM (-4)
|
||||
#define TS_ERR_STATE (-5)
|
||||
|
||||
// Defaults
|
||||
#define TS_DEFAULT_STACK_SIZE 8192
|
||||
#define TS_NAME_MAX 32
|
||||
|
||||
// Priority levels
|
||||
#define TS_PRIORITY_LOW 0
|
||||
#define TS_PRIORITY_NORMAL 5
|
||||
#define TS_PRIORITY_HIGH 10
|
||||
|
||||
// Task states
|
||||
typedef enum {
|
||||
TaskStateReady = 0,
|
||||
TaskStateRunning = 1,
|
||||
TaskStatePaused = 2,
|
||||
TaskStateTerminated = 3
|
||||
} TaskStateE;
|
||||
|
||||
// Task entry function signature
|
||||
typedef void (*TaskEntryT)(void *arg);
|
||||
|
||||
// Initialize the task system. The calling context becomes task 0 (main).
|
||||
// The task array grows dynamically as tasks are created.
|
||||
int32_t tsInit(void);
|
||||
|
||||
// Shut down the task system and free all resources.
|
||||
void tsShutdown(void);
|
||||
|
||||
// Create a new task. Pass 0 for stackSize to use TS_DEFAULT_STACK_SIZE.
|
||||
// Returns the task ID (>= 0) on success, or a negative error code.
|
||||
int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t stackSize, int32_t priority);
|
||||
|
||||
// Yield CPU to the next eligible ready task (credit-based round-robin).
|
||||
void tsYield(void);
|
||||
|
||||
// Pause a task. Cannot pause the main task (id 0).
|
||||
// If a task pauses itself, an implicit yield occurs.
|
||||
int32_t tsPause(uint32_t taskId);
|
||||
|
||||
// Resume a paused task.
|
||||
int32_t tsResume(uint32_t taskId);
|
||||
|
||||
// Set a task's scheduling priority.
|
||||
int32_t tsSetPriority(uint32_t taskId, int32_t priority);
|
||||
|
||||
// Get a task's priority, or TS_ERR_PARAM on invalid id.
|
||||
int32_t tsGetPriority(uint32_t taskId);
|
||||
|
||||
// Get a task's current state.
|
||||
TaskStateE tsGetState(uint32_t taskId);
|
||||
|
||||
// Get the currently running task's ID.
|
||||
uint32_t tsCurrentId(void);
|
||||
|
||||
// Get a task's name, or NULL on invalid id.
|
||||
const char *tsGetName(uint32_t taskId);
|
||||
|
||||
// Terminate the calling task. Must not be called from the main task.
|
||||
void tsExit(void);
|
||||
|
||||
// Get the number of non-terminated tasks.
|
||||
uint32_t tsActiveCount(void);
|
||||
|
||||
#endif // TASKSWITCH_H
|
||||
1895
tasks/thirdparty/stb_ds.h
vendored
Normal file
1895
tasks/thirdparty/stb_ds.h
vendored
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue