commit 9a630c179e45cbf574f3613efce2f5d0c507031b
parent ba46ad17f18ed4f0afd3bfe6d924a137e5ecc42d
Author: Sylvia Ivory <git@sivory.net>
Date: Sat, 24 Jan 2026 22:39:02 -0800
Use non-global interface for scheduler
Diffstat:
3 files changed, 152 insertions(+), 185 deletions(-)
diff --git a/src/coroutine.zig b/src/coroutine.zig
@@ -1,167 +0,0 @@
-const std = @import("std");
-const Allocator = std.mem.Allocator;
-
-// tid zero is reserved for scheduler
-var tid_counter: usize = 1;
-// TODO; should we use PriorityQueue?
-var tasks: std.ArrayList(*Task) = undefined;
-var scheduler_task: *Task = undefined;
-
-const Task = struct {
- tid: usize,
-
- func: ?*const fn (?*anyopaque) callconv(.c) void,
- arg: ?*anyopaque,
-
- saved_sp: *usize,
- // r0-r3 are caller-saved and so we don't need to store them
- // r4-r11 are callee-saved
- // r12 is scratch
- // r13 is sp
- // r14 is lr
- // r15 is pc
- stack: [1024]usize align(8),
-};
-
-fn alloc_id() usize {
- const id = tid_counter;
- tid_counter += 1;
- return id;
-}
-
-fn push_stack(sp: *usize, value: usize) *usize {
- const sp_new: *usize = @ptrFromInt(@intFromPtr(sp) - @sizeOf(usize));
- sp_new.* = value;
- return sp_new;
-}
-
-pub fn initialize(allocator: Allocator) !void {
- tasks = try .initCapacity(allocator, 0);
-}
-
-pub fn fork(allocator: Allocator, f: *const fn (?*anyopaque) callconv(.c) void, arg: ?*anyopaque) !void {
- var t = try allocator.create(Task);
-
- t.tid = alloc_id();
- t.func = f;
- t.arg = arg;
-
- // We should *hopefully* already be aligned to 8 bytes
- const stack_top = @intFromPtr(&t.stack) + t.stack.len;
- var sp: *usize = @ptrFromInt(stack_top);
- // But might as well check for safety
- std.debug.assertAligned(sp, .@"8");
-
- // r4-r11
- sp = push_stack(sp, @intFromPtr(&trampoline));
- for (4..12) |v| {
- sp = push_stack(sp, 15 - v);
- }
-
- t.saved_sp = sp;
-
- _ = try tasks.append(allocator, t);
-}
-
-pub noinline fn join(allocator: Allocator) !void {
- // Nothing to do
- if (tasks.items.len == 0) return;
-
- var t = try allocator.create(Task);
- t.tid = 0;
- t.saved_sp = @ptrFromInt(@frameAddress());
-
- scheduler_task = t;
-
- const first_task = tasks.items[0];
-
- context_switch(&t.saved_sp, first_task.saved_sp);
-}
-
-pub fn get_current_task() *Task {
- return tasks.items[0];
-}
-
-pub fn get_current_tid() ?usize {
- return get_current_task().tid;
-}
-
-pub fn yield() void {
- // We're the only thread, so we can resume operation
- if (tasks.items.len == 1) {
- return;
- }
-
- // Put ourselves into the end of the queue
- // Context switch to the new thread
- const old = tasks.orderedRemove(0);
- const new = tasks.items[0];
- _ = tasks.appendAssumeCapacity(old);
-
- context_switch(&old.saved_sp, new.saved_sp);
-}
-
-fn exit() void {
- // Remove task from list
- const old = tasks.orderedRemove(0);
- const new = if (tasks.items.len == 0) scheduler_task else tasks.items[0];
-
- // TODO; ideally we'd dealloc old
- // but that requires an allocator
- context_switch(&old.saved_sp, new.saved_sp);
-}
-
-noinline fn trampoline() void {
- const t = get_current_task();
- if (t.func) |func| {
- func(t.arg);
- }
- exit();
-}
-
-fn pop_stack(sp: **usize) usize {
- const sp_new: *usize = @ptrFromInt(@intFromPtr(sp.*) + @sizeOf(usize));
- const value = sp.*.*;
- sp.* = sp_new;
- return value;
-}
-
-// export fn dump_stack(r0: usize) callconv(.c) void {
-// // var sp: *usize = @ptrFromInt(@import("pi").get_sp());
-// var sp: *usize = @ptrFromInt(r0);
-
-// @import("devices/mini-uart.zig").writer.print("stack dump (sp = 0x{X}):\n", .{@intFromPtr(sp)}) catch {};
-
-// for (4..12) |r| {
-// const v = pop_stack(&sp);
-
-// @import("devices/mini-uart.zig").writer.print(" r{d} = 0x{X}\n", .{ r, v }) catch {};
-// }
-
-// const lr = pop_stack(&sp);
-// @import("devices/mini-uart.zig").writer.print(" lr = 0x{X}\n", .{lr}) catch {};
-// }
-
-comptime {
- // r0 - **current_task_sp
- // r1 - *next_task_sp
-
- // Store lr, r4-r11 registers on stack
- // Store sp to current_task.saved_sp
- // Move current_task.next.saved_sp into sp
- // Load lr, t4-r11 registers from stack into registers
- // Return
-
- // Remember, currently sp is of current_task's stack
- asm (
- \\ .global context_switch;
- \\ .type context_switch, %function;
- \\ context_switch:
- \\ push {r4-r11,lr}
- \\ str sp, [r0]
- \\ mov sp, r1
- \\ pop {r4-r11,lr}
- \\ bx lr
- );
-}
-extern fn context_switch(current_task_sp: **usize, next_task_sp: *usize) void;
diff --git a/src/labs/5-coroutine.zig b/src/labs/5-coroutine.zig
@@ -1,16 +1,16 @@
const std = @import("std");
const uart = @import("devices/mini-uart.zig");
-const coroutine = @import("coroutine.zig");
+const Tasks = @import("tasks.zig");
export fn syscall_handler() void {}
-fn thread_test(arg: ?*anyopaque) callconv(.c) void {
+fn thread_test(scheduler: *Tasks, arg: ?*anyopaque) callconv(.c) void {
_ = arg;
while (true) {
- uart.writer.print("in thread tid={d}!\n", .{coroutine.get_current_tid().?}) catch {};
- coroutine.yield();
+ uart.writer.print("in thread tid={d}!\n", .{scheduler.get_tid()}) catch {};
+ scheduler.yield();
}
}
@@ -18,20 +18,14 @@ pub fn main() !void {
var buffer: [1024 * 1024]u8 = undefined;
var fba: std.heap.FixedBufferAllocator = .init(&buffer);
- uart.write_slice("initializing coroutines\n");
- try coroutine.initialize(fba.allocator());
+ var scheduler = Tasks.new(fba.allocator());
- try coroutine.fork(fba.allocator(), thread_test, null);
- try coroutine.fork(fba.allocator(), thread_test, null);
- try coroutine.fork(fba.allocator(), thread_test, null);
- try coroutine.fork(fba.allocator(), thread_test, null);
- try coroutine.fork(fba.allocator(), thread_test, null);
- try coroutine.fork(fba.allocator(), thread_test, null);
- try coroutine.fork(fba.allocator(), thread_test, null);
+ try scheduler.fork(thread_test, null);
+ try scheduler.fork(thread_test, null);
+ try scheduler.fork(thread_test, null);
+ try scheduler.fork(thread_test, null);
+ try scheduler.fork(thread_test, null);
+ try scheduler.fork(thread_test, null);
- uart.write_slice("joining coroutines\n");
-
- try coroutine.join(fba.allocator());
-
- uart.write_slice("done!\n");
+ try scheduler.join();
}
diff --git a/src/scheduler.zig b/src/scheduler.zig
@@ -0,0 +1,140 @@
+const std = @import("std");
+
+const Self = @This();
+
+tid_counter: usize,
+tasks_head: ?*Task,
+tasks_tail: ?*Task,
+scheduler_task: *Task,
+allocator: std.mem.Allocator,
+
+pub const Task = struct {
+ tid: usize,
+ next: ?*Task,
+
+ // TODO; make generic(?)
+ func: *const fn (*Self, ?*anyopaque) callconv(.c) void,
+ arg: ?*anyopaque,
+
+ saved_sp: *usize,
+ // r0-r3 are caller-saved and so we don't need to store them
+ // r4-r11 are callee-saved
+ // r12 is scratch
+ // r13 is sp
+ // r14 is lr
+ // r15 is pc
+ stack: [1024]usize align(8),
+};
+
+pub fn new(gpa: std.mem.Allocator) Self {
+ return .{ .tid_counter = 1, .tasks_head = null, .tasks_tail = null, .scheduler_task = undefined, .allocator = gpa };
+}
+
+fn push_stack(sp: *usize, value: usize) *usize {
+ const sp_new: *usize = @ptrFromInt(@intFromPtr(sp) - @sizeOf(usize));
+ sp_new.* = value;
+ return sp_new;
+}
+
+pub fn fork(self: *Self, f: *const fn (*Self, ?*anyopaque) callconv(.c) void, arg: ?*anyopaque) !void {
+ var task = try self.allocator.create(Task);
+
+ task.tid = self.tid_counter;
+ self.tid_counter += 1;
+
+ task.func = f;
+ task.arg = arg;
+ task.next = null;
+
+ const stack_top = @intFromPtr(&task.stack) + task.stack.len;
+ var sp: *usize = @ptrFromInt(stack_top);
+ std.debug.assertAligned(sp, .@"8");
+
+ // tr
+ sp = push_stack(sp, @intFromPtr(&trampoline));
+
+ // r4-r11
+ for (4..12) |_| {
+ sp = push_stack(sp, 0);
+ }
+ // r3 (for trampoline)
+ sp = push_stack(sp, @intFromPtr(self));
+
+ task.saved_sp = sp;
+
+ if (self.tasks_tail) |tail| {
+ tail.next = task;
+ }
+ self.tasks_tail = task;
+
+ if (self.tasks_head == null) {
+ self.tasks_head = task;
+ }
+}
+
+pub fn join(self: *Self) !void {
+ if (self.tasks_head == null) {
+ // Nothing to do
+ return;
+ }
+
+ self.scheduler_task = try self.allocator.create(Task);
+ self.scheduler_task.tid = 0;
+ self.scheduler_task.saved_sp = @ptrFromInt(@frameAddress());
+
+ context_switch(&self.scheduler_task.saved_sp, self.tasks_head.?.saved_sp);
+}
+
+pub fn yield(self: *Self) void {
+ if (self.tasks_head == self.tasks_tail) {
+ return;
+ }
+
+ const current = self.tasks_head.?;
+ self.tasks_head = current.next;
+ current.next = null;
+ self.tasks_tail.?.next = current;
+ self.tasks_tail = current;
+
+ const next = self.tasks_head.?;
+
+ context_switch(¤t.saved_sp, next.saved_sp);
+}
+
+pub fn get_tid(self: *Self) usize {
+ return self.tasks_head.?.tid;
+}
+
+export fn trampoline_inner(self: *Self) callconv(.c) void {
+ const current = self.tasks_head.?;
+ current.func(self, current.arg);
+
+ if (current.next) |next| {
+ self.tasks_head = next;
+ } else {
+ self.tasks_head = self.scheduler_task;
+ }
+
+ context_switch(¤t.saved_sp, self.tasks_head.?.saved_sp);
+}
+
+comptime {
+ asm (
+ \\ .global trampoline
+ \\ .type trampoline, %function;
+ \\ trampoline:
+ \\ mov r0, r3
+ \\ bl trampoline_inner
+ \\ .global context_switch;
+ \\ .type context_switch, %function;
+ \\ context_switch:
+ \\ push {r3-r11,lr}
+ \\ str sp, [r0]
+ \\ mov sp, r1
+ \\ pop {r3-r11,lr}
+ \\ bx lr
+ );
+}
+
+extern fn trampoline() void;
+extern fn context_switch(current_task_sp: **usize, next_task_sp: *usize) void;