369 lines
10 KiB
Zig
369 lines
10 KiB
Zig
//! Thread management utilities and data structures.
|
|
|
|
const std = @import("std");
|
|
|
|
const arena = @import("arena.zig");
|
|
const arch = @import("kernel.zig").arch;
|
|
const log = @import("debug.zig").log;
|
|
const mem = @import("mem.zig");
|
|
const sync = @import("sync.zig");
|
|
const object = @import("object.zig");
|
|
const abi = @import("abi");
|
|
|
|
const ProcessAddressSpace = mem.vmm.ProcessAddressSpace;
|
|
const Object = object.Object;
|
|
const Handle = abi.Handle;
|
|
|
|
// TODO: are kernel threads needed at all if we're doing a microkernel?
|
|
|
|
/// Signature for kernel thread entry
|
|
pub const KernelThreadFn = fn (usize) callconv(.C) void;
|
|
|
|
pub fn kernel_return() callconv(.C) noreturn {
|
|
Thread.exit_current();
|
|
}
|
|
|
|
/// Task to run when there are no real threads in the queue
|
|
pub fn idle_function(arg: usize) callconv(.C) noreturn {
|
|
_ = arg;
|
|
while (true) {
|
|
arch.wait_for_interrupt();
|
|
}
|
|
}
|
|
|
|
/// Represents a single execution thread.
|
|
pub const Thread = struct {
|
|
const MAX_HANDLES: usize = 64;
|
|
|
|
/// Arena.
|
|
allocator: *arena.Arena,
|
|
/// Architecture-specific task context.
|
|
arch_context: arch.Context,
|
|
|
|
/// Queue to which this thread belongs
|
|
queue: ?*Queue = null,
|
|
/// Next thread in the queue.
|
|
next: ?*Thread = null,
|
|
/// Previous thread in the queue.
|
|
prev: ?*Thread = null,
|
|
|
|
// TODO move to process
|
|
address_space: ?ProcessAddressSpace = null,
|
|
// TODO move to process
|
|
handle_table: [MAX_HANDLES]?Object = [_]?Object{null} ** MAX_HANDLES,
|
|
|
|
pub const Error = error{out_of_memory} || mem.vmm.AddressSpaceError;
|
|
|
|
/// Creates a new (kernel) thread with given `function` and `arg`ument.
|
|
pub fn create_kernel(a: *arena.Arena, function: *const KernelThreadFn, arg: usize) *Thread {
|
|
const thread = a.create(Thread);
|
|
thread.* = .{
|
|
.allocator = a,
|
|
.arch_context = arch.Context.kernel(function, arg),
|
|
};
|
|
return thread;
|
|
}
|
|
|
|
pub fn create_user(
|
|
a: *arena.Arena,
|
|
address_space: ProcessAddressSpace,
|
|
pc: usize,
|
|
sp: usize,
|
|
arg: usize,
|
|
) *Thread {
|
|
const thread = a.create(Thread);
|
|
thread.* = .{
|
|
.allocator = a,
|
|
.address_space = address_space,
|
|
.arch_context = arch.Context.user(&address_space, pc, sp, arg),
|
|
};
|
|
// "self" process object is granted to all processes
|
|
const process_object = Object{ .process = object.ProcessObject {
|
|
.inner = thread,
|
|
} };
|
|
_ = thread.grant(process_object);
|
|
return thread;
|
|
}
|
|
|
|
/// Enters the thread, does not return.
|
|
pub fn enter(self: *@This()) noreturn {
|
|
self.arch_context.enter();
|
|
}
|
|
|
|
/// Switches from `from` to `self` thread.
|
|
pub fn switch_from(self: *@This(), from: *@This()) void {
|
|
self.arch_context.switch_from(&from.arch_context);
|
|
}
|
|
|
|
pub fn grant(self: *@This(), obj: Object) Handle {
|
|
for (0..MAX_HANDLES) |i| {
|
|
if (self.handle_table[i] == null) {
|
|
self.handle_table[i] = obj;
|
|
return @enumFromInt(i + 1);
|
|
}
|
|
}
|
|
@panic("TODO: ran out of objects");
|
|
}
|
|
|
|
pub fn handle(self: *@This(), h: Handle) ?Object {
|
|
const index = @as(usize, @intFromEnum(h));
|
|
if (index > MAX_HANDLES or index == 0) {
|
|
return null;
|
|
}
|
|
return self.handle_table[index - 1];
|
|
}
|
|
|
|
pub fn dequeue(self: *@This()) void {
|
|
// TODO queueing information should be put under a lock for SMP to work properly
|
|
if (self.queue) |q| {
|
|
q.dequeue(self);
|
|
}
|
|
}
|
|
|
|
pub fn current() *@This() {
|
|
return Queue.t_this_cpu.?.current.?;
|
|
}
|
|
|
|
pub fn exit_current() noreturn {
|
|
// Mask IRQs so they don't break current thread's state
|
|
const mask = arch.IrqGuard.acquire();
|
|
defer mask.release();
|
|
|
|
const curr = Thread.current();
|
|
curr.dequeue();
|
|
|
|
yield();
|
|
|
|
@panic("This code should not be reachable");
|
|
}
|
|
};
|
|
|
|
/// Per-CPU thread queue structure.
|
|
pub const Queue = struct {
|
|
/// Idle task context. Used when there are no other tasks running.
|
|
idle: arch.Context,
|
|
/// Current thread pointer.
|
|
current: ?*Thread = null,
|
|
/// Thread queue head pointer.
|
|
head: ?*Thread = null,
|
|
/// Queue's lock
|
|
lock: sync.Spinlock = .{},
|
|
|
|
/// Pointer to this CPU's thread queue.
|
|
pub threadlocal var t_this_cpu: ?*Queue = null;
|
|
|
|
/// Sets up a thread queue for the current CPU.
|
|
pub fn init_this_cpu(a: *arena.Arena) void {
|
|
const idle = arch.Context.idle();
|
|
const q = a.create(Queue);
|
|
q.* = .{ .idle = idle };
|
|
t_this_cpu = q;
|
|
}
|
|
|
|
/// Enters a task on this CPU.
|
|
pub fn enter(self: *@This()) noreturn {
|
|
if (self.head) |gt| {
|
|
self.current = gt;
|
|
gt.enter();
|
|
} else {
|
|
self.current = null;
|
|
self.idle.enter();
|
|
}
|
|
}
|
|
|
|
/// Yields CPU to the next available task.
|
|
pub fn yield(self: *@This()) void {
|
|
// TODO locking here
|
|
if (self.current) |curr| {
|
|
// Switching from thread
|
|
if (curr.next) |next| {
|
|
// ... to thread
|
|
if (next != curr) {
|
|
self.current = next;
|
|
next.switch_from(curr);
|
|
}
|
|
} else if (self.head) |h| {
|
|
// ... to thread (head)
|
|
if (h != curr) {
|
|
self.current = h;
|
|
h.switch_from(curr);
|
|
}
|
|
} else {
|
|
// ... to idle
|
|
self.current = null;
|
|
self.idle.switch_from(&curr.arch_context);
|
|
}
|
|
} else {
|
|
// Switching from idle
|
|
if (self.head) |gt| {
|
|
// ... to thread
|
|
self.current = gt;
|
|
gt.arch_context.switch_from(&self.idle);
|
|
return;
|
|
}
|
|
// ... back to idle
|
|
}
|
|
}
|
|
|
|
/// Adds an available task to this queue.
|
|
pub fn enqueue(self: *@This(), t: *Thread) void {
|
|
var guard = self.lock.lock_irqsave();
|
|
defer guard.release();
|
|
|
|
t.queue = self;
|
|
|
|
if (self.head) |gt| {
|
|
t.next = gt;
|
|
t.prev = gt.prev;
|
|
gt.prev.?.next = t;
|
|
gt.prev = t;
|
|
} else {
|
|
self.head = t;
|
|
t.next = t;
|
|
t.prev = t;
|
|
}
|
|
}
|
|
|
|
/// # Invariants
|
|
///
|
|
/// `t` must be a thread within the `self` queue.
|
|
fn dequeue(self: *@This(), t: *Thread) void {
|
|
var guard = self.lock.lock_irqsave();
|
|
defer guard.release();
|
|
|
|
t.queue = null;
|
|
|
|
if (t == self.head) {
|
|
if (t.next == t) {
|
|
self.head = null;
|
|
t.next = null;
|
|
t.prev = null;
|
|
return;
|
|
}
|
|
|
|
if (t.next) |tn| {
|
|
tn.prev = t.prev;
|
|
}
|
|
if (t.prev) |tp| {
|
|
tp.next = t.next;
|
|
}
|
|
|
|
self.head = t.next;
|
|
} else {
|
|
if (t.next) |tn| {
|
|
tn.prev = t.prev;
|
|
}
|
|
if (t.prev) |tp| {
|
|
tp.next = t.next;
|
|
}
|
|
}
|
|
|
|
t.next = null;
|
|
t.prev = null;
|
|
}
|
|
};
|
|
|
|
/// Helper data structure to represent kernel stacks in task contexts.
|
|
pub fn KStack(comptime SIZE: usize) type {
|
|
return extern struct {
|
|
// Has to be at exactly offset 0x00, used in assembly
|
|
/// Stack pointer. Aliases `data`.
|
|
sp: *usize,
|
|
|
|
/// Stack data represented as a slice of `SIZE` machine-sized words.
|
|
data: *[SIZE]usize,
|
|
/// Physical base address at which the stack is allocated.
|
|
physical_base: mem.PhysicalAddress,
|
|
|
|
/// Allocates a new kernel stack.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics on Out-of-Memory condition. TODO Fix this.
|
|
pub fn create() @This() {
|
|
const physical_base = mem.phys.alloc_pages(SIZE * @sizeOf(usize) / 0x1000) orelse @panic("OOM");
|
|
const ptr = @as(*[SIZE]usize, @ptrFromInt(physical_base.virtualize()));
|
|
|
|
return .{
|
|
.data = ptr,
|
|
.physical_base = physical_base,
|
|
.sp = @ptrFromInt(@intFromPtr(&ptr[0]) + SIZE * @sizeOf(usize)),
|
|
};
|
|
}
|
|
|
|
/// Pushes a machine-sized word onto the stack.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics if a push would overflow the stack.
|
|
pub fn push(self: *@This(), value: usize) void {
|
|
if (self.sp == &self.data[0]) {
|
|
@panic("KStack overflow");
|
|
}
|
|
self.sp = @ptrFromInt(@intFromPtr(self.sp) - @sizeOf(usize));
|
|
self.sp.* = value;
|
|
}
|
|
};
|
|
}
|
|
|
|
/// Adds a thread to some CPU queue for execution.
|
|
pub fn enqueue(t: *Thread) void {
|
|
Queue.t_this_cpu.?.enqueue(t);
|
|
}
|
|
|
|
/// Enters thread execution on the current CPU.
|
|
pub fn enter() noreturn {
|
|
Queue.t_this_cpu.?.enter();
|
|
}
|
|
|
|
/// Yields this CPU's execution to a next thread.
|
|
pub fn yield() void {
|
|
Queue.t_this_cpu.?.yield();
|
|
}
|
|
|
|
pub fn test_create_user_from_code(a: *arena.Arena, code: []const u8) Thread.Error!*Thread {
|
|
const L3 = mem.vmm.L3;
|
|
const CODE_BASE: usize = 0x200000;
|
|
|
|
var address_space = try ProcessAddressSpace.init(a);
|
|
errdefer {
|
|
address_space.clear();
|
|
}
|
|
|
|
// @ 0x200000
|
|
const code_page_count = L3.page_count(code.len);
|
|
log.info("Code is {} pages", .{code_page_count});
|
|
var offset: usize = 0;
|
|
var address: usize = CODE_BASE;
|
|
while (offset < code.len) {
|
|
const page_offset = address % L3.SIZE;
|
|
const amount = @min(L3.SIZE - page_offset, code.len - offset);
|
|
|
|
const page = mem.phys.alloc_page() orelse return error.out_of_memory;
|
|
try address_space.map_single_page(address, page);
|
|
|
|
const page_data = @as([*]u8, @ptrFromInt(page.virtualize()))[0..amount];
|
|
@memcpy(page_data, code[offset .. offset + amount]);
|
|
|
|
address += amount;
|
|
offset += amount;
|
|
}
|
|
|
|
// @ 0x400000
|
|
const sp_base = 0x400000;
|
|
for (0..8) |i| {
|
|
const page = mem.phys.alloc_page() orelse return error.out_of_memory;
|
|
try address_space.map_single_page(sp_base + i * L3.SIZE, page);
|
|
}
|
|
const sp = sp_base + 8 * L3.SIZE;
|
|
|
|
log.info("Enter with sp = 0x{x}", .{sp});
|
|
|
|
const thread = Thread.create_user(a, address_space, CODE_BASE, sp, 1234);
|
|
|
|
_ = thread.grant(Object { .physical_memory = object.PhysicalMemoryObject {} });
|
|
_ = thread.grant(Object { .debug = object.DebugObject {} });
|
|
|
|
return thread;
|
|
}
|