hachem.wtf
aboutsummaryrefslogtreecommitdiff
path: root/boot/sched.zig
blob: 60a8fa08abf4a071a1f10ce96f301faf0831de9d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
// tiny preemptive round robin scheduler. a task is a saved frame + cr3 + kernel
// stack, task 0 is the kernel event loop. the pit tick swaps the interrupted
// frame for the next ready tasks and iretqs in, which covers ring 0 and ring 3
// alike. each task has its own kernel stack so preempting one mid syscall (e.g. a
// read() parked on sti/hlt) doesnt clobber anothers rsp0

const std = @import("std");
const Frame = @import("regs.zig").Frame;
const vmm = @import("vmm.zig");
const gdt = @import("gdt.zig");
const bridge = @import("bridge");

const MAX_TASKS = 8;
const KSTACK_SIZE = 16 * 1024;

const State = enum { free, ready, dead, blocked };

const Task = struct {
    frame: Frame = std.mem.zeroes(Frame),
    cr3: u64 = 0,
    kstack_top: u64 = 0, // tss rsp0 for this task, 0 for the ring 0 kernel task
    state: State = .free,
};

var tasks: [MAX_TASKS]Task = @splat(.{});
var kstacks: [MAX_TASKS][KSTACK_SIZE]u8 align(16) = undefined;
var current: usize = 0;
var spawn_count: u64 = 0;
// the one task currently blocked in read(), if any
var blocked_reader: ?usize = null;
// the "foreground" task that owns keyboard input (set when a task first reads,
// cleared when it exits). keystrokes route to it, into stdin_buf when it isnt
// currently blocked, so nothing is lost between its reads. else the shell
var foreground: ?usize = null;
var stdin_buf: [64]u8 = undefined;
var stdin_head: usize = 0;
var stdin_tail: usize = 0;

fn stdinPush(byte: u8) void {
    if (stdin_tail -% stdin_head >= stdin_buf.len) return; // full, drop
    stdin_buf[stdin_tail % stdin_buf.len] = byte;
    stdin_tail +%= 1;
}
fn stdinPop() ?u8 {
    if (stdin_head == stdin_tail) return null;
    const byte = stdin_buf[stdin_head % stdin_buf.len];
    stdin_head +%= 1;
    return byte;
}

// task 0 = the kernel/event loop, running in whatever cr3 boot set up
pub fn init(kernel_cr3: u64) void {
    tasks[0] = .{ .cr3 = kernel_cr3, .state = .ready, .kstack_top = 0 };
    current = 0;
}

// queue a ring 3 process. it starts running the next time the scheduler picks
// it (the tick copies its initial frame onto the stack and iretqs in). rdi is
// seeded with a small per spawn id so demo programs can tell instances apart
pub fn spawn(cr3: u64, entry: u64, ustack: u64) void {
    var i: usize = 1;
    while (i < MAX_TASKS) : (i += 1) {
        if (tasks[i].state != .free) continue;
        var f = std.mem.zeroes(Frame);
        f.rip = entry;
        f.cs = 0x1b; // user code (0x18) | rpl 3
        f.rflags = 0x202; // reserved bit + if=1, so its preemptible
        f.rsp = ustack;
        f.ss = 0x23; // user data (0x20) | rpl 3
        f.rdi = spawn_count;
        spawn_count += 1;
        tasks[i] = .{
            .frame = f,
            .cr3 = cr3,
            .kstack_top = @intFromPtr(&kstacks[i]) + KSTACK_SIZE,
            .state = .ready,
        };
        return;
    }
}

// number of live user processes (ready or blocked), excluding the kernel task
pub fn procCount() u64 {
    var count: u64 = 0;
    var i: usize = 1;
    while (i < MAX_TASKS) : (i += 1) {
        if (tasks[i].state == .ready or tasks[i].state == .blocked) count += 1;
    }
    return count;
}

var proc_exited_flag: bool = false;

// mark the running process dead. it then sti+hlts, the next tick reclaims the
// slot and switches away, never returning here
pub fn exitCurrent() void {
    if (current != 0) {
        tasks[current].state = .dead;
        proc_exited_flag = true; // the kernel task reprints the prompt on seeing this
        if (foreground == current) { // hand keyboard back to the shell
            foreground = null;
            stdin_head = 0;
            stdin_tail = 0;
        }
    }
}

// did a process exit since last checked? (consumed once.)
pub fn takeProcExited() bool {
    const e = proc_exited_flag;
    proc_exited_flag = false;
    return e;
}

// called at the top of read(): the caller becomes the foreground reader. returns
// a buffered byte if one is waiting (so read() neednt block), else null
pub fn readReady() ?u8 {
    foreground = current;
    return stdinPop();
}

// block the running task in read(). `resume_frame` is a context that iretqs
// straight back to just after the processs int 0x80, so when input arrives we
// set its rax and mark it ready, and the scheduler resumes it there
pub fn blockCurrentOnRead(resume_frame: Frame) void {
    if (current == 0) return; // the kernel task never blocks like this
    tasks[current].frame = resume_frame;
    tasks[current].state = .blocked;
    blocked_reader = current;
}

// route one input byte. if a task is blocked in read(), wake it with the byte
// else if a foreground reader exists, buffer it for its next read(). returns
// false only when nothing wants it (so the caller feeds the shell instead)
pub fn deliverInput(byte: u8) bool {
    if (blocked_reader) |task| {
        tasks[task].frame.rax = byte; // read()s return value
        tasks[task].state = .ready;
        blocked_reader = null;
        return true;
    }
    if (foreground != null) {
        stdinPush(byte);
        return true;
    }
    return false;
}

// the preemption point, called from the pit irq with the interrupted frame
pub fn tick(frame: *Frame) void {
    // dont preempt task 0 mid vm call, the shared fiber cant be re-entered
    if (bridge.vmBusy()) return;

    if (tasks[current].state == .dead) {
        tasks[current].state = .free; // reclaim, dont bother saving its context
    } else if (tasks[current].state == .blocked) {
        // keep the resume frame saved by blockcurrentonread, dont clobber it
    } else {
        tasks[current].frame = frame.*; // freeze the current task
    }

    // round robin to the next ready task
    var next = current;
    var i = current;
    var n: usize = 0;
    while (n < MAX_TASKS) : (n += 1) {
        i = (i + 1) % MAX_TASKS;
        if (tasks[i].state == .ready) {
            next = i;
            break;
        }
    }
    // if the current task cant continue (dead/blocked) and nothing else is
    // ready, fall back to the kernel task (0), which is always ready
    if (tasks[current].state != .ready and next == current) next = 0;
    if (next == current) return; // nothing else to run, keep going

    current = next;
    frame.* = tasks[current].frame;
    vmm.loadPml4(tasks[current].cr3);
    if (tasks[current].kstack_top != 0) gdt.setKernelStack(tasks[current].kstack_top);
}