任务调度 - 理论讲解
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中运行,通过系统调用访问内核服务!