任务调度 - 理论讲解

1. 调度器概述

调度器决定哪个进程在什么时候获得CPU。

调度类型

类型说明切换时机
协作式进程主动让出CPU进程调用yield()
抢占式定时器中断强制切换时间片用完

我们实现抢占式调度——更可靠,不怕进程不配合。

2. 可编程间隔定时器(PIT)

8253/8254 PIT

PIT产生周期性的IRQ0中断,是抢占式调度的时钟源。

基础频率: 1,193,182 Hz
端口:     0x40 (通道0数据)
          0x43 (命令端口)

配置PIT频率

void timer_init(uint32_t frequency) {
    uint16_t divisor = 1193182 / frequency;
 
    // 命令字节: 通道0, lobyte/hibyte, 方波模式
    outb(0x43, 0x36);
 
    // 写入分频值(先低后高)
    outb(0x40, divisor & 0xFF);
    outb(0x40, (divisor >> 8) & 0xFF);
}
 
// timer_init(100) → 每秒100次中断 (10ms间隔)

3. 上下文切换

什么是上下文?

进程的”上下文”是它在CPU上运行所需的全部状态:

  • 通用寄存器 (EAX-EDI)
  • 栈指针 (ESP)、基址指针 (EBP)
  • 指令指针 (EIP)
  • 标志寄存器 (EFLAGS)
  • 页目录 (CR3)

汇编实现

; switch_context(prev, next)
; 参数在栈上: [esp+4]=prev, [esp+8]=next
global switch_context
switch_context:
    ; 保存被调用者保存寄存器
    push ebp
    push ebx
    push esi
    push edi
 
    ; 保存当前ESP到prev->esp
    mov eax, [esp + 20]    ; prev指针 (4 pushes + return addr = 20)
    mov [eax + 48], esp    ; 偏移48 = process_t.esp
 
    ; 切换到next的ESP
    mov eax, [esp + 24]    ; next指针
    mov esp, [eax + 48]    ; 恢复next的ESP
 
    ; 恢复寄存器
    pop edi
    pop esi
    pop ebx
    pop ebp
 
    ret    ; 返回到next进程的EIP

上下文切换时栈的变化

切换前 (进程A的栈):        切换后 (进程B的栈):
┌──────────┐               ┌──────────┐
│ ...      │               │ ...      │
│ return   │               │ return   │
│ EBP      │               │ EBP      │
│ EBX      │               │ EBX      │
│ ESI      │               │ ESI      │
│ EDI      │ ← ESP(saved)  │ EDI      │ ← ESP(restored)
└──────────┘               └──────────┘

4. Round-Robin 调度算法

原理

所有就绪进程排成一个环形队列,每个进程获得固定的时间片。时间片用完后切换到下一个进程。

时间片 = 10个tick (100ms @ 100Hz)

→ [A] → [B] → [C] → [A] → [B] → [C] → ...
  10ms   10ms  10ms

实现

void schedule(void) {
    process_t *prev = current;
 
    // 找下一个就绪进程
    process_t *next = prev->next;
    while (next != prev) {
        if (next == NULL) next = proc_list;  // 回到链表头
        if (next->state == PROCESS_READY) break;
        next = next->next;
    }
 
    if (next == prev) return;  // 没有其他就绪进程
 
    // 状态转换
    if (prev->state == PROCESS_RUNNING) {
        prev->state = PROCESS_READY;
    }
    next->state = PROCESS_RUNNING;
    current = next;
 
    // 上下文切换
    switch_context(prev, next);
}

5. 定时器中断中的调度

void timer_handler(registers_t *regs) {
    tick++;
 
    // 减少当前进程的时间片
    if (current->time_slice > 0) {
        current->time_slice--;
    }
 
    // 时间片用完,触发调度
    if (current->time_slice == 0) {
        current->time_slice = DEFAULT_TIME_SLICE;
        schedule();
    }
}

6. 临界区保护

调度器本身不能被中断打断:

void schedule(void) {
    __asm__ volatile("cli");  // 关中断
 
    // ... 调度逻辑 ...
    // ... 上下文切换 ...
 
    __asm__ volatile("sti");  // 开中断
}

下一步

多任务操作系统基本成型了!最后一章将实现用户态程序——让用户代码在Ring 3中运行,通过系统调用访问内核服务!