tianole/docs/agents/tasks/04-time-scheduler.md
2026-05-12 17:26:39 +08:00

198 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 04 时钟、内核线程与调度
## 目标
建立 kernel 内部并发执行能力,为驱动等待、异步 I/O、进程和用户态做准备。
## 前置条件
- 中断入口可用。
- 内存分配可用。
- 至少一个 timer 可接入。
## 建议边界
- `kernel/sched/core.c`run queue、调度选择、tick 和 IRQ exit 调度边界。
- `kernel/sched/thread.c`:线程对象创建、初始栈、退出和 DEAD 线程回收。
- `kernel/sched/wait.c`:等待队列。
- `kernel/sched/idle.c`idle thread。
- `kernel/sched/sched.h`:调度子系统私有接口,不向通用内核层公开内部状态。
- `kernel/selftest/sched.c`:当前阶段的调度自测和启动演示线程。
- `kernel/time/`:通用时间与 timer 抽象。
- `arch/x86/`:具体 timer、上下文切换。
## 实现内容
### 1. Timer 与时钟基础
- 接入一个可替换的硬件 timer 后端,当前可以先用 PIT periodic timer。
- 通用层只暴露 `timer_tick()`、`timer_ticks()` 和后续 timeout/timer callback 所需接口。
- 调度器不能直接读取 PIT/APIC 硬件细节,只能依赖通用 time/timer 层。
- tick handler 中只做短路径工作:更新时间、唤醒到期 sleep、标记 reschedule不在 IRQ handler 内做复杂策略。
- 后续预留 one-shot timer、高精度 timer 和 timer wheel/min-heap 等实现空间。
### 2. Kernel thread 与上下文切换
- 建立 kernel thread 对象,包含 id、状态、入口、参数、内核栈、调度链表节点和等待链表节点。
- 建立新线程 trampoline让线程从独立内核栈进入入口函数。
- 建立 arch 私有上下文切换入口,只保存/恢复该 ABI 需要的调度上下文。
- 线程入口返回不能落入未知地址,必须统一进入 thread exit 路径。
- 线程对象和内核栈不能依赖“永不释放”,必须有明确生命周期和回收规则。
### 3. Run queue 与调度核心
- 建立动态 run queue线程数量不能固定写死。
- 先实现单 CPU round-robin再为优先级、时间片、per-cpu run queue 留边界。
- 调度核心需要集中处理 `RUNNING`、`READY`、`SLEEPING`、`WAITING`、`DEAD` 等状态转换。
- 调度选择、入队、出队和当前线程切换必须有统一锁规则。
- idle thread 必须作为没有 runnable thread 时的兜底执行体。
### 4. Interrupt-exit reschedule
- timer IRQ 只设置 `need_resched`,真正切换应收敛在明确的 IRQ 返回边界。
- `sched_irq_exit()` 要逐步演进为 trap-frame aware 模型,明确从中断返回时哪些现场可以被切换。
- 不要长期把普通协作式 `sched_yield()` 栈切换入口当成完整抢占式切换模型。
- 后续进入用户态后,用户态返回边界、内核可抢占点和 syscall 返回边界要能共享这套设计。
- 任意 IRQ handler 都不应该直接调用会睡眠的接口。
### 5. Sleep、wait queue 与 lost wakeup
- `sched_sleep()` 负责按 tick 睡眠并由 timer 唤醒。
- wait queue 要提供基础 sleep/wakeup、条件等待和带 timeout 的条件等待。
- 条件检查、入队睡眠和 wakeup 必须有明确锁规则,避免 lost wakeup。
- wakeup 必须发生在修改条件之后;等待方必须在同一同步规则下检查条件并决定是否睡眠。
- wait queue API 要区分可睡眠路径和不可睡眠路径,不能允许 IRQ handler 进入睡眠。
- timeout wait 被 timer 唤醒后,必须能从 wait queue 清理残留节点。
### 6. Locking 与不可睡眠上下文
- 建立单 CPU interrupt-safe lock保存中断状态、关闭中断、释放时恢复原状态。
- 明确哪些锁可以在 IRQ 入口使用,哪些路径允许睡眠,哪些路径必须原子完成。
- run queue、wait queue、timer sleep list、thread id 分配等共享状态必须逐步纳入锁保护。
- 禁止持有不可睡眠锁时调用 `sched_yield()`、`sched_sleep()` 或 wait queue sleep。
- 后续 SMP 前,需要先把接口命名和调用约束整理清楚,避免把单 CPU 假设泄漏到所有调用方。
### 7. Thread lifecycle 与回收
- 线程入口返回后进入统一退出路径,而不是直接释放自身栈。
- 区分当前线程不能立即释放自身内核栈的问题,调度器只能在安全边界回收非当前 DEAD 线程。
- 为未来 `join/wait`、进程退出、引用计数和资源释放预留 `ZOMBIE` 或等价状态。
- 线程释放必须覆盖线程对象、内核栈、等待队列残留、run queue 残留和调试信息。
- 回收路径需要自测,避免重复释放、释放当前线程栈或释放仍在队列中的线程。
### 8. Deferred work、completion 与后续驱动基础
- 为 workqueue/deferred work 预留接口位置,让 IRQ handler 可以把复杂工作推迟到线程上下文。
- 为 completion 或一次性事件等待预留基础,后续驱动初始化和 I/O 完成会使用。
- 为 timer callback 或 delayed work 预留设计,避免所有超时逻辑都手写在调度器里。
- 这些能力可以晚于基础 wait queue 实现,但任务文档必须提前记录边界,避免后续驱动直接依赖临时接口。
## Linux 参考原则
本阶段不照搬 Linux CFS、SMP、RCU、hrtimer 等完整复杂度,但接口边界应参考 Linux 的长期分层方式:
- `kernel/sched/` 风格:调度策略、线程状态和 run queue 维护集中在调度子系统,不散落到驱动或 `kernel/main.c`
- `wait_event*` 风格:等待方围绕“条件检查 + 入队睡眠 + 被唤醒后重新检查条件”组织逻辑,条件本身由调用方拥有。
- `wake_up*` 风格:唤醒方先改变受保护条件,再唤醒等待队列;否则容易出现 lost wakeup。
- `spin_lock_irqsave()` 风格:在中断和普通内核路径共享的数据结构上,先用保存/关闭中断的单 CPU lock 建立正确语义。
- `kthread` 风格:线程函数返回、显式退出、停止请求和资源回收要有统一生命周期,不依赖“这个线程永远不结束”。
- `timer/workqueue` 风格IRQ handler 做最少工作,复杂处理推迟到线程上下文或 deferred work。
## 非玩具化约束
- 调度器不能直接绑定某一种硬件 timer。
- 线程数量不能固定写死。
- 等待队列要能被驱动、文件系统和进程等待复用。
- 日志只能辅助观察,不能成为调度逻辑的一部分。
- 内核对象不能依赖“永不释放”的假设。
- 任何可睡眠路径和不可睡眠路径要从接口上区分。
- 任何会修改 run queue、wait queue、sleep list 或 thread state 的路径都要说明锁规则。
- wait queue 条件等待必须能解释为什么不会丢唤醒。
- IRQ handler 不能直接执行可能睡眠或长时间运行的工作。
- 线程状态转换必须单向、可审计,不能通过随意改 `state` 修补行为。
- 当前阶段可以只支持单 CPU但接口不能把“永远单 CPU”写死成公共契约。
- arch 层只负责 trap、IRQ、timer 硬件和上下文切换细节,不承载通用调度策略。
## 验收方式
- 两个以上 kernel thread 能轮转。
- 线程可以 sleep 并被 timer 唤醒。
- 调度现场保存和恢复稳定。
- 基础对象生命周期规则有文档和调用约束。
- wait queue 能完成普通 wakeup、条件等待和 timeout 等待。
- timeout 路径不会把线程永久留在 wait queue 中。
- timer IRQ 触发后,调度请求在明确的 IRQ exit 边界被消费。
- 线程入口正常返回后进入统一退出和回收路径。
- `scripts/check.sh` 覆盖 timer、thread、sleep、wait queue、timeout wait 和退出回收的可观测日志。
- 新增调度 API 必须同时更新公开头文件、自测和任务文档。
## 当前状态
基础完成:
- 已建立 x86 PIC remap外部 IRQ 使用 vector 32-47不再和 CPU exception 混用。
- 已接入 PIT periodic timer当前频率为 100Hz。
- 已建立通用 `timer_tick()` 入口和 `timer_ticks()` 计数接口。
- 已在 trap dispatch 中区分 CPU exception 和外部 IRQ。
- 已建立 IRQ handler 注册表timer IRQ0 通过 `irq_register()` 接入分发路径。
- 已对已处理 IRQ 发送 EOI避免中断只触发一次。
- 已建立最小 kernel thread 对象,包含 id、状态、入口、参数、内核栈和 run queue 链接。
- 已建立动态 run queue不固定写死线程数量。
- 已能通过 `kernel_thread_create()` 动态分配线程对象和内核栈。
- 已建立 x86 上下文切换入口,保存/恢复 callee-saved 寄存器和栈指针。
- 已建立线程 trampoline新线程能从独立内核栈进入自己的入口函数。
- 已建立协作式 round-robin两个 kernel thread 能通过 `sched_yield()` 轮转运行。
- 已把调度接入 `timer_tick()`timer tick 会唤醒到期 sleep 线程并标记 `need_resched`
- 已建立 idle thread所有普通线程 sleep/wait 时由 idle 承接 CPU。
- 已提供 `sched_sleep()`,线程可以睡眠指定 tick 数并被 timer 唤醒。
- 已提供 `wait_queue_init()`、`wait_queue_sleep()`、`wait_queue_wake_one()` 和 `wait_queue_wake_all()`
- 已提供 `wait_queue_wait()``wait_queue_wait_timeout()`,调用方可以等待条件成立,并在超时路径上获得失败返回值。
- wait queue 已有内部 interrupt-safe lock条件检查、等待入队和 wakeup 队列修改已收敛到同一同步边界,降低 lost wakeup 风险。
- 已建立单 CPU interrupt-safe lock 基础,当前 `spin_lock_irqsave()` 会保存并关闭中断,`spin_unlock_irqrestore()` 会恢复原中断状态。
- 已把 `kernel_thread_create()` 中的线程 id 分配和 run queue 入队纳入 interrupt-safe lock 保护。
- 已建立 `sched_irq_exit()`timer IRQ 只设置 `need_resched`trap 的 IRQ 返回边界统一消费调度请求。
- 已建立最小 DEAD 线程回收路径,调度前会释放非当前 DEAD 线程的内核栈和线程对象。
- 已在调度私有头中加入 thread state helper调度核心、线程退出和 wait queue 路径不再直接散写主要状态转换。
- 已把调度代码按职责拆分为 `core.c`、`thread.c`、`wait.c`、`idle.c` 和私有 `sched.h`,并把当前阶段自测/演示线程移到 `kernel/selftest/sched.c`
- `scripts/check.sh` 已验证 `timer initialized`、`timer tick=1/2/3`、`scheduler initialized`、`kernel thread selftest ok`、timer 驱动线程轮转、`sched_sleep()`、wait queue wakeup、条件等待和超时等待。
后续扩展:
### A. 调度状态机收紧
- 明确合法状态转换,例如 `READY -> RUNNING -> READY`、`RUNNING -> SLEEPING/WAITING`、`SLEEPING/WAITING -> READY`、`RUNNING -> DEAD`。
- 继续扩大状态转换辅助函数覆盖范围,减少外部代码直接写 `thread->state`
- 自测非法状态转换和重复入队问题。
### B. wait queue 锁语义
- 为 wait queue 增加内部锁或要求调用方持有指定锁,并在接口命名中体现约束。
- 继续把条件所属数据的修改规则文档化;当前 wait queue 内部锁已经覆盖条件检查、等待入队和 wakeup 队列修改。
- 区分 `wake_one`、`wake_all`、timeout wakeup 和条件 wakeup 的状态处理。
- 检查 wakeup 是否可能唤醒 DEAD、RUNNING 或未入队线程。
### C. interrupt-exit reschedule
- 把当前 `sched_irq_exit()` 继续收敛为更严格的 trap-frame aware interrupt-exit reschedule 模型。
- 明确 interrupt nested、idle、当前线程不可抢占等情况下是否允许调度。
- 避免把普通线程栈切换入口长期当成完整抢占式切换。
- 为未来 syscall return 和 user-mode return 复用同一 reschedule 边界。
### D. thread lifecycle
- 为线程退出增加更完整的生命周期状态、引用规则和最终释放约束。
- 补充“当前线程不能释放自身内核栈”的文档和自测。
- 为未来 `kthread_stop()`、join/wait 和进程退出保留接口空间。
- 回收路径需要覆盖等待队列残留、run queue 残留和 timer sleep 残留。
### E. deferred execution
- 增加最小 workqueue 或 deferred work让 IRQ handler 可以只入队工作并唤醒 worker。
- 增加 completion 风格的一次性等待原语,供驱动初始化和异步完成使用。
- 增加 delayed work 或 timer callback 的设计草案,避免 timeout 逻辑复制到每个子系统。
下一阶段:
- 继续在 `04-time-scheduler.md` 内推进 wait queue 锁语义、线程生命周期和更严格的 interrupt-exit reschedule。
- 在这些边界稳定前,不急着进入 `05-input-events.md` 的键盘驱动实现。