-
Notifications
You must be signed in to change notification settings - Fork 69
Kernel
This page teaches you how the MentOS kernel works - the brain of the operating system.
Important: All code examples and implementation details are from the actual MentOS kernel source code. You can explore these files in kernel/ directory of the MentOS repository.
The kernel is the core program that:
- Controls the hardware - CPU, RAM, disk, keyboard, screen
- Manages resources - Decides which program gets CPU time, memory, disk space
- Provides services - File reading, process creation, network communication
- Enforces security - Prevents programs from interfering with each other
Think of it as the traffic controller of your computer:
- Multiple programs want CPU time → kernel schedules them
- Multiple programs need memory → kernel allocates RAM
- Programs want to read files → kernel accesses the disk
The kernel runs in ring 0 (privileged mode) while your programs run in ring 3 (restricted mode). Programs must ASK the kernel for help through system calls.
┌──────────────────────────────────────────────────────┐
│ User Programs (ring 3) │
│ shell, ls, cat, editor, games, etc. │
└─────────────────┬────────────────────────────────────┘
│ System Calls (INT 0x80)
┌─────────────────┴────────────────────────────────────┐
│ Kernel (ring 0) │
│ │
│ ┌───────────────┐ ┌──────────────┐ ┌───────────┐ │
│ │ Process │ │ Memory │ │ File │ │
│ │ Management │ │ Management │ │ System │ │
│ │ │ │ │ │ │ │
│ │ • Scheduler │ │ • Paging │ │ • VFS │ │
│ │ • fork/exec │ │ • Allocators │ │ • EXT2 │ │
│ │ • Signals │ │ • Heap mgmt │ │ • ProcFS │ │
│ └───────────────┘ └──────────────┘ └───────────┘ │
│ │
│ ┌───────────────┐ ┌──────────────┐ ┌───────────┐ │
│ │ Drivers │ │ Interrupts │ │ IPC │ │
│ │ │ │ │ │ │ │
│ │ • Keyboard │ │ • Timer │ │ • Pipes │ │
│ │ • Disk (ATA) │ │ • Syscalls │ │ • Signals │ │
│ │ • RTC/PS2 │ │ • Exceptions │ │ • SysV IPC│ │
│ └───────────────┘ └──────────────┘ └───────────┘ │
└─────────────────┬────────────────────────────────────┘
│
┌─────────────────┴────────────────────────────────────┐
│ Hardware (CPU, RAM, Disk, etc.) │
└───────────────────────────────────────────────────────┘
Each box solves a specific problem. Let's understand them one by one.
The Problem: You have 1 CPU but want to run 10 programs simultaneously.
The Solution: The kernel creates the illusion of multiple CPUs by rapidly switching between programs.
Timeline (on every timer interrupt):
───────────────────────────────────────────────────>
Program A │ Program B │ Program A │ Program C
running │ running │ running │ running
The scheduler decides who runs next:
- Timer interrupt fires
- Save current program's state (registers, stack pointer)
- Pick next program to run (based on priority/fairness)
- Load new program's state
- Resume execution
In MentOS specifically, the PIT is programmed with TICKS_PER_SECOND = 1193 Hz
(kernel/inc/hardware/timer.h), so the tick handler runs roughly every 0.84 ms. Textbook
examples usually quote 100 Hz / 10 ms; that is a common Unix figure, not MentOS's.
Note also that the timer tick is not the only reschedule point. scheduler_run() is called
from the timer IRQ handler, at the end of the int 0x80 syscall handler, from the generic
exception path, from the page-fault handler, and from signal delivery — so a process that
blocks inside a system call yields the CPU immediately rather than waiting for the next tick.
Every running program is represented by a task_struct (see kernel/inc/process/process.h):
typedef struct task_struct {
pid_t pid; // Process ID
__volatile__ long state; // TASK_RUNNING, TASK_STOPPED, etc.
// Scheduling
list_head_t run_list;
sched_entity_t se;
// CPU/FPU state
thread_struct_t thread;
// Memory
mm_struct_t *mm;
// Files
vfs_file_descriptor_t *fd_list;
int max_fd;
// Signals
sighand_t sighand;
sigset_t blocked;
sigpending_t pending;
// Misc
char name[TASK_NAME_MAX_LENGTH];
char cwd[PATH_MAX];
} task_struct;MentOS Implementation: See kernel/inc/process/process.h for full definition and kernel/src/process/ for task management code.
Real-world analogy: Think of task_struct as a "snapshot" of a program. The kernel can freeze any program, save its snapshot, load another program's snapshot, and resume it - like saving your game progress!
MentOS lets you pick the scheduling policy at build time with the CMake option
SCHEDULER_TYPE (kernel/CMakeLists.txt). All the policies below are selectable, but only
Round-Robin is implemented: in kernel/src/process/scheduler_algorithm.c the other branches
are deliberately left as /*...*/ student exercises. Selecting one of them intentionally
causes the build to fail at those placeholders until the missing implementation is completed.
Read the sections below as "the algorithm you are being asked to implement", not "what the
kernel does today". See Scheduling.
1. Round-Robin (RR) - Default, and the only complete implementation
Queue: [A, B, C]
→ Run A for 10ms
→ Run B for 10ms
→ Run C for 10ms
→ Repeat
Implementation: __scheduler_rr() in kernel/src/process/scheduler_algorithm.c (complete)
2. Completely Fair Scheduler (CFS) - Linux-inspired (exercise: __scheduler_cfs())
Track "virtual runtime" for each process:
A: 100ms
B: 150ms ← runs more recently
C: 80ms ← runs longest ago
→ Pick process with lowest vruntime (C)
Implementation: __scheduler_cfs() in kernel/src/process/scheduler_algorithm.c
(skeleton only — the vruntime comparison is left blank)
3. Priority-based - Higher priority = more CPU (exercise: __scheduler_priority())
Priority queue:
High priority (20): [Process A]
Medium priority (10): [Process B, Process C]
Low priority (0): [Process D]
→ Always run highest priority first
See Scheduling for details on each algorithm.
The fork() syscall creates a new process:
// User program:
pid_t pid = fork();
if (pid == 0) {
// Child process
printf("I'm the child!\n");
} else {
// Parent process
printf("I created child PID %d\n", pid);
}What happens in the kernel:
// kernel/src/process/process.c
pid_t sys_fork(pt_regs_t *f)
{
task_struct *current = scheduler_get_current_process();
scheduler_store_context(f, current);
task_struct *proc = __alloc_task(current, current, current->name);
proc->mm = mm_clone(current->mm);
// Child returns 0
proc->thread.regs.eax = 0;
proc->thread.regs.eflags |= EFLAG_IF;
// Inherit ids
proc->sid = current->sid;
proc->pgid = current->pgid;
proc->uid = current->uid;
proc->ruid = current->ruid;
proc->gid = current->gid;
proc->rgid = current->rgid;
scheduler_enqueue_task(proc);
return proc->pid;
}The classic trick: Copy-on-Write (COW)
In mature Unix kernels, fork() avoids copying memory up front:
- Child shares parent's memory pages
- Pages marked "read-only"
- If either writes, kernel copies the page first
- This makes
fork()fast!
What MentOS actually does today. MentOS has the COW machinery — vm_area_clone() takes a
cow argument, page table entries carry a kernel_cow bit, and __page_handle_cow() in
kernel/src/mem/page_fault.c resolves COW faults — but sys_fork() does not use it.
The chain is:
sys_fork() → mm_clone(current->mm) → vm_area_clone(mm, area, /* cow = */ 0, GFP_HIGHUSER)
With cow == 0, vm_area_clone() allocates fresh physical pages for the child and does an
eager vmem_memcpy() of the whole area. So in the current tree fork() performs a full,
eager copy of the address space, and the COW path is exercised by other parts of the VM
subsystem rather than by process creation.
This is worth knowing when you measure fork() performance or set a breakpoint expecting
COW faults — you will not see them coming from fork().
The Problem: Multiple programs running, but they all need memory. How do we prevent them from overwriting each other?
The Solution: Virtual memory - each program thinks it has the entire address space (0x00000000 - 0xBFFFFFFF) to itself!
MentOS Implementation: kernel/src/mem/ - Contains paging, page tables, memory allocators
Program A thinks: Program B thinks:
0x00000000: My code 0x00000000: My code
0x10000000: My heap 0x10000000: My heap
0xBFFFFFFF: My stack 0xBFFFFFFF: My stack
But in PHYSICAL RAM:
0x00100000: Actually Program A's code
0x00200000: Actually Program B's code
0x00300000: Actually Program A's heap
0x00400000: Actually Program B's heap
The CPU's Memory Management Unit (MMU) translates virtual addresses to physical addresses using page tables.
Classic 32-bit x86 paging with 4 KiB pages splits a virtual address 10 / 10 / 12:
Virtual Address: 0x12345678
= 0001 0010 00 | 11 0100 0101 | 0110 0111 1000
└── 10 bits ─┘ └── 10 bits ─┘ └─── 12 bits ──┘
│
├─ Top 10 bits: (0x12345678 >> 22) & 0x3FF = 0x048
│ │
│ └──> Page Directory[0x048] → Points to Page Table
│ │
├─ Next 10 bits: (0x12345678 >> 12) & 0x3FF = 0x345
│ │
│ └──> Page Table[0x345] → (assume it holds frame number 0x00400)
│
└─ Bottom 12 bits: 0x12345678 & 0xFFF = 0x678 (offset within page)
│
└──> Physical Address: (0x00400 × 4096) + 0x678 = 0x00400678
Two things to be careful about when reading this example:
- The directory index, table index and offset are derived from the virtual address — you can
recompute all three with the shifts shown above. (An earlier version of this page used
0x0D1as the page-table index; that value does not follow from0x12345678. The correct index is0x345.) - The frame number
0x00400is not derived from the virtual address. It is whatever the operating system stored in that page table entry; here we simply assume the value0x00400so the arithmetic has something to work with. Change the page table contents and the physical address changes, while the three indices stay the same.
One more consequence worth remembering: the CPU caches these translations in the TLB. If you
modify a page directory or page table entry for an address that has already been touched, the
hardware keeps using the stale translation until the entry is invalidated (invlpg, or a CR3
reload — which is what switching page directory during a context switch does). Page tables are
not re-read on every memory access.
Data structures:
// Page directory (one per process)
struct page_directory {
page_dir_entry_t entries[1024]; // Each points to a page table
};
// Page table (many per process)
struct page_table {
page_table_entry_t pages[1024]; // Each points to a 4KB physical page
};
// Page table entry
struct page_table_entry {
unsigned int present : 1; // Is page in RAM?
unsigned int rw : 1; // Read/write or read-only?
unsigned int user : 1; // User-accessible or kernel-only?
unsigned int frame : 20; // Physical page frame number
};The kernel has multiple memory allocators for different needs:
1. Buddy Allocator - Allocates physical pages (4KB chunks)
Free memory split into powers of 2:
Order 0: 4KB pages
Order 1: 8KB chunks (2 pages)
Order 2: 16KB chunks (4 pages)
...
Order 10: 4MB chunks (1024 pages)
Request 12KB?
→ Split 16KB chunk into 8KB + 8KB
→ Split 8KB into 4KB + 4KB
→ Give 8KB + 4KB = 12KB
2. Slab Allocator - Caches common object sizes
Frequently allocated objects (task_struct, file, inode):
→ Pre-allocate a "slab" of these objects
→ Allocation is just taking from cache (fast!)
→ Deallocation returns to cache (no fragmentation)
3. kmalloc/kfree - Kernel's malloc
kmalloc(1024) → Uses slab allocator for common sizes
kmalloc(100000) → Uses buddy allocator for large chunks
The Problem: The disk is just a giant array of bytes. How do we organize it into files and folders?
The Solution: The Virtual File System (VFS) provides an abstraction layer. Programs use open/read/write, and VFS translates to the actual filesystem (EXT2, ProcFS, etc.).
User Program:
fd = open("/home/user/file.txt", O_RDONLY);
read(fd, buffer, 100);
↓
System Call (INT 0x80)
↓
VFS Layer:
vfs_open("/home/user/file.txt")
→ Parse path: / → home → user → file.txt
→ Resolve to a filesystem object
→ Create vfs_file_t structure
→ Install it in the calling task's fd_list and return that slot's index
(a file descriptor is a PER-PROCESS index, not a global identifier)
↓
EXT2 Filesystem Driver:
ext2_read(inode, buffer, offset, count)
→ Read inode's block list
→ Find physical disk blocks
→ Call disk driver
↓
ATA Disk Driver:
ata_read_sectors(block_number, buffer)
→ Send commands to disk controller
→ Wait for disk to read data
→ Copy data to buffer
MentOS exposes VFS objects via vfs_file_t and related types (see kernel/inc/fs/vfs_types.h).
A terminology warning before you read this. In Linux, the responsibilities below are split
across three structures: struct inode (the on-disk object: owner, mode, size, link count),
struct dentry (the name and its place in the tree) and struct file (one open instance, with
its own file position). MentOS folds all three into a single vfs_file_t, which carries the
name, the inode number, the permissions and f_pos at once. The Unix concepts still
apply, but do not expect a one-to-one mapping onto Linux's VFS.
typedef struct vfs_file {
char name[NAME_MAX];
void *device;
uint32_t mask;
uint32_t uid;
uint32_t gid;
uint32_t flags;
uint32_t ino;
uint32_t length;
uint32_t open_flags;
size_t f_pos;
vfs_file_operations_t *fs_operations;
} vfs_file_t;
typedef struct super_block {
char name[NAME_MAX];
char path[PATH_MAX];
struct vfs_file *root;
file_system_type_t *type;
} super_block_t;
typedef struct vfs_file_descriptor {
struct vfs_file *file_struct;
int flags_mask;
} vfs_file_descriptor_t;See File Systems for complete details.
The Problem: Hardware needs to notify the CPU (keyboard pressed, disk finished reading, timer tick).
The Solution: Interrupts - hardware signals that pause the CPU and run a handler function.
MentOS Implementation: kernel/src/descriptor_tables/ (GDT/IDT setup) and kernel/src/hardware/ (timer/IRQ handling)
Hardware Interrupts (IRQs):
IRQ 0: Timer (MentOS programs the PIT at 1193 Hz)
IRQ 1: Keyboard
IRQ 14/15: Disk (ATA)
Software Interrupts:
INT 0x80: System calls (see kernel/inc/system/syscall.h)
CPU Exceptions:
0: Divide by zero
6: Invalid opcode
13: General protection fault
14: Page fault
1. CPU executing normal code:
mov eax, [ebx]
add eax, 5
← Timer interrupt fires (IRQ 0)
2. CPU automatically:
• Pushes current state (EFLAGS, CS, EIP) onto stack
• Looks up handler in IDT (Interrupt Descriptor Table)
• Jumps to handler
3. Handler runs:
void timer_handler(pt_regs_t *regs) {
tick_count++;
scheduler_tick(); // Maybe switch processes
}
4. Handler returns (IRET instruction):
• Pops state from stack
• Resumes interrupted code
struct idt_entry {
uint16_t offset_low; // Handler address (low 16 bits)
uint16_t selector; // Code segment
uint8_t zero; // Reserved
uint8_t type_attr; // Gate type, DPL, present
uint16_t offset_high; // Handler address (high 16 bits)
};
// 256 entries (0-255)
idt_entry_t idt[256];Setting up an interrupt handler:
// kernel/src/descriptor_tables/idt.c
void idt_install_handler(uint8_t num, void (*handler)(pt_regs_t *)) {
idt[num].offset_low = (uint32_t)handler & 0xFFFF;
idt[num].offset_high = ((uint32_t)handler >> 16) & 0xFFFF;
idt[num].selector = KERNEL_CODE_SEGMENT;
idt[num].type_attr = 0x8E; // Present, ring 0, interrupt gate
}The Problem: Each hardware device has its own interface (registers, commands, protocols).
The Solution: Device drivers - kernel modules that know how to talk to specific hardware.
// kernel/src/drivers/keyboard.c
void keyboard_init(void) {
// Install interrupt handler for IRQ 1
install_interrupt_handler(IRQ_KEYBOARD, keyboard_handler);
enable_irq(IRQ_KEYBOARD);
}
void keyboard_handler(pt_regs_t *regs) {
// Read scan code from keyboard controller
uint8_t scancode = inb(KEYBOARD_DATA_PORT); // I/O port 0x60
// Translate scancode to ASCII
char key = scancode_to_ascii(scancode);
// Add to keyboard buffer
keyboard_buffer_push(key);
}// kernel/src/drivers/ata.c
void ata_read_sector(uint32_t lba, void *buffer) {
// Send LBA (Logical Block Address) to disk
outb(ATA_PORT_LBA_LOW, lba & 0xFF);
outb(ATA_PORT_LBA_MID, (lba >> 8) & 0xFF);
outb(ATA_PORT_LBA_HIGH, (lba >> 16) & 0xFF);
// Send READ command
outb(ATA_PORT_COMMAND, ATA_CMD_READ);
// Wait for disk ready
while (!(inb(ATA_PORT_STATUS) & ATA_STATUS_DRQ));
// Read 512 bytes from data port
insw(ATA_PORT_DATA, buffer, 256); // Read 256 words (512 bytes)
}When the bootloader jumps to the kernel, here's what happens:
kmain() does not start from a bare CPU. By the time it runs, the bootstrap code in
boot/ has already put the CPU in 32-bit protected mode, built bootstrap page tables and
enabled paging. kmain() receives a boot_info_t * describing that state.
The following mirrors the real order in kernel/src/kernel.c; note especially that
paging_init() comes after the descriptor tables and the syscall table, because the
bootstrap mappings are good enough until then.
// kernel/src/kernel.c
int kmain(boot_info_t *boot_informations)
{
boot_info = *boot_informations;
// Refuse to boot unless a Multiboot loader put 0x2BADB002 in EAX.
if (boot_info.magic != MULTIBOOT_BOOTLOADER_MAGIC) { return 1; }
resource_register_init(); // 1. resource registry
video_init(); // 2. VGA text mode
/* ...initialize Multiboot modules... */
pmmngr_init(&boot_info); // 3. physical memory manager (buddy allocator, zones)
kmem_cache_init(); // 4. slab allocator
init_gdt(); // 5. GDT + TSS descriptor
init_idt(); // 6. IDT (including vector 128 for int 0x80)
syscall_init(); // 7. fill sys_call_table, install the int 0x80 handler
pic8259_init_irq(); // 8. PIC / IRQ routing
relocate_modules();
paging_init(&boot_info); // 9. the kernel's OWN paging subsystem
vmem_init(); // 10. virtual memory mapping helpers
timer_install(); // 11. PIT
rtc_initialize(); // real-time clock
vfs_init(); // 12. Virtual File System
ata_initialize(); // 13. ATA disk driver
ext2_initialize(); // 14. EXT2 filesystem driver
vfs_mount("ext2", "/", "/dev/hda"); // 15. mount the root filesystem
fhs_initialize(); // 16. create the standard FHS directories
mem_devs_initialize(); // 17. memory devices
procfs_module_init(); // 18. procfs, then mounted at /proc
/* ...procv (/proc/video), procs, procipc... */
sem_init(); msq_init(); shm_init(); // 19. System V IPC
ps2_initialize(); keyboard_initialize(); // 20. PS/2 + keyboard
scheduler_initialize(); // 21. scheduler
init_tasking(); // 22. process management
/* ...optional scheduler feedback... */
// 23. First userspace process: /bin/runtests when GRUB passed the
// "runtests" command line, /bin/init otherwise.
process_create_init(runtests ? "/bin/runtests" : "/bin/init");
fpu_install(); // 24. FPU
signals_init(); // 25. signals
// 26. Switch to init's page directory and jump into user mode.
paging_switch_pgd(init_process->mm->pgd);
scheduler_enter_user_jmp(/* ... */);
}Common misreadings of this sequence:
-
"Step 9 turns paging on." No — paging was turned on by
boot/src/boot.cbeforekmain().paging_init()replaces the bootstrap page directory with the kernel's managed one. -
"The kernel ends in an idle loop." It does not fall through to
hlt(). It jumps into the init process and never returns; from then on the kernel only runs in response to interrupts, exceptions and system calls. -
"
/procis mounted by init." It is mounted by the kernel, in step 18, before any userspace process exists.
The x86 CPU has privilege levels (rings 0-3). MentOS uses:
- Ring 0 - Kernel mode (full access)
- Ring 3 - User mode (restricted)
Switching happens on syscalls, interrupts, and exceptions.
The timer interrupt preempts (forcibly pauses) the running program:
Program A running
↓
Timer interrupt fires
↓
Kernel scheduler picks Program B
↓
Program A is paused (saved state)
Program B resumes
This prevents any program from hogging the CPU.
Multiple parts of the kernel might access shared data:
// BAD: Race condition!
if (free_list_head != NULL) {
// ← Interrupt here could corrupt free_list!
node = free_list_head;
free_list_head = node->next;
}
// GOOD: Use spinlock
spinlock_lock(&free_list_lock);
if (free_list_head != NULL) {
node = free_list_head;
free_list_head = node->next;
}
spinlock_unlock(&free_list_lock);// User calls read():
read(fd, buf, 100);
// Libc wrapper:
_syscall3(read, int, fd, void *, buf, size_t, count) {
eax = SYSCALL_NUMBER_READ; // 3
ebx = fd;
ecx = buf;
edx = count;
INT 0x80; // ← Switch to kernel mode
return eax;
}
// Kernel (kernel/src/system/syscall.c):
void syscall_handler(pt_regs_t *f) {
if (f->eax >= SYSCALL_NUMBER) { // out of table range
f->eax = -ENOSYS;
} else {
// Dispatch through a table, not a switch. syscall_init() stored
// sys_read at sys_call_table[__NR_read].
SystemCall5 fun = (SystemCall5)sys_call_table[f->eax];
f->eax = fun(f->ebx, f->ecx, f->edx, f->esi, f->edi);
}
scheduler_run(f); // a syscall is a reschedule point
}fork, clone, execve and sigreturn are dispatched differently: instead of unpacking
registers, the handler passes the saved register frame pt_regs_t *f itself, because those
calls have to read and modify the frame the CPU will return through.
Start here:
-
kernel/src/kernel.c -
kmain()function (initialization) - kernel/src/process/scheduler.c - How processes are scheduled
- kernel/src/mem/paging.c - Virtual memory implementation
- kernel/src/system/syscall.c - System call dispatcher
- kernel/src/drivers/ - Device drivers (keyboard, disk, etc.)
Key directories:
kernel/
├── src/
│ ├── kernel.c # Main kernel initialization
│ ├── process/ # Process management and scheduling
│ │ ├── scheduler.c # CPU scheduling algorithms
│ │ ├── fork.c # Process creation
│ │ └── wait.c # Process synchronization
│ ├── mem/ # Memory management
│ │ ├── paging.c # Virtual memory (page tables)
│ │ ├── zone.c # Physical memory zones
│ │ └── slab.c # Object allocator
│ ├── fs/ # File systems
│ │ ├── vfs.c # Virtual File System
│ │ ├── ext2.c # EXT2 filesystem
│ │ └── namei.c # Path resolution
│ ├── drivers/ # Device drivers
│ │ ├── keyboard.c # Keyboard driver
│ │ ├── ata.c # Disk driver
│ │ └── video.c # VGA text mode
│ ├── system/ # System calls
│ │ ├── syscall.c # Syscall dispatcher
│ │ └── signal.c # Signal handling
│ └── descriptor_tables/ # CPU tables
│ ├── gdt.c # Global Descriptor Table
│ ├── idt.c # Interrupt Descriptor Table
│ └── isr.c # Interrupt Service RoutinesThe sections below are a per-subsystem reference; the narrative walkthrough above is the better starting point if you are reading this page for the first time.
Handlers are registered through the helpers declared in kernel/inc/descriptor_tables/isr.h:
// CPU exceptions and software interrupts (vector 0-31, plus 128 for int 0x80)
int isr_install_handler(unsigned i, interrupt_handler_t handler, char *description);
// Hardware IRQs (IRQ 0-15, mapped by the PIC onto vectors 32-47)
int irq_install_handler(unsigned i, interrupt_handler_t handler, char *description);For example, syscall_init() ends with:
isr_install_handler(SYSTEM_CALL, &syscall_handler, "syscall_handler");Located in kernel/src/drivers/
Implemented Drivers (kernel/src/drivers/):
-
PS/2 + Keyboard (
ps2.c,keyboard/) - PS/2 controller and keyboard input handling -
ATA (
ata.c) - IDE/ATA disk driver; this is what backs/dev/hda -
RTC (
rtc.c) - Real-time clock: system time and calendar -
Mouse (
mouse.c), FDC (fdc.c), memory devices (mem.c)
Related, but living elsewhere in the tree:
-
Video - VGA text mode console, in
kernel/src/io/video.c -
FPU (
kernel/src/devices/fpu.c) and PCI (kernel/src/devices/pci.c)
Driver interface: MentOS deliberately has no generic driver-model struct. Each driver
exposes its own *_initialize() entry point, which kmain() calls directly and which
registers an IRQ handler through irq_install_handler() when it needs one. Do not go looking
for a device_driver_t — there isn't one.
Located in kernel/inc/io/ and kernel/src/io/
Kernel Logging:
- 8 log levels (0=
EMERGto 7=DEBUG), plusLOGLEVEL_DEFAULT - Per-file log level control, by defining
__DEBUG_HEADER__and__DEBUG_LEVEL__before includingio/debug.h - Debug macros:
pr_emerg(),pr_crit(),pr_err(),pr_warning(),pr_notice(),pr_debug()
Available Log Levels:
// lib/inc/sys/kernel_levels.h
#define LOGLEVEL_DEFAULT (-1) // default-level messages
#define LOGLEVEL_EMERG 0 // system is unusable
#define LOGLEVEL_ALERT 1 // action must be taken immediately
#define LOGLEVEL_CRIT 2 // critical conditions
#define LOGLEVEL_ERR 3 // error conditions
#define LOGLEVEL_WARNING 4 // warning conditions
#define LOGLEVEL_NOTICE 5 // normal but significant condition
#define LOGLEVEL_INFO 6 // informational
#define LOGLEVEL_DEBUG 7 // debug-level messagesDebug Functions:
-
dbg_print_*()- Print various data structures -
panic()- Kernel panic with message
Located in kernel/inc/system/ and kernel/src/system/
syscall_init() in kernel/src/system/syscall.c currently registers 77 handlers, across
these categories (the authoritative list is that function itself):
- Process Management - fork, exec, exit, wait, getpid, setpgid, signals
- File Operations - open, close, read, write, lseek, stat, chmod
- Memory - brk, mmap, munmap
- IPC - semget, semop, msgget, msgsnd, msgrcv, shmget, shmat
- Filesystem - mkdir, rmdir, unlink, symlink, readlink
- Timing - time, sleep, timer operations
- User/Group - getuid, setuid, getgid, setgid
- Misc - ioctl, fcntl, uname, reboot
See System Calls page for complete reference.
See Kernel Initialization Sequence above for the real order taken from
kernel/src/kernel.c:kmain(). In outline:
1. External Multiboot loader (GRUB)
└─ Real mode → protected mode, load bootloader.bin, jump to boot_entry
2. MentOS bootstrap (boot/src/boot.S, boot/src/boot.c)
├─ Set up the 4 MiB bootstrap stack
├─ Parse the embedded kernel ELF
├─ Build bootstrap page tables and ENABLE PAGING
├─ Relocate the kernel's PT_LOAD segments
└─ Jump to kmain()
3. kmain() (kernel/src/kernel.c)
├─ Check the Multiboot magic (0x2BADB002)
├─ Physical memory manager, then slab allocator
├─ GDT, IDT, syscall table, PIC
├─ paging_init() — replace the bootstrap page directory
├─ Timer, RTC
├─ VFS, ATA, EXT2, mount / from /dev/hda, FHS, procfs at /proc
├─ System V IPC (sem, msq, shm)
├─ PS/2 and keyboard
├─ Scheduler and tasking
├─ Create /bin/init (or /bin/runtests in test mode)
└─ FPU, signals
4. Jump into the init process
└─ The kernel never returns; it runs only on interrupts, exceptions and syscalls
The scheduler performs context switching through:
- Store Context - Save current process registers to task_struct
- Select Next - Choose next process based on scheduling algorithm
- Restore Context - Load new process registers from its task_struct
- Switch CR3 - Load new page directory for memory mapping
void scheduler_store_context(pt_regs_t *f, task_struct *process) {
// Save CPU state from interrupt frame to task
process->thread.esp0 = f->esp;
process->thread.ss0 = f->ss;
// ... save other registers
}
void scheduler_run(pt_regs_t *f) {
// Store current process context
scheduler_store_context(f, current);
// Select next process
current = select_next_process();
// Switch memory context
paging_switch_page_directory(current->mm->pgdir);
// Restore registers from interrupt frame
f->esp = current->thread.esp0;
f->eip = current->thread.eip;
// ... restore other registers
}The kernel handles CPU exceptions like page faults, general protection faults, etc.
Not every page fault is an error. The x86 CPU pushes an error code whose low bits say what
happened; MentOS names them in kernel/src/mem/page_fault.c:
| Bit | Mask | Meaning when set |
|---|---|---|
| 0 | ERR_PRESENT |
The page was present → this is a protection violation, not a missing page |
| 1 | ERR_RW |
The access was a write |
| 2 | ERR_USER |
The access came from user mode (ring 3) |
| 3 | ERR_RESERVED |
A reserved bit was set in a paging structure |
| 4 | ERR_INST |
The fault happened on an instruction fetch |
Careful: bit 0 set means the page was present. The doc comment on
ERR_PRESENTinkernel/src/mem/page_fault.ccurrently reads "Page not present", which is the inverse of what the bit means; the code uses it correctly (if (!(err_code & ERR_PRESENT)) ... "Page not present"). Trust the code and the Intel manual, not that comment.
The handler's behaviour depends on who faulted and why:
// kernel/src/mem/page_fault.c (shape of the logic, simplified)
void page_fault_handler(pt_regs_t *f) {
uint32_t addr = get_cr2();
// 1. Copy-on-write: the page is present but marked kernel_cow, and this was
// a write. Allocate a private copy and let the instruction retry.
if (entry && entry->kernel_cow) {
__page_handle_cow(entry);
return;
}
// 2. A user-mode fault we cannot resolve: kill the offending process.
if (task && (f->err_code & ERR_USER)) {
sys_kill(task->pid, SIGSEGV);
scheduler_run(f);
return;
}
// 3. A kernel-mode fault we cannot resolve: there is nobody to blame and
// no safe way to continue.
__page_fault_panic(f, addr);
}So: user faults can end in SIGSEGV, resolvable COW faults are fixed up silently, and
kernel faults panic. It is not true that every page fault panics.
1. Process Creation (fork)
├─ Allocate new task_struct
├─ Clone parent's memory (mm_clone → vm_area_clone with cow = 0,
│ i.e. an EAGER copy in the current tree — see "Creating Processes" above)
├─ Copy file descriptors
├─ Copy signal handlers
└─ Add to scheduler runqueue
2. Process Execution (exec)
├─ Check the file is executable (ELF ET_EXEC, or a #! script)
├─ Destroy the old mm and build a blank address space
├─ Load the ELF PT_LOAD segments
└─ Jump to entry point
(NOTE: the old image is torn down before the new one is known to load
successfully. See [[Process Management]] — this differs from the usual
Unix guarantee that a failed execve() leaves the process untouched.)
3. Process Termination (exit)
├─ Close file descriptors
├─ Free memory
├─ Notify parent (SIGCHLD)
├─ Reparent children to init
└─ Remove from scheduler
- Architecture - Overall system structure
- System Calls - How userspace talks to kernel
- Scheduling - CPU scheduling algorithms explained
- File Systems - VFS and EXT2 details
- Userspace Programs - How programs use kernel services
- Debugging - How to debug kernel code
Key takeaway: The kernel is not magic - it's code that manages hardware and provides services. Start with one subsystem (e.g., scheduler) and trace through how it works!