Skip to content

Process Management

NtinosTheGamer2324 edited this page Dec 11, 2025 · 2 revisions

Process Management

ModuOS implements preemptive multitasking with a round-robin scheduler, allowing multiple processes to run concurrently.

Overview

┌──────────────────────────────────────────────────────┐
│              Process Management Layer                │
│                                                      │
│  ┌────────────────┐      ┌─────────────────────┐   │
│  │ Process Table  │◄────►│   Scheduler         │   │
│  │ (256 max)      │      │   (Round-Robin)     │   │
│  └────────┬───────┘      └──────────┬──────────┘   │
│           │                          │              │
│  ┌────────▼──────────────────────────▼──────────┐  │
│  │         Context Switcher (ASM)               │  │
│  │         - Save CPU state                     │  │
│  │         - Switch stacks                      │  │
│  │         - Restore CPU state                  │  │
│  └──────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────┘

Process Structure

File: include/moduos/kernel/process/process.h

typedef struct process {
    uint32_t pid;                        // Process ID
    uint32_t parent_pid;                 // Parent process ID
    char name[PROCESS_NAME_MAX];         // Process name (64 chars)
    
    process_state_t state;               // Current state
    int exit_code;                       // Exit status
    
    cpu_state_t cpu_state;               // Saved CPU registers
    
    uint64_t page_table;                 // Page table (PML4)
    void *kernel_stack;                  // Kernel stack (8KB)
    void *user_stack;                    // User stack (64KB)
    
    void *fd_table[MAX_OPEN_FILES];      // File descriptors (16 max)
    
    uint64_t time_slice;                 // Remaining time slice
    uint64_t total_time;                 // Total CPU time
    
    int priority;                        // Priority (0=highest)
    
    int argc;                            // Argument count
    char **argv;                         // Argument vector
    
    struct process *next;                // Linked list pointer
} process_t;

Process States

typedef enum {
    PROCESS_STATE_READY = 0,      // Ready to run
    PROCESS_STATE_RUNNING,        // Currently executing
    PROCESS_STATE_BLOCKED,        // Waiting for I/O
    PROCESS_STATE_SLEEPING,       // Sleeping (timer)
    PROCESS_STATE_ZOMBIE,         // Terminated, waiting for parent
    PROCESS_STATE_TERMINATED      // Fully terminated
} process_state_t;

State Transitions:

        ┌──────────────────────┐
        │      READY           │
        └──┬────────────────▲──┘
           │                │
    schedule()         yield()
           │                │
        ┌──▼────────────────┴──┐
        │      RUNNING         │
        └──┬────────┬──────────┘
           │        │
      exit()    sleep()/block()
           │        │
        ┌──▼──┐  ┌──▼─────┐
        │ZOMBIE│  │BLOCKED │
        └─────┘  │SLEEPING│
                 └──┬─────┘
                    │
                wake_up()
                    │
                 ┌──▼──┐
                 │READY│
                 └─────┘

CPU State Structure

File: include/moduos/kernel/process/process.h

typedef struct {
    uint64_t r15;      // +0   - Callee-saved
    uint64_t r14;      // +8   - Callee-saved
    uint64_t r13;      // +16  - Callee-saved
    uint64_t r12;      // +24  - Callee-saved
    uint64_t rbx;      // +32  - Callee-saved
    uint64_t rbp;      // +40  - Frame pointer
    uint64_t rip;      // +48  - Instruction pointer
    uint64_t rsp;      // +56  - Stack pointer
    uint64_t rflags;   // +64  - CPU flags (includes interrupt enable)
} cpu_state_t;

Why only these registers?

  • ModuOS follows System V ABI calling convention
  • Callee-saved registers (r15, r14, r13, r12, rbx, rbp) must be preserved across function calls
  • Caller-saved registers (rax, rcx, rdx, rsi, rdi, r8-r11) are automatically saved by the compiler
  • rflags is critical for preserving interrupt enable flag

Process Management API

Process Initialization

void process_init(void);

Steps:

  1. Initialize process table
  2. Set all slots to empty
  3. Create idle process (PID 0)
  4. Set current process to idle

Process Creation

process_t* process_create(const char *name, 
                          void (*entry_point)(void), 
                          int priority);

process_t* process_create_with_args(const char *name, 
                                    void (*entry_point)(void), 
                                    int priority,
                                    int argc, 
                                    char **argv);

Steps:

  1. Find free PID in process table
  2. Allocate process structure
  3. Allocate kernel stack (8KB)
  4. Allocate user stack (64KB)
  5. Initialize CPU state with entry point
  6. Set up initial stack frame
  7. Add to scheduler ready queue
  8. Return process pointer

Stack Setup:

// Set up initial stack frame
uint64_t *stack = (uint64_t*)((uint8_t*)proc->kernel_stack + KERNEL_STACK_SIZE);
stack--;  // Stack grows downward

// Push return address (entry point)
proc->cpu_state.rip = (uint64_t)entry_point;
proc->cpu_state.rsp = (uint64_t)stack;
proc->cpu_state.rbp = (uint64_t)stack;
proc->cpu_state.rflags = 0x202;  // IF=1 (interrupts enabled)

Process Termination

void process_exit(int exit_code);
void process_kill(uint32_t pid);

Exit Flow:

  1. Set process state to ZOMBIE
  2. Store exit code
  3. Close all open file descriptors
  4. Free user stack (keep kernel stack for cleanup)
  5. Call scheduler to switch to next process
  6. Parent reaps zombie and frees remaining resources

Process Queries

process_t* process_get_current(void);      // Get current running process
process_t* process_get_by_pid(uint32_t pid);  // Find process by PID

Process Control

void process_yield(void);                    // Voluntarily yield CPU
void process_sleep(uint64_t milliseconds);   // Sleep for specified time
void process_wake(uint32_t pid);             // Wake sleeping process

Scheduler

File: src/kernel/process/process.c

Scheduler Initialization

void scheduler_init(void);

Steps:

  1. Initialize ready queue (linked list)
  2. Set up timer interrupt (10ms time slice)
  3. Enable preemption

Scheduling Algorithm

Round-Robin with Priority:

void schedule(void) {
    process_t *current = current_process;
    process_t *next = NULL;
    
    // Find next ready process
    for (int priority = 0; priority < MAX_PRIORITY; priority++) {
        for (process_t *p = ready_queue; p != NULL; p = p->next) {
            if (p->state == PROCESS_STATE_READY && p->priority == priority) {
                next = p;
                goto found;
            }
        }
    }
    
found:
    if (next == NULL) {
        next = idle_process;  // No process ready, run idle
    }
    
    if (next == current) {
        return;  // Continue running current process
    }
    
    // Switch processes
    if (current) {
        current->state = PROCESS_STATE_READY;
    }
    next->state = PROCESS_STATE_RUNNING;
    current_process = next;
    
    context_switch(&current->cpu_state, &next->cpu_state);
}

Features:

  • Priority-based: Lower priority number = higher priority
  • Round-robin: Equal priority processes share CPU time
  • Preemptive: Timer interrupt forces context switch
  • Fair: All processes eventually get CPU time

Time Slice Management

void scheduler_tick(void) {
    process_t *current = current_process;
    
    if (current) {
        current->time_slice--;
        current->total_time++;
        
        if (current->time_slice == 0) {
            // Time slice expired, reschedule
            current->time_slice = TIME_SLICE_DEFAULT;
            schedule();
        }
    }
}

Time Slice: Default 10ms (adjustable)

Process Queue Management

void scheduler_add_process(process_t *proc);
void scheduler_remove_process(process_t *proc);

Context Switching

File: src/arch/AMD64/syscall/context_switch.asm

Context Switch Assembly

global context_switch
; void context_switch(cpu_state_t *old_state, cpu_state_t *new_state)
; RDI = old_state, RSI = new_state

context_switch:
    ; Check if old_state is NULL
    test rdi, rdi
    jz .load_new
    
    ; Save current process state
    mov [rdi + 0],  r15       ; r15
    mov [rdi + 8],  r14       ; r14
    mov [rdi + 16], r13       ; r13
    mov [rdi + 24], r12       ; r12
    mov [rdi + 32], rbx       ; rbx
    mov [rdi + 40], rbp       ; rbp
    
    ; Save return address (RIP)
    lea rax, [.return]
    mov [rdi + 48], rax       ; rip
    
    ; Save stack pointer
    mov [rdi + 56], rsp       ; rsp
    
    ; Save RFLAGS
    pushfq
    pop rax
    mov [rdi + 64], rax       ; rflags

.load_new:
    ; Restore new process state
    mov r15, [rsi + 0]        ; r15
    mov r14, [rsi + 8]        ; r14
    mov r13, [rsi + 16]       ; r13
    mov r12, [rsi + 24]       ; r12
    mov rbx, [rsi + 32]       ; rbx
    mov rbp, [rsi + 40]       ; rbp
    mov rsp, [rsi + 56]       ; rsp
    
    ; Restore RFLAGS
    mov rax, [rsi + 64]
    push rax
    popfq
    
    ; Jump to new process (restore RIP)
    jmp [rsi + 48]

.return:
    ret

How it Works:

  1. Save: Store all callee-saved registers to old process structure
  2. Switch Stack: Load new process's stack pointer
  3. Restore: Load all registers from new process structure
  4. Jump: Transfer control to new process

Key Points:

  • Atomic: Interrupts should be disabled during switch
  • Stack Switch: RSP changes from old to new stack
  • Preserves RFLAGS: Maintains interrupt enable flag

ELF Executable Loading

File: src/kernel/loader/elf.c

ELF Format Support

ModuOS can load ELF64 (Executable and Linkable Format) binaries.

int elf_load(const char *path, void **entry_point);

Steps:

  1. Read ELF file from filesystem
  2. Verify ELF magic number (0x7F 'E' 'L' 'F')
  3. Parse ELF header and program headers
  4. Allocate memory for segments
  5. Load segments into memory
  6. Set up initial stack
  7. Return entry point address

ELF Header (simplified):

typedef struct {
    uint8_t  e_ident[16];    // Magic number and class
    uint16_t e_type;         // Object file type (EXEC)
    uint16_t e_machine;      // Machine type (x86-64)
    uint32_t e_version;      // ELF version
    uint64_t e_entry;        // Entry point address
    uint64_t e_phoff;        // Program header offset
    uint64_t e_shoff;        // Section header offset
    // ...
} Elf64_Ehdr;

Program Execution

void exec_program(const char *path, int argc, char **argv) {
    void *entry_point;
    
    // Load ELF executable
    if (elf_load(path, &entry_point) != 0) {
        return;  // Load failed
    }
    
    // Create new process
    process_t *proc = process_create_with_args(path, entry_point, 0, argc, argv);
    
    // Add to scheduler
    scheduler_add_process(proc);
}

Kernel vs. User Mode

Privilege Levels

Ring 0 (Kernel):
- Full hardware access
- All instructions available
- Can modify CR3, IDT, GDT
- Runs kernel code and drivers

Ring 3 (User):
- Limited hardware access
- Restricted instructions
- Cannot modify system tables
- Runs user applications

Mode Switching

User → Kernel (System Call):

User code → INT 0x80 → Syscall handler (Ring 0) → 
Process request → Return to user (Ring 3)

Kernel → User (Process Start):

Kernel creates process → Sets up stack → 
IRET instruction → CPU switches to Ring 3

Process Synchronization

Current Implementation

ModuOS currently has basic synchronization:

  • Process blocking on I/O
  • Sleep/wake mechanism
  • Yield for voluntary context switch

Future: Mutexes and Semaphores

// Future API
typedef struct {
    int locked;
    process_t *owner;
    process_t *wait_queue;
} mutex_t;

void mutex_lock(mutex_t *mutex);
void mutex_unlock(mutex_t *mutex);

Process Memory Layout

User Stack (grows down)
0x00007FFFFFFFFFFF
        |
        v
  [ User Stack ]
  [ 64KB ]

        ...

  [ Code Segment ]
  [ Data Segment ]
  [ BSS Segment ]

        ^
        |
Heap (grows up)
0x0000000000400000

────────────────────────────
Kernel Space
0xFFFFFFFF80000000

  [ Kernel Stack ]
  [ 8KB per process ]

  [ Kernel Code/Data ]

Idle Process

Special process (PID 0) that runs when no other process is ready:

void idle_process_entry(void) {
    while (1) {
        __asm__ volatile("hlt");  // Halt CPU until interrupt
    }
}

Purpose:

  • Saves power when system is idle
  • Always ready to run
  • Lowest priority

Debugging Processes

Process Listing

void process_list(void) {
    for (int i = 0; i < MAX_PROCESSES; i++) {
        process_t *p = &process_table[i];
        if (p->state != PROCESS_STATE_TERMINATED) {
            printf("PID: %d, Name: %s, State: %d\n", 
                   p->pid, p->name, p->state);
        }
    }
}

Stack Traces

void print_stack_trace(process_t *proc) {
    uint64_t *rbp = (uint64_t*)proc->cpu_state.rbp;
    
    for (int i = 0; i < 10 && rbp; i++) {
        uint64_t rip = *(rbp + 1);
        printf("  [%d] 0x%llx\n", i, rip);
        rbp = (uint64_t*)*rbp;
    }
}

Performance Considerations

Context Switch Overhead

Typical overhead: ~1-5 microseconds

  • Save registers: ~10 instructions
  • Switch stack: 1 instruction
  • Restore registers: ~10 instructions
  • Cache pollution: varies

Optimization Tips

  1. Minimize Time Slice: Shorter = more responsive, but more overhead
  2. Priority Tuning: Give interactive tasks higher priority
  3. Reduce Context Switches: Batch operations when possible

Next Steps

Clone this wiki locally