Skip to content
Galfurian edited this page Aug 19, 2026 · 9 revisions

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.

What is a Kernel?

The kernel is the core program that:

  1. Controls the hardware - CPU, RAM, disk, keyboard, screen
  2. Manages resources - Decides which program gets CPU time, memory, disk space
  3. Provides services - File reading, process creation, network communication
  4. 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.

The Big Picture

┌──────────────────────────────────────────────────────┐
│               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.

1. Process Management - "Running Multiple Programs"

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.

How It Works

Timeline (on every timer interrupt):
───────────────────────────────────────────────────>
 Program A  │ Program B  │ Program A  │ Program C
 running    │ running    │ running    │ running

The scheduler decides who runs next:

  1. Timer interrupt fires
  2. Save current program's state (registers, stack pointer)
  3. Pick next program to run (based on priority/fairness)
  4. Load new program's state
  5. 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.

Key Data Structure: task_struct

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!

Scheduling Algorithms

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.

Creating Processes: fork()

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().

2. Memory Management - "Giving Each Program Its Own Space"

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

How Paging Works

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.

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 0x0D1 as the page-table index; that value does not follow from 0x12345678. The correct index is 0x345.)
  • The frame number 0x00400 is not derived from the virtual address. It is whatever the operating system stored in that page table entry; here we simply assume the value 0x00400 so 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
};

Memory Allocators

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

3. File System - "Organizing Data on Disk"

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.).

VFS Architecture

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

Key Data Structures

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.

4. Interrupt Handling - "Responding to Events"

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)

Types of Interrupts

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

How Interrupts Work

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

Interrupt Descriptor Table (IDT)

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
}

5. Device Drivers - "Talking to Hardware"

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.

Example: Keyboard Driver

// 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);
}

Example: Disk Driver (ATA)

// 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)
}

Kernel Initialization Sequence

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.c before kmain(). 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.
  • "/proc is mounted by init." It is mounted by the kernel, in step 18, before any userspace process exists.

Key Kernel Concepts

1. Kernel vs User Mode

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.

2. Preemptive Multitasking

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.

3. Synchronization

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);

4. System Call Mechanism

// 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.

Exploring the Kernel Code

Start here:

  1. kernel/src/kernel.c - kmain() function (initialization)
  2. kernel/src/process/scheduler.c - How processes are scheduled
  3. kernel/src/mem/paging.c - Virtual memory implementation
  4. kernel/src/system/syscall.c - System call dispatcher
  5. 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 Routines

Kernel Subsystem Reference

The 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.

Interrupt Service Routines

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");

Device Drivers

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.

I/O and Debugging

Located in kernel/inc/io/ and kernel/src/io/

Kernel Logging:

  • 8 log levels (0=EMERG to 7=DEBUG), plus LOGLEVEL_DEFAULT
  • Per-file log level control, by defining __DEBUG_HEADER__ and __DEBUG_LEVEL__ before including io/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 messages

Debug Functions:

  • dbg_print_*() - Print various data structures
  • panic() - Kernel panic with message

System Calls

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.

Initialization Sequence

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

Context Switching

The scheduler performs context switching through:

  1. Store Context - Save current process registers to task_struct
  2. Select Next - Choose next process based on scheduling algorithm
  3. Restore Context - Load new process registers from its task_struct
  4. 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
}

Exception Handling

The kernel handles CPU exceptions like page faults, general protection faults, etc.

Example: Page Fault Handler

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_PRESENT in kernel/src/mem/page_fault.c currently 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.

Process Lifecycle

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

Further Reading


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!

Clone this wiki locally