Skip to content

Instantly share code, notes, and snippets.

@musteresel
Last active August 24, 2024 21:27
Show Gist options
  • Save musteresel/f7c33e9b5236b0af8d990303246224db to your computer and use it in GitHub Desktop.
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
// 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");
}
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)!
;; 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