pluto/src/kernel/scheduler.zig
2022-03-06 15:22:34 +00:00

416 lines
15 KiB
Zig

const std = @import("std");
const expectEqual = std.testing.expectEqual;
const expectError = std.testing.expectError;
const assert = std.debug.assert;
const log = std.log.scoped(.scheduler);
const builtin = @import("builtin");
const is_test = builtin.is_test;
const build_options = @import("build_options");
const arch = @import("arch.zig").internals;
const panic = @import("panic.zig").panic;
const task = @import("task.zig");
const vmm = @import("vmm.zig");
const mem = @import("mem.zig");
const fs = @import("filesystem/vfs.zig");
const elf = @import("elf.zig");
const pmm = @import("pmm.zig");
const Task = task.Task;
const EntryPoint = task.EntryPoint;
const Allocator = std.mem.Allocator;
const TailQueue = std.TailQueue;
/// The default stack size of a task. Currently this is set to a page size.
const STACK_SIZE: u32 = arch.MEMORY_BLOCK_SIZE / @sizeOf(usize);
/// Pointer to the start of the main kernel stack
extern var KERNEL_STACK_START: []u32;
extern var KERNEL_STACK_END: []u32;
/// The current task running
var current_task: *Task = undefined;
/// Array list of all runnable tasks
var tasks: TailQueue(*Task) = undefined;
/// Whether the scheduler is allowed to switch tasks.
var can_switch: bool = true;
///
/// The idle task that just halts the CPU but the CPU can still handle interrupts.
///
fn idle() noreturn {
arch.spinWait();
}
pub fn taskSwitching(enabled: bool) void {
can_switch = enabled;
}
///
/// Round robin. This will first save the the current tasks stack pointer, then will pick the next
/// task to be run from the queue. It will add the current task to the end of the queue and pop the
/// next task from the front as set this as the current task. Then will return the stack pointer
/// of the next task to be loaded into the stack register to load the next task stack to pop off
/// its state. Interrupts are assumed disabled.
///
/// Argument:
/// IN ctx: *arch.CpuState - Pointer to the exception context containing the contents
/// of the registers at the time of a exception.
///
/// Return: usize
/// The new stack pointer to the next stack of the next task.
///
pub fn pickNextTask(ctx: *arch.CpuState) usize {
switch (build_options.test_mode) {
.Scheduler => if (!current_task.kernel) {
if (!arch.runtimeTestCheckUserTaskState(ctx)) {
panic(null, "User task state check failed\n", .{});
}
},
else => {},
}
// Save the stack pointer from old task
current_task.stack_pointer = @ptrToInt(ctx);
// If we can't switch, then continue with the current task
if (!can_switch) {
return current_task.stack_pointer;
}
// Pick the next task
// If there isn't one, then just return the same task
if (tasks.pop()) |new_task_node| {
// Get the next task
const next_task = new_task_node.data;
// Move some pointers to don't need to allocate memory, speeds things up
new_task_node.data = current_task;
new_task_node.prev = null;
new_task_node.next = null;
// Add the 'current_task' node to the end of the queue
tasks.prepend(new_task_node);
current_task = next_task;
}
// Context switch in the interrupt stub handler which will pop the next task state off the
// stack
return current_task.stack_pointer;
}
///
/// Create a new task and add it to the scheduling queue. No locking.
///
/// Arguments:
/// IN entry_point: EntryPoint - The entry point into the task. This must be a function.
/// IN allocator: Allocator - The allocator to use
///
/// Error: Allocator.Error
/// OutOfMemory - If there isn't enough memory for the a task/stack. Any memory allocated will
/// be freed on return.
///
pub fn scheduleTask(new_task: *Task, allocator: Allocator) Allocator.Error!void {
var task_node = try allocator.create(TailQueue(*Task).Node);
task_node.* = .{ .data = new_task };
tasks.prepend(task_node);
}
///
/// Initialise the scheduler. This will set up the current task to the code that is currently
/// running. So if there is a task switch before kmain can finish, can continue when switched back.
/// This will set the stack to KERNEL_STACK_START from the linker stript. This will also create the
/// idle task for when there is no more tasks to run.
///
/// Arguments:
/// IN allocator: Allocator - The allocator to use when needing to allocate memory.
/// IN mem_profile: *const mem.MemProfile - The system's memory profile used for runtime testing.
///
/// Error: Allocator.Error
/// OutOfMemory - There is no more memory. Any memory allocated will be freed on return.
///
pub fn init(allocator: Allocator, mem_profile: *const mem.MemProfile) Allocator.Error!void {
// TODO: Maybe move the task init here?
log.info("Init\n", .{});
defer log.info("Done\n", .{});
// Init the task list for round robin
tasks = TailQueue(*Task){};
// Set up the init task to continue execution
current_task = try allocator.create(Task);
errdefer allocator.destroy(current_task);
// PID 0
current_task.pid = 0;
const kernel_stack_size = @ptrToInt(&KERNEL_STACK_END) - @ptrToInt(&KERNEL_STACK_START);
current_task.kernel_stack = @intToPtr([*]u32, @ptrToInt(&KERNEL_STACK_START))[0..kernel_stack_size];
current_task.user_stack = &[_]usize{};
current_task.kernel = true;
// ESP will be saved on next schedule
// Run the runtime tests here
switch (build_options.test_mode) {
.Scheduler => runtimeTests(allocator, mem_profile),
else => {},
}
// Create the idle task when there are no more tasks left
var idle_task = try Task.create(@ptrToInt(idle), true, &vmm.kernel_vmm, allocator);
errdefer idle_task.destroy(allocator);
try scheduleTask(idle_task, allocator);
}
// For testing the errdefer
const FailingAllocator = std.testing.FailingAllocator;
const testing_allocator = &std.testing.base_allocator_instance.allocator;
fn test_fn1() void {}
fn test_fn2() void {}
var test_pid_counter: u7 = 1;
fn createTestTask(allocator: Allocator) Allocator.Error!*Task {
var t = try allocator.create(Task);
errdefer allocator.destroy(t);
t.pid = test_pid_counter;
// Just alloc something
t.kernel_stack = try allocator.alloc(u32, 1);
t.stack_pointer = 0;
test_pid_counter += 1;
return t;
}
fn destroyTestTask(self: *Task, allocator: Allocator) void {
if (@ptrToInt(self.kernel_stack.ptr) != @ptrToInt(&KERNEL_STACK_START)) {
allocator.free(self.kernel_stack);
}
allocator.destroy(self);
}
test "pickNextTask" {
var ctx: arch.CpuState = std.mem.zeroes(arch.CpuState);
var allocator = std.testing.allocator;
tasks = TailQueue(*Task){};
// Set up a current task
current_task = try allocator.create(Task);
defer allocator.destroy(current_task);
current_task.pid = 0;
current_task.kernel_stack = @intToPtr([*]u32, @ptrToInt(&KERNEL_STACK_START))[0..4096];
current_task.stack_pointer = @ptrToInt(&KERNEL_STACK_START);
// Create two tasks and schedule them
var test_fn1_task = try Task.create(@ptrToInt(test_fn1), true, undefined, allocator);
defer test_fn1_task.destroy(allocator);
try scheduleTask(test_fn1_task, allocator);
var test_fn2_task = try Task.create(@ptrToInt(test_fn2), true, undefined, allocator);
defer test_fn2_task.destroy(allocator);
try scheduleTask(test_fn2_task, allocator);
// Get the stack pointers of the created tasks
const fn1_stack_pointer = test_fn1_task.stack_pointer;
const fn2_stack_pointer = test_fn2_task.stack_pointer;
try expectEqual(pickNextTask(&ctx), fn1_stack_pointer);
// The stack pointer of the re-added task should point to the context
try expectEqual(tasks.first.?.data.stack_pointer, @ptrToInt(&ctx));
// Should be the PID of the next task
try expectEqual(current_task.pid, 1);
try expectEqual(pickNextTask(&ctx), fn2_stack_pointer);
// The stack pointer of the re-added task should point to the context
try expectEqual(tasks.first.?.data.stack_pointer, @ptrToInt(&ctx));
// Should be the PID of the next task
try expectEqual(current_task.pid, 2);
try expectEqual(pickNextTask(&ctx), @ptrToInt(&ctx));
// The stack pointer of the re-added task should point to the context
try expectEqual(tasks.first.?.data.stack_pointer, @ptrToInt(&ctx));
// Should be back tot he beginning
try expectEqual(current_task.pid, 0);
// Reset the test pid
test_pid_counter = 1;
// Free the queue
while (tasks.pop()) |elem| {
allocator.destroy(elem);
}
}
test "createNewTask add new task" {
// Set the global allocator
var allocator = std.testing.allocator;
// Init the task list
tasks = TailQueue(*Task){};
var test_fn1_task = try Task.create(@ptrToInt(test_fn1), true, undefined, allocator);
defer test_fn1_task.destroy(allocator);
try scheduleTask(test_fn1_task, allocator);
try expectEqual(tasks.len, 1);
// Free the memory
allocator.destroy(tasks.first.?);
}
test "init" {
var allocator = std.testing.allocator;
try init(allocator, undefined);
try expectEqual(current_task.pid, 0);
try expectEqual(@ptrToInt(current_task.kernel_stack.ptr), @ptrToInt(&KERNEL_STACK_START));
try expectEqual(current_task.kernel_stack.len, @ptrToInt(&KERNEL_STACK_END) - @ptrToInt(&KERNEL_STACK_START));
try expectEqual(tasks.len, 1);
// Free the tasks created
current_task.destroy(allocator);
while (tasks.pop()) |elem| {
elem.data.destroy(allocator);
allocator.destroy(elem);
}
}
/// A volatile pointer used to control a loop outside the task. This is so to ensure a task switch
/// ocurred.
var is_set: *volatile bool = undefined;
///
/// The test task function.
///
fn task_function() noreturn {
log.info("Switched\n", .{});
is_set.* = false;
while (true) {}
}
///
/// This tests that variables in registers and on the stack are preserved when a task switch
/// occurs. Also tests that a global volatile can be test in one task and be reacted to in another.
///
/// Arguments:
/// IN allocator: Allocator - The allocator to use when needing to allocate memory.
///
fn rt_variable_preserved(allocator: Allocator) void {
// Create the memory for the boolean
is_set = allocator.create(bool) catch unreachable;
defer allocator.destroy(is_set);
is_set.* = true;
var test_task = Task.create(@ptrToInt(task_function), true, &vmm.kernel_vmm, allocator) catch |e| panic(@errorReturnTrace(), "Failed to create task in rt_variable_preserved: {}\n", .{e});
scheduleTask(test_task, allocator) catch |e| panic(@errorReturnTrace(), "Failed to schedule a task in rt_variable_preserved: {}\n", .{e});
// TODO: Need to add the ability to remove tasks
var w: u32 = 0;
var x: u32 = 1;
var y: u32 = 2;
var z: u32 = 3;
while (is_set.*) {
if (w != 0) {
panic(@errorReturnTrace(), "FAILED: w not 0, but: {}\n", .{w});
}
if (x != 1) {
panic(@errorReturnTrace(), "FAILED: x not 1, but: {}\n", .{x});
}
if (y != 2) {
panic(@errorReturnTrace(), "FAILED: y not 2, but: {}\n", .{y});
}
if (z != 3) {
panic(@errorReturnTrace(), "FAILED: z not 3, but: {}\n", .{z});
}
}
// Make sure these are the same values
if (w != 0) {
panic(@errorReturnTrace(), "FAILED: w not 0, but: {}\n", .{w});
}
if (x != 1) {
panic(@errorReturnTrace(), "FAILED: x not 1, but: {}\n", .{x});
}
if (y != 2) {
panic(@errorReturnTrace(), "FAILED: y not 2, but: {}\n", .{y});
}
if (z != 3) {
panic(@errorReturnTrace(), "FAILED: z not 3, but: {}\n", .{z});
}
log.info("SUCCESS: Scheduler variables preserved\n", .{});
}
///
/// Test the initialisation and running of a task running in user mode
///
/// Arguments:
/// IN allocator: *std.mem.Allocator - The allocator to use when intialising the task
/// IN mem_profile: mem.MemProfile - The system's memory profile. Determines the end address of the user task's VMM.
///
fn rt_user_task(allocator: Allocator, mem_profile: *const mem.MemProfile) void {
for (&[_][]const u8{ "/user_program_data.elf", "/user_program.elf" }) |user_program| {
// 1. Create user VMM
var task_vmm = allocator.create(vmm.VirtualMemoryManager(arch.VmmPayload)) catch |e| {
panic(@errorReturnTrace(), "Failed to allocate VMM for {s}: {}\n", .{ user_program, e });
};
task_vmm.* = vmm.VirtualMemoryManager(arch.VmmPayload).init(0, @ptrToInt(mem_profile.vaddr_start), allocator, arch.VMM_MAPPER, undefined) catch |e| panic(@errorReturnTrace(), "Failed to create the vmm for {s}: {}\n", .{ user_program, e });
const user_program_file = fs.openFile(user_program, .NO_CREATION) catch |e| {
panic(@errorReturnTrace(), "Failed to open {s}: {}\n", .{ user_program, e });
};
defer user_program_file.close();
var code: [1024 * 9]u8 = undefined;
const code_len = user_program_file.read(code[0..code.len]) catch |e| {
panic(@errorReturnTrace(), "Failed to read {s}: {}\n", .{ user_program, e });
};
const program_elf = elf.Elf.init(code[0..code_len], builtin.cpu.arch, allocator) catch |e| panic(@errorReturnTrace(), "Failed to load {s}: {}\n", .{ user_program, e });
defer program_elf.deinit();
var user_task = task.Task.createFromElf(program_elf, false, task_vmm, allocator) catch |e| {
panic(@errorReturnTrace(), "Failed to create task for {s}: {}\n", .{ user_program, e });
};
scheduleTask(user_task, allocator) catch |e| {
panic(@errorReturnTrace(), "Failed to schedule the task for {s}: {}\n", .{ user_program, e });
};
var num_allocatable_sections: usize = 0;
var size_allocatable_sections: usize = 0;
for (program_elf.section_headers) |section| {
if (section.flags & elf.SECTION_ALLOCATABLE != 0) {
num_allocatable_sections += 1;
size_allocatable_sections += std.mem.alignForward(section.size, vmm.BLOCK_SIZE);
}
}
// Only a certain number of elf section are expected to have been allocated in the vmm
if (task_vmm.allocations.count() != num_allocatable_sections) {
panic(@errorReturnTrace(), "VMM allocated wrong number of virtual regions for {s}. Expected {} but found {}\n", .{ user_program, num_allocatable_sections, task_vmm.allocations.count() });
}
const allocated_size = (task_vmm.bmp.num_entries - task_vmm.bmp.num_free_entries) * vmm.BLOCK_SIZE;
if (size_allocatable_sections != allocated_size) {
panic(@errorReturnTrace(), "VMM allocated wrong amount of memory for {s}. Expected {} but found {}\n", .{ user_program, size_allocatable_sections, allocated_size });
}
}
}
///
/// The scheduler runtime tests that will test the scheduling functionality.
///
/// Arguments:
/// IN allocator: Allocator - The allocator to use when needing to allocate memory.
/// IN mem_profile: *const mem.MemProfile - The system's memory profile. Used to set up user task VMMs.
///
fn runtimeTests(allocator: Allocator, mem_profile: *const mem.MemProfile) void {
arch.enableInterrupts();
rt_user_task(allocator, mem_profile);
rt_variable_preserved(allocator);
while (true) {}
}