Last active
August 24, 2024 21:27
-
-
Save musteresel/f7c33e9b5236b0af8d990303246224db to your computer and use it in GitHub Desktop.
Cooperative multitasking / green threads in C; MIT licence and let me know with a comment if you're using this
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Cooperative scheduling using setjmp and longjmp | |
// | |
// This is my attempt at writing a cooperative scheduling library in | |
// as much pure, standard compliant C code as possible. It uses just | |
// a single non-C function which needs to be implemented in Assembly | |
// for each platform / target / calling convention. | |
// | |
// Author: Daniel Jour | |
// ------------------------------------------------------------------- | |
// START of "library" code | |
// ------------------------------------------------------------------- | |
// _FORTIFY_SOURCE enables a check in longjmp that disallows jumping | |
// "down" the stack. This is usefull in a scenario where longjmp | |
// jumps only within one stack, but breaks when jumping from one stack | |
// to another (as one will inevitably have a lower address than the | |
// other). | |
// | |
// This is the commit that introduced the check: | |
// | |
// https://github.com/bminor/glibc/commit/b50f8e42ba3010f0141e6a482e0820f658e89b63 | |
// | |
// This is the commit which made longjmp OS-specific (moved the code around quite a bit): | |
// | |
// https://github.com/bminor/glibc/commit/98b1e6c8668259044a20a016a5a5957b226ce04b | |
// | |
// And finally, here the actual check / jump to the failure routine: | |
// | |
// https://github.com/bminor/glibc/blob/master/sysdeps/unix/sysv/linux/x86_64/____longjmp_chk.S#L105 | |
// | |
// Disabling _FORTIFY_SOURCE (level 1 is not sufficient, needs to be | |
// disabled) doesn't use above longjmp_chk, but rather a version | |
// without the check. | |
// | |
#undef _FORTIFY_SOURCE | |
#include <assert.h> | |
#include <setjmp.h> | |
#include <stdio.h> | |
#include <stdlib.h> | |
#include <time.h> | |
#define STACK_SIZE 2000 | |
// A task structure, storing the state of a task. Contains a "next" | |
// pointer to enable building task lists. | |
// | |
struct Task { | |
// Human friendly name, purely for debugging purposes atm. | |
// | |
char * name; | |
// The stack of the task. | |
// | |
char stack[STACK_SIZE]; | |
// Saved context of the task. | |
// | |
jmp_buf env; | |
// For building task lists. | |
// | |
struct Task * next; | |
}; | |
// Global context of the scheduler, longjmp to this context puts us in | |
// the scheduler. | |
// | |
jmp_buf scheduler; | |
// Global state keeping track of the current task, used to save | |
// context. This is a CIRCULAR list! currentTask is the head of the | |
// list. The last item in the list points to currentTask again! | |
// | |
struct Task * currentTask = NULL; | |
// Currently not used as intended, list of tasks which have completed | |
// and can be freed. Tasks cannot be freed while they're still | |
// running (even in their "teardown" phase) because they still need | |
// the stack... | |
// | |
struct Task * tasksToFree = NULL; | |
// Yield the current task, go back to the scheduler and let another | |
// task (or the same, if no other tasks available) run. | |
// | |
void Yield(void) | |
{ | |
if (setjmp(currentTask->env) == 0) | |
{ | |
longjmp(scheduler, 1); | |
} | |
} | |
// The Scheduler, sets the context of the scheduler and then selects a | |
// next task as long as there are tasks to run. | |
// | |
void Schedule(void) | |
{ | |
setjmp(scheduler); | |
if (currentTask) | |
{ | |
// printf("Switching tasks... from %s to %s\n", currentTask->name, currentTask->next->name); | |
currentTask = currentTask->next; | |
longjmp(currentTask->env, 1); | |
} | |
puts("Schedule() returns"); | |
} | |
// External, hardware dependent function. Changes to the new_stack | |
// and calls TaskStart, passing Function and user_data along. | |
// | |
extern void TaskStartOnNewStack(void * new_stack, | |
void (*Function)(void *), | |
void * user_data); | |
// This runs already on the new stack of a task, it's similar to | |
// _start. It jumps to the user provided Function, passing user_data. | |
// When that function returns this function takes care of teardown of | |
// the task. This function must NOT return! | |
// | |
void TaskStart(void (*Function)(void *), void * user_data) | |
{ | |
Function(user_data); | |
struct Task * me = currentTask; | |
if (currentTask->next == currentTask) | |
{ | |
currentTask = NULL; | |
} | |
else | |
{ | |
for (;currentTask->next != me;currentTask = currentTask->next); | |
assert(currentTask->next == me); | |
currentTask->next = me->next; | |
} | |
me->next = tasksToFree; | |
tasksToFree = me; | |
longjmp(scheduler, 1); | |
assert(0); // Not reached | |
} | |
// Start a new (or the first) task. | |
// | |
void StartTask(char * name, | |
void (*Function)(void *), | |
void * user_data) | |
{ | |
printf("StartTask %s\n", name); | |
struct Task * task = malloc(sizeof *task); | |
assert(task); | |
task->name = name; | |
if (currentTask) | |
{ | |
if (setjmp(currentTask->env) == 0) | |
{ | |
task->next = currentTask; | |
struct Task * prev = currentTask; | |
for (; prev->next != currentTask; prev = prev->next); | |
prev->next = task; | |
currentTask = task; | |
TaskStartOnNewStack(task->stack + STACK_SIZE, | |
Function, | |
user_data); | |
} | |
else | |
{ | |
// New task started, continue this task | |
} | |
} | |
else | |
{ | |
currentTask = task; | |
task->next = task; | |
TaskStartOnNewStack(task->stack + STACK_SIZE, | |
Function, | |
user_data); | |
} | |
} | |
// ------------------------------------------------------------------- | |
// END of "Library code", START of example | |
// ------------------------------------------------------------------- | |
// A task which waits 10 seconds, printing "Waiting" every second. | |
// | |
void taskSeconds(void * data) | |
{ | |
time_t start = time(NULL); | |
while ((time(NULL) - start) < 10) | |
{ | |
time_t s = time(NULL); | |
while ((time(NULL) - s) < 1) | |
{ | |
Yield(); | |
} | |
puts("Waiting"); | |
} | |
} | |
// A task which waits 12 seconds, printing "Waiting also" every 3 | |
// seconds. | |
// | |
void taskMoreSeconds(void * data) | |
{ | |
time_t start = time(NULL); | |
while ((time(NULL) - start) < 12) | |
{ | |
time_t s = time(NULL); | |
while ((time(NULL) - s) < 3) | |
{ | |
Yield(); | |
} | |
puts("Waiting also"); | |
} | |
} | |
// Just one dummy task. | |
// | |
void task1(void * data) | |
{ | |
int * ip = data; | |
printf("task1(%d)\n", *ip); | |
StartTask("seconds", taskSeconds, NULL); | |
StartTask("moreSeconds", taskMoreSeconds, NULL); | |
Yield(); | |
} | |
// Global state for the dummy task above. Cannot be on the stack of | |
// task0 because task0 might finish before task1! | |
// | |
int x = 21; | |
// Another dummy task. | |
// | |
void task0(void * data) | |
{ | |
int * ip = data; | |
printf("get ready to yield\n"); | |
Yield(); | |
StartTask("1", task1, &x); | |
Yield(); | |
printf("task0(%d)\n", *ip); | |
} | |
// Originally this was the main function; renamed to mains just for | |
// test purposes! | |
// | |
// There's one thing that's not yet ok as far as I understand things: | |
// Schedule() sets up the target of the longjmp(scheduler) calls, and | |
// then returns. This means that *technically* jumping to the | |
// scheduler is undefined behavior! It works nonetheless, probably | |
// because Schedule() doesn't need the stack (no local variables etc). | |
// | |
// It has one sideeffect, though: When Schedule() returns the second | |
// time (when all tasks have finished), it's like returning from | |
// StartTask() in this function. In other words, it then prints | |
// "Done". | |
// | |
// This needs to be refactored so that there's a special | |
// StartFirstTask() which contains the Schedule() functionality, too. | |
// | |
int mains() | |
{ | |
int i = 42; | |
Schedule(); | |
printf("After Schedule()\n"); | |
StartTask("0", task0, &i); | |
printf("Done\n"); | |
return 0; | |
} | |
// See above, mostly testing the stack layout after the Scheduler | |
// returns the second time. | |
// | |
int main() { | |
puts("Start"); | |
mains(); | |
printf("End\n"); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
How to build on an x86-64 linux system: | |
nasm -f elf64 hal.s | |
gcc -Wall -Wextra -c coop-sched.c | |
gcc -o tryit *.o | |
./tryit | |
There's still one bug, more in the comments of the source file(s)! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;; Implementation for x86-64, works on linux gcc / clang, not | |
;; tested with MSVC calling conventions! | |
section .text | |
global TaskStartOnNewStack | |
extern TaskStart | |
TaskStartOnNewStack: | |
;; rdi -> void * new_stack | |
;; rsi -> void (*Function)(void *) | |
;; rdx -> void * user_data | |
mov rsp, rdi | |
mov rdi, rsi | |
mov rsi, rdx | |
call TaskStart | |
;; NOT REACHED --- hopefully! |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment