Linux 里的 sleep(2) 背后是什么?

进程调用 sleep,内核把它标记为"阻塞",让出 CPU 给其他进程,等时间到了再唤醒它。实现这个功能需要两个组件配合:定时器(知道时间到了)+ 调度器(切换进程)。

好消息是这两个我们都有了:PIT 100Hz 时钟 + round-robin 调度器。这一章只需要把它们接起来。

核心思路

SYS_SLEEP(ms)
  → sleep_until = pit_get_ticks() + ms/10
  → state = PROC_BLOCKED

IRQ0 handler(每 10ms)
  → pit_tick()      // ticks++
  → timer_tick()    // 扫描所有进程,到期则唤醒
  → sched_tick()    // 调度下一个 READY 进程

进程睡觉时设好"闹钟"(sleep_until),然后自己进入 PROC_BLOCKED。 每次时钟中断,timer_tick 扫一遍所有进程,到期的改回 PROC_READY,下次调度就能运行了。

实现

1. process_t 加一个字段

typedef struct {
    // ... 原有字段
    uint64_t sleep_until;   // 睡到哪个 tick,0 表示没在睡
} process_t;

2. timer.c

void timer_tick(void) {
    uint64_t now = pit_get_ticks();
    for (int i = 0; i < MAX_PROCS; i++) {
        if (procs[i].state == PROC_BLOCKED && procs[i].sleep_until > 0) {
            if (now >= procs[i].sleep_until) {
                procs[i].sleep_until = 0;
                procs[i].state = PROC_READY;
            }
        }
    }
}

逻辑极简:遍历,到期,唤醒。

3. isr.c 接入

if (irq == 0) {
    pit_tick();
    timer_tick();   // ← 新增
    sched_tick(regs);
}

顺序很重要:先更新 ticks,再检查超时,最后做调度。

4. SYS_SLEEP 实现

case SYS_SLEEP: {
    uint64_t ms = a1;
    uint64_t ticks = (ms * 100) / 1000;  // 100Hz,每 tick 10ms
    if (ticks == 0) ticks = 1;
    current->sleep_until = pit_get_ticks() + ticks;
    current->state = PROC_BLOCKED;
    return 0;
}

syscall 返回后,sched_tick 会发现当前进程是 PROC_BLOCKED(不是 PROC_RUNNING), 直接跳过,切到其他进程。睡眠就这样发生了。

测试

用户程序很简单:

mov rax, SYS_WRITE
lea rdi, [rel msg_before]   ; "going to sleep for 2s..."
syscall

mov rax, SYS_SLEEP
mov rdi, 2000               ; 2000ms
syscall

mov rax, SYS_WRITE
lea rdi, [rel msg_after]    ; "woke up!"
syscall

运行结果:

sleep test: going to sleep for 2s...
sleep test: woke up!

两条消息之间有明显的 ~2 秒延迟。

几个细节

sleep_until = 0 的特殊语义

用 0 表示"没在睡",避免进程刚创建(sleep_until 默认 0)时被 timer_tick 误唤醒。

与 proc_wait 共用 PROC_BLOCKED

proc_wait 等子进程时也用 PROC_BLOCKED,靠子进程退出时主动唤醒父进程。 两者区分靠 sleep_until

  • sleep_until > 0 → 定时睡眠,timer_tick 唤醒
  • sleep_until == 0 → 等待事件(wait),事件发生时手动唤醒

精度

PIT 100Hz,最小粒度 10ms。sleep(1ms) 实际最多等 10ms。 如果需要更高精度,可以把 PIT 频率调高,或改用 HPET/APIC timer,这里就不搞了。

小结

sleep 的本质是:把自己标记为不可调度,等到某个条件满足再变回可调度。 这里条件是"时间到了",其他情况(等 IO、等信号量)机制完全一样。

至此我们的 OS 有了:进程调度、系统调用、TTY、文件系统、管道、信号、poll、字符设备、定时睡眠。 下一章继续往前走。