forked from PoliEcho-holding-org/kern
PREEMPTIVE MULTITASKING!!!!
This commit is contained in:
@@ -28,4 +28,5 @@ DS_Store
|
||||
*.swo
|
||||
/.clangd
|
||||
/.cache
|
||||
/compile_commands.json
|
||||
|
||||
|
||||
+58
-19
@@ -1,25 +1,64 @@
|
||||
System Calls
|
||||
============
|
||||
|
||||
Syscall handlers are prefixed by 'sys_'. Syscalls can be called using int 0x80.
|
||||
Arguments are passed in registers like this:
|
||||
* EAX - Syscall number
|
||||
* EBX - Argument 0
|
||||
* ECX - Argument 1
|
||||
* EDX - Argument 2
|
||||
pid_t fork(void);
|
||||
Creates a child process.
|
||||
|
||||
If the syscall returns, the return value will be in EAX.
|
||||
void exit(int status);
|
||||
Terminate the calling process.
|
||||
|
||||
pid_t waitpid(pid_t pid);
|
||||
Wait untill a process changes state.
|
||||
|
||||
int kill(pid_t, int sig);
|
||||
Send a signal to a process.
|
||||
|
||||
pid_t getpid(void);
|
||||
Get PID of calling process.
|
||||
|
||||
void sleep(size_t t);
|
||||
Sleep for **t** clock ticks.
|
||||
|
||||
int exec(char* path, char** argv);
|
||||
Execute a program.
|
||||
|
||||
int brk(void* addr);
|
||||
Change data segment size.
|
||||
|
||||
int open(char* path, int flags);
|
||||
Open and possibly create a file.
|
||||
|
||||
int close(int fd);
|
||||
Close a file descriptor.
|
||||
|
||||
ssize_t read(int fd, void* buf, size_t count);
|
||||
Read up to **count** bytes into **buf**.
|
||||
|
||||
ssize_t write(int fd, void* buf, size_t count);
|
||||
Write up to **count** bytes from **buf**.
|
||||
|
||||
int dup(int oldfd);
|
||||
Duplicate a file descriptor.
|
||||
|
||||
int pipe(int pipe[2]);
|
||||
Create a pipe and return it's file descriptors in **pipe**.
|
||||
|
||||
int chdir(char* path);
|
||||
Change the current directory of calling process.
|
||||
|
||||
int mkdir(char* path);
|
||||
Create a new directory.
|
||||
|
||||
int mknod(char* path, int maj, int min);
|
||||
Create a new device file.
|
||||
|
||||
int link(char* f1, char* f2);
|
||||
Creates another filename (**f2**) for **f1**.
|
||||
|
||||
int unlink(char* path);
|
||||
Delete a filename.
|
||||
|
||||
int fstat(int fd, struct stat* statbuf);
|
||||
Return info about a file.
|
||||
|
||||
Syscall Table (TODO: Update this)
|
||||
-------------
|
||||
| NR | SYSC NAME | RETURN (EAX) | ARG0 (EBX) | ARG1 (ECX) | ARG2 (EDX) |
|
||||
| 00 | reserved | - | - | - | - |
|
||||
| 01 | sleep | - | uint ms | - | - |
|
||||
| 02 | read | int count | int fd | char* buf | uint count |
|
||||
| 03 | write | int count | int fd | char* buf | uint count |
|
||||
| 04 | open | int fd | char* path | int oflag | - |
|
||||
| 05 | close | int status | int fd | - | - |
|
||||
| 06 | malloc | void* buf | int count | - | - |
|
||||
| 07 | free | - | void* buf | - | - |
|
||||
| 08 | seek | #TBD | | | |
|
||||
|
||||
|
||||
+54
-42
@@ -3,6 +3,10 @@
|
||||
|
||||
#include <kernel.h>
|
||||
|
||||
// Privilege levels
|
||||
#define DPL_KERNEL 0
|
||||
#define DPL_USER 3
|
||||
|
||||
/*
|
||||
* Segment selector explanation (why kcode is 0x08?)
|
||||
*
|
||||
@@ -26,8 +30,11 @@
|
||||
* s 0 for the GDT and the index is one, because it
|
||||
* is the second entry in our GDT (after null desc.)
|
||||
*/
|
||||
#define SEGMENT_SEL(_priv, _tab, _i)\
|
||||
(uint16_t)(((_i) << 3) | ((_tab) << 2) | (_priv))
|
||||
#define SEGMENT_SEL(_idx, _tab, _dpl) (\
|
||||
(((_dpl) ) & 3) | \
|
||||
(((_tab) << 2) & 4) | \
|
||||
(((_idx) << 3) ) \
|
||||
)
|
||||
|
||||
/*
|
||||
* Entries:
|
||||
@@ -40,21 +47,43 @@
|
||||
*/
|
||||
#define GDT_ENTRIES 6
|
||||
#define GDT_NULL SEGMENT_SEL(0, 0, 0)
|
||||
#define GDT_KERNEL_CODE SEGMENT_SEL(0, 0, 1)
|
||||
#define GDT_KERNEL_DATA SEGMENT_SEL(0, 0, 2)
|
||||
#define GDT_USER_CODE SEGMENT_SEL(3, 0, 3)
|
||||
#define GDT_USER_DATA SEGMENT_SEL(3, 0, 4)
|
||||
#define GDT_TSS SEGMENT_SEL(0, 0, 5)
|
||||
#define GDT_KERNEL_CODE SEGMENT_SEL(1, 0, DPL_KERNEL)
|
||||
#define GDT_KERNEL_DATA SEGMENT_SEL(2, 0, DPL_KERNEL)
|
||||
#define GDT_USER_CODE SEGMENT_SEL(3, 0, DPL_USER)
|
||||
#define GDT_USER_DATA SEGMENT_SEL(4, 0, DPL_USER)
|
||||
#define GDT_TSS SEGMENT_SEL(5, 0, DPL_KERNEL)
|
||||
|
||||
#define GDT_DESC(_lim, _base, _acc, _flag) (struct gdt_entry){\
|
||||
.limit = (_lim) & 0xffff,\
|
||||
.base_low = (uint16_t)(_base),\
|
||||
.base_mid = (uint8_t)((_base) >> 16),\
|
||||
.access = (_acc),\
|
||||
.flags = ((_flag & 0x0f) << 4) | (((_lim) >> 16) & 0x0f),\
|
||||
.base_high = (uint8_t)((_base) >> 24),\
|
||||
#define GDT_DESC(_lim, _base, _access, _flags) (struct gdt_entry) { \
|
||||
.base_low = (_base) & 0xFFFF, \
|
||||
.base_mid = ((_base) >> 16) & 0xFF, \
|
||||
.base_high = ((_base) >> 24) & 0xFF, \
|
||||
.limit = (_lim) & 0xFFFF, \
|
||||
.flags = ((_flags) & 0xF0) | (((_lim) >> 16) & 0x0F), \
|
||||
.access = (_access) \
|
||||
}
|
||||
|
||||
/*
|
||||
* GDT descriptor/entry explanation
|
||||
*
|
||||
* 0-15 low 16 bits of limit
|
||||
* 15-31 low 16 bits of base
|
||||
* 32-39 mid 8 bits of base
|
||||
* 40-47 access byte
|
||||
* 40 Accessed
|
||||
* 41 Read/Write
|
||||
* 42 Direction/Conforming
|
||||
* 43 Executable
|
||||
* 44 Descriptor type
|
||||
* 45-46 DPL
|
||||
* 47 Present
|
||||
* 48-51 high 4 bits of limit
|
||||
* 52-55 flags
|
||||
* 52 Reserved
|
||||
* 53 Long-mode
|
||||
* 54 Size (0 - 16-bit, 1 - 32-bit)
|
||||
* 55 Granularity (0 - limit in bytes, 1 - limit in pages)
|
||||
* 56-63 high 8 bits of base
|
||||
*/
|
||||
struct gdt_entry {
|
||||
uint16_t limit;
|
||||
uint16_t base_low;
|
||||
@@ -70,40 +99,23 @@ struct gdt_pointer {
|
||||
} __packed;
|
||||
|
||||
struct tss_pointer {
|
||||
uint16_t link;
|
||||
uint16_t __reserved0;
|
||||
uint16_t prev_task, __reserved0;
|
||||
uint32_t esp0;
|
||||
uint16_t ss0;
|
||||
uint16_t __reserved1;
|
||||
uint16_t ss0, __reserved1;
|
||||
uint32_t esp1;
|
||||
uint16_t ss1;
|
||||
uint16_t __reserved2;
|
||||
uint16_t ss1, __reserved2;
|
||||
uint32_t esp2;
|
||||
uint16_t ss2;
|
||||
uint16_t __reserved3;
|
||||
uint16_t ss2, __reserved3;
|
||||
uint32_t cr3;
|
||||
uint32_t eip;
|
||||
uint32_t eflags;
|
||||
uint32_t eax;
|
||||
uint32_t ecx;
|
||||
uint32_t edx;
|
||||
uint32_t ebx;
|
||||
uint32_t esp;
|
||||
uint32_t ebp;
|
||||
uint32_t esi;
|
||||
uint32_t edi;
|
||||
uint16_t es;
|
||||
uint16_t __reserved4;
|
||||
uint16_t cs;
|
||||
uint16_t __reserved5;
|
||||
uint16_t ss;
|
||||
uint16_t __reserved6;
|
||||
uint16_t ds;
|
||||
uint16_t __reserved7;
|
||||
uint16_t fs;
|
||||
uint16_t __reserved8;
|
||||
uint16_t gs;
|
||||
uint16_t __reserved9;
|
||||
uint32_t eax, ecx, edx, ebx, esp, ebp, esi, edi;
|
||||
uint16_t es, __reserved4;
|
||||
uint16_t cs, __reserved5;
|
||||
uint16_t ss, __reserved6;
|
||||
uint16_t ds, __reserved7;
|
||||
uint16_t fs, __reserved8;
|
||||
uint16_t gs, __reserved9;
|
||||
uint16_t ldtr;
|
||||
uint32_t __reserved10;
|
||||
uint16_t iopb;
|
||||
|
||||
@@ -22,6 +22,6 @@ int isr_handle_interrupt(struct isr_int_frame iframe);
|
||||
// Assembly definitions (isr.s)
|
||||
extern void isr_return(void);
|
||||
extern void* isr_redirect_table[48]; // ISR Table
|
||||
extern void isr128(); // int 0x80 - syscall
|
||||
extern void isr128(void); // int 0x80 - syscall
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#ifndef _SCHED_H
|
||||
#define _SCHED_H
|
||||
|
||||
// Amount of CPU time each process gets (in ms)
|
||||
#define SCHED_QUANTUM 40
|
||||
|
||||
void schedule(void);
|
||||
|
||||
#endif
|
||||
@@ -9,6 +9,8 @@
|
||||
#define PIT_CH2 0x42
|
||||
#define PIT_CMD 0x43
|
||||
|
||||
#define TIMER_FREQ 1000
|
||||
|
||||
int timer_init(void);
|
||||
void timer_wait(size_t ticks);
|
||||
|
||||
|
||||
+15
-25
@@ -10,14 +10,14 @@ void gdt_reload(void) {
|
||||
__asm__ volatile (
|
||||
"cli\n"
|
||||
"lgdt (%0)\n"
|
||||
"ljmp $0x08, $1f\n"
|
||||
"1:\n"
|
||||
"movw $0x10, %%ax\n"
|
||||
"movw %%ax, %%ds\n"
|
||||
"movw %%ax, %%es\n"
|
||||
"movw %%ax, %%fs\n"
|
||||
"movw %%ax, %%gs\n"
|
||||
"movw %%ax, %%ss\n"
|
||||
"ljmp $0x08, $1f\n"
|
||||
"1:"
|
||||
:
|
||||
: "m" (gdtp)
|
||||
: "memory", "rax"
|
||||
@@ -26,38 +26,28 @@ void gdt_reload(void) {
|
||||
|
||||
void tss_reload(void) {
|
||||
__asm__ volatile ("ltr %0" : : "rm"(GDT_TSS));
|
||||
gdt[5].access |= 0x02;
|
||||
}
|
||||
|
||||
void tss_esp0(uint32_t esp0) { tss.esp0 = esp0; }
|
||||
|
||||
int gdt_init(void) {
|
||||
// Null descriptor (0)
|
||||
gdt[0] = GDT_DESC(0, 0, 0, 0);
|
||||
|
||||
// Kernel code (8)
|
||||
gdt[1] = GDT_DESC(0xfffff, 0, 0x9a, 0xc);
|
||||
|
||||
// Kernel data (16)
|
||||
gdt[2] = GDT_DESC(0xfffff, 0, 0x92, 0xc);
|
||||
|
||||
// User code (24)
|
||||
gdt[3] = GDT_DESC(0xfffff, 0, 0xfa, 0xc);
|
||||
|
||||
// User data (32)
|
||||
gdt[4] = GDT_DESC(0xfffff, 0, 0xf2, 0xc);
|
||||
|
||||
// TSS (40)
|
||||
memset(&tss, 0, sizeof(tss));
|
||||
gdt[5] = GDT_DESC(sizeof(tss) - 1, (uint32_t)&tss, 0x89, 0x40);
|
||||
tss.iopb = sizeof(struct tss_pointer);
|
||||
tss.esp0 = __kernel_stack_top;
|
||||
tss.ss0 = GDT_KERNEL_DATA;
|
||||
|
||||
// GDT Pointer
|
||||
gdtp.limit = sizeof(gdt) - 1;
|
||||
gdtp.base = (uint32_t)&gdt;
|
||||
|
||||
// Setup TSS
|
||||
memset(&tss, 0, sizeof(tss));
|
||||
tss.ss0 = GDT_KERNEL_DATA;
|
||||
tss.iopb = sizeof(struct tss_pointer);
|
||||
|
||||
// Load descriptors
|
||||
gdt[0] = GDT_DESC(0, 0, 0, 0); // Null descriptor
|
||||
gdt[1] = GDT_DESC(0xFFFFFFFF, 0, 0x9A, 0xC0); // Kernel code
|
||||
gdt[2] = GDT_DESC(0xFFFFFFFF, 0, 0x92, 0xC0); // Kernel data
|
||||
gdt[3] = GDT_DESC(0xFFFFFFFF, 0, 0xFA, 0xC0); // User code
|
||||
gdt[4] = GDT_DESC(0xFFFFFFFF, 0, 0xF2, 0xC0); // User data
|
||||
gdt[5] = GDT_DESC(sizeof(tss) - 1, (uint32_t)&tss, 0x89, 0);
|
||||
|
||||
gdt_reload();
|
||||
tss_reload();
|
||||
|
||||
|
||||
+2
-1
@@ -89,12 +89,13 @@ void isr_send_eoi(uint8_t irq) {
|
||||
int isr_handle_interrupt(struct isr_int_frame iframe) {
|
||||
if (iframe.interrupt < 32) {
|
||||
// ISRs 0-31 - Exceptions
|
||||
if (iframe.error) debugf("[kernel] Error code: 0x%08x\n", iframe.error);
|
||||
panic(exceptions[iframe.interrupt]);
|
||||
} else if (iframe.interrupt >= 32 && iframe.interrupt < 48) {
|
||||
// ISRs 32-47 - Hardware interrupts
|
||||
uint8_t irq = iframe.interrupt - 32;
|
||||
if (irq_handlers[irq]) irq_handlers[irq](&iframe);
|
||||
isr_send_eoi(irq);
|
||||
if (irq_handlers[irq]) irq_handlers[irq](&iframe);
|
||||
} else if (iframe.interrupt == 128) {
|
||||
// ISR 128 - System call
|
||||
return syscall_handler(&iframe);
|
||||
|
||||
+2
-2
@@ -30,14 +30,14 @@ isr_common:
|
||||
[extern isr_handle_interrupt]
|
||||
call isr_handle_interrupt
|
||||
[global isr_return]
|
||||
isr_return: ; Also used by task switching
|
||||
isr_return: ; Also used by a newly created task
|
||||
pop gs
|
||||
pop fs
|
||||
pop es
|
||||
pop ds
|
||||
popad
|
||||
add esp, 8 ; Pop interupt number and error code
|
||||
iretd ; Pop CS, EIP & EFLAGS (Also SS & ESP if it's from userspace)
|
||||
iret ; Pop CS, EIP & EFLAGS (Also SS & ESP if it's from userspace)
|
||||
|
||||
%macro ISR_WITH_ERRNO 1
|
||||
isr%1:
|
||||
|
||||
+30
-4
@@ -10,6 +10,8 @@
|
||||
#include <pmm.h>
|
||||
#include <tty.h>
|
||||
|
||||
void test(void);
|
||||
|
||||
__noreturn
|
||||
void kmain(void* ptr, uint32_t magic) {
|
||||
if (boot_init_mb1(ptr, magic) != 0) panic("Unsupported bootloader.");
|
||||
@@ -20,6 +22,8 @@ void kmain(void* ptr, uint32_t magic) {
|
||||
|
||||
// System init
|
||||
if (gdt_init() != 0) panic("Failed to initalize GDT.");
|
||||
debugf("[gdt] Segment selectors: NULL: 0x%04x, KCODE: 0x%04x, KDATA: 0x%04x, UCODE: 0x%04x, UDATA: 0x%04x, TSS: 0x%04x\n",
|
||||
GDT_NULL, GDT_KERNEL_CODE, GDT_KERNEL_DATA, GDT_USER_CODE, GDT_USER_DATA, GDT_TSS);
|
||||
if (isr_init() != 0) panic("Failed to initalize ISRs.");
|
||||
if (pag_init() != 0) panic("Failed to initalize Paging.");
|
||||
|
||||
@@ -34,14 +38,36 @@ void kmain(void* ptr, uint32_t magic) {
|
||||
if (timer_init() != 0) panic("Failed to initialize timer.");
|
||||
if (task_init() != 0) panic("Failed to initialize tasking.");
|
||||
|
||||
// TODO: Scheduler
|
||||
// Create a temporary test task
|
||||
task_create(task_create_id(), (uint32_t)test, 1, pag_get_pagedir(), 0, NULL);
|
||||
|
||||
size_t delay = 0;
|
||||
while (1) {
|
||||
delay++;
|
||||
|
||||
if (delay < 0x1FFFFFF) continue;
|
||||
delay = 0;
|
||||
|
||||
tty_puts("[kernel] Hello!\n");
|
||||
}
|
||||
|
||||
// TODO: Syscalls
|
||||
// TODO: Interprocess communication
|
||||
// TODO: Tasking
|
||||
// TODO: Load & run /sbin/init
|
||||
|
||||
while (1);
|
||||
|
||||
panic("kmain() reached end.");
|
||||
}
|
||||
|
||||
// An imaginary second task
|
||||
void test(void) {
|
||||
size_t delay = 0;
|
||||
while (1) {
|
||||
delay++;
|
||||
|
||||
if (delay < 0x2FFFFFF) continue;
|
||||
delay = 0;
|
||||
|
||||
tty_puts("[test] Hello!\n");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
#include <kernel.h>
|
||||
#include <task.h>
|
||||
#include <gdt.h>
|
||||
|
||||
#include <sched.h>
|
||||
|
||||
void schedule(void) {
|
||||
if (task_first == NULL) return;
|
||||
|
||||
struct task* next = task_current->next;
|
||||
if (next == NULL) next = task_first;
|
||||
|
||||
while (next->state != TASK_STATE_READY) {
|
||||
next = next->next;
|
||||
if (next == NULL) next = task_first;
|
||||
}
|
||||
|
||||
struct task* old = task_current;
|
||||
task_current = next;
|
||||
|
||||
tss_esp0(next->kstack_bottom);
|
||||
|
||||
// NOTE: Might not return
|
||||
task_switch(old, next);
|
||||
}
|
||||
|
||||
+8
-5
@@ -31,7 +31,7 @@ int task_init(void) {
|
||||
}
|
||||
|
||||
void task_create(size_t id, uint32_t eip, int kernel_task, uint32_t* pagedir, int argc, char** argv) {
|
||||
__asm__ volatile ("cli"); // Make sure we don't get interrupted
|
||||
__asm__ volatile ("cli"); // Make sure we don't get interrupted
|
||||
|
||||
uint32_t kstack = (uint32_t)kmalloc(0x1000 - 16) + (0x1000 - 16);
|
||||
uint8_t* kesp = (uint8_t*)kstack;
|
||||
@@ -55,7 +55,7 @@ void task_create(size_t id, uint32_t eip, int kernel_task, uint32_t* pagedir, in
|
||||
trap->eflags = 0x200; // Enable interrupts
|
||||
trap->eip = eip;
|
||||
|
||||
kesp -= sizeof(struct isr_int_frame);
|
||||
kesp -= sizeof(struct task_switch_ctx);
|
||||
struct task_switch_ctx* ctx = (struct task_switch_ctx*)kesp;
|
||||
ctx->edi = 0;
|
||||
ctx->esi = 0;
|
||||
@@ -82,6 +82,9 @@ void task_create(size_t id, uint32_t eip, int kernel_task, uint32_t* pagedir, in
|
||||
new->heap_start = USER_HEAP;
|
||||
new->heap_end = USER_HEAP;
|
||||
|
||||
debugf("[task] NEW! Task #%d - EIP = 0x%08x, ESP = 0x%08x%s\n",
|
||||
id, eip, kesp, kernel_task ? " (kernel)" : "");
|
||||
|
||||
// We don't need to setup a stack frame
|
||||
if (kernel_task) {
|
||||
__asm__ volatile ("sti");
|
||||
@@ -91,7 +94,7 @@ void task_create(size_t id, uint32_t eip, int kernel_task, uint32_t* pagedir, in
|
||||
void* old_pagedir = pag_get_pagedir();
|
||||
pag_set_pagedir(new->pagedir);
|
||||
|
||||
// Setup argc, argv & environ
|
||||
// Setup argc, argv
|
||||
size_t arg_len = 0;
|
||||
for (int i = 0; i < argc; i++) arg_len += strlen(argv[i]) + 1;
|
||||
size_t page_count = dceil(4 + (argc * 4) + 10 + arg_len, PAGE_SIZE);
|
||||
@@ -186,7 +189,7 @@ void task_set_user_heap(struct task* task, size_t heap_end) {
|
||||
else if (new_page_top > old_page_top) { // Grow
|
||||
int num = new_page_top - old_page_top;
|
||||
|
||||
for (size_t i = 0; i < num; i++) {
|
||||
for (int i = 0; i < num; i++) {
|
||||
uint32_t paddr = (uint32_t)pmm_alloc();
|
||||
if (paddr == 0) {
|
||||
debugf("[task] Task %d tried to grow heap too much.\n", task->id);
|
||||
@@ -199,7 +202,7 @@ void task_set_user_heap(struct task* task, size_t heap_end) {
|
||||
} else { // Shrink
|
||||
int num = old_page_top - new_page_top;
|
||||
|
||||
for (size_t i = 0; i < num; i++) {
|
||||
for (int i = 0; i < num; i++) {
|
||||
uint32_t vaddr = old_page_top * PAGE_SIZE + i * PAGE_SIZE;
|
||||
uint32_t paddr = pag_virt_to_phys(vaddr);
|
||||
pmm_free(paddr);
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ bits 32
|
||||
global task_switch
|
||||
task_switch:
|
||||
mov eax, [esp + 4] ; eax = struct task* old
|
||||
mov edx, [esp + 8] ; ebx = struct task* nex
|
||||
|
||||
mov edx, [esp + 8] ; ebx = struct task* new
|
||||
|
||||
;; Push registers that weren't pushed by the CDECL call
|
||||
push ebp
|
||||
push ebx
|
||||
|
||||
+5
-2
@@ -1,5 +1,6 @@
|
||||
#include <kernel.h>
|
||||
#include <system.h>
|
||||
#include <sched.h>
|
||||
#include <isr.h>
|
||||
#include <tty.h>
|
||||
|
||||
@@ -19,7 +20,7 @@ void timer_wait(size_t t) {
|
||||
|
||||
int pit_init(void) {
|
||||
isr_register(0, pit_handler);
|
||||
pit_phase(0, 2, 1000);
|
||||
pit_phase(0, 2, TIMER_FREQ);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -39,8 +40,10 @@ void pit_phase(uint8_t ch, uint8_t mode, uint32_t hz) {
|
||||
}
|
||||
|
||||
void pit_handler(struct isr_int_frame* iframe) {
|
||||
(void)iframe; // We must take it as an argument even if we don't use it
|
||||
timer_ticks++;
|
||||
|
||||
if (timer_ticks % 1000 == 0) tty_puts(".");
|
||||
// TODO: This assumes the timer is at 1000 Hz
|
||||
if (timer_ticks % SCHED_QUANTUM == 0) schedule();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user