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

12 KiB
Raw Blame History

04 时钟、内核线程与调度

目标

建立 kernel 内部并发执行能力,为驱动等待、异步 I/O、进程和用户态做准备。

前置条件

  • 中断入口可用。
  • 内存分配可用。
  • 至少一个 timer 可接入。

建议边界

  • kernel/sched/core.crun queue、调度选择、tick 和 IRQ exit 调度边界。
  • kernel/sched/thread.c:线程对象创建、初始栈、退出和 DEAD 线程回收。
  • kernel/sched/wait.c:等待队列。
  • kernel/sched/idle.cidle 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 留边界。
  • 调度核心需要集中处理 RUNNINGREADYSLEEPINGWAITINGDEAD 等状态转换。
  • 调度选择、入队、出队和当前线程切换必须有统一锁规则。
  • 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_reschedtrap 的 IRQ 返回边界统一消费调度请求。
  • 已建立最小 DEAD 线程回收路径,调度前会释放非当前 DEAD 线程的内核栈和线程对象。
  • 已在调度私有头中加入 thread state helper调度核心、线程退出和 wait queue 路径不再直接散写主要状态转换。
  • 已把调度代码按职责拆分为 core.cthread.cwait.cidle.c 和私有 sched.h,并把当前阶段自测/演示线程移到 kernel/selftest/sched.c
  • scripts/check.sh 已验证 timer initializedtimer tick=1/2/3scheduler initializedkernel thread selftest ok、timer 驱动线程轮转、sched_sleep()、wait queue wakeup、条件等待和超时等待。

后续扩展:

A. 调度状态机收紧

  • 明确合法状态转换,例如 READY -> RUNNING -> READYRUNNING -> SLEEPING/WAITINGSLEEPING/WAITING -> READYRUNNING -> DEAD
  • 继续扩大状态转换辅助函数覆盖范围,减少外部代码直接写 thread->state
  • 自测非法状态转换和重复入队问题。

B. wait queue 锁语义

  • 为 wait queue 增加内部锁或要求调用方持有指定锁,并在接口命名中体现约束。
  • 继续把条件所属数据的修改规则文档化;当前 wait queue 内部锁已经覆盖条件检查、等待入队和 wakeup 队列修改。
  • 区分 wake_onewake_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 的键盘驱动实现。