真实面经题目 · 原创解析
内核多线程模块的线程调度是怎么实现的?
Linux 内核多线程模块的线程调度通常不是模块自己实现调度器,而是模块创建多个内核线程后交给 Linux 统一调度。每个内核线程都有 task_struct,进入某个调度类,挂到 CPU 运行队列上,由 CFS、实时调度类或其他调度类根据状态、优先级、vruntime、CPU 负载和亲和性决定何时运行、抢占、阻塞、唤醒和上下文切换。模块开发者更关注线程创建、等待唤醒、停止退出、锁边界和不可睡眠上下文。
真实面经题目 · 原创解析
Linux 内核多线程模块的线程调度通常不是模块自己实现调度器,而是模块创建多个内核线程后交给 Linux 统一调度。每个内核线程都有 task_struct,进入某个调度类,挂到 CPU 运行队列上,由 CFS、实时调度类或其他调度类根据状态、优先级、vruntime、CPU 负载和亲和性决定何时运行、抢占、阻塞、唤醒和上下文切换。模块开发者更关注线程创建、等待唤醒、停止退出、锁边界和不可睡眠上下文。
可以从三个层次回答。第一,调度对象是什么:Linux 调度器调度的是 task_struct 描述的任务,内核线程、用户进程、用户线程最终都以任务实体参与调度;内核线程通常没有独立用户地址空间,但仍然是可调度实体。第二,调度过程怎么走:线程处于 TASK_RUNNING 时进入某个 CPU 的运行队列,普通线程通常由 CFS 根据 vruntime 和权重选择,实时线程由 RT 调度类按优先级和 FIFO/RR 规则选择;时钟 tick、主动让出、阻塞、唤醒、优先级变化或抢占条件满足时会触发重新调度。第三,运行时发生什么:调度器选出下一个任务,context_switch 保存当前上下文并恢复下一个上下文;线程等待 IO、锁、事件或超时时会进入睡眠状态,从运行队列移除,唤醒后重新入队。多核下每个 CPU 有本地运行队列,还会做负载均衡、迁移和 CPU 亲和性处理。
Linux 调度器的核心对象是 task_struct。无论普通进程、用户态 pthread,还是 kthread_create、kthread_run 创建的内核线程,最终都有一个 task_struct。用户线程通常在 NPTL 模型下按 1:1 映射为内核可见任务;内核线程也有 task_struct,但通常没有独立用户态地址空间。调度器主要关心状态、调度类、优先级、CPU 亲和性和运行队列位置。
内核模块常用 kthread_run 或 kthread_create 创建线程,线程函数通常循环检查 kthread_should_stop,处理工作,没有工作时进入等待。模块卸载时要调用 kthread_stop,并确保等待中的线程能被唤醒并正常退出。创建多个线程只是把工作拆成多个可调度实体,CPU 分配和抢占仍由内核调度器完成。
每个 CPU 都有自己的 runqueue,保存该 CPU 上可运行的任务。普通 CFS 任务进入 CFS 结构,实时任务进入实时队列,deadline 任务进入 deadline 结构。线程只有处于 TASK_RUNNING 状态时才参与 CPU 竞争;一旦等待 IO、锁、事件或定时睡眠,就会从可运行集合中移除,唤醒后再根据负载和亲和性选择 CPU 入队。
普通线程默认使用 CFS。CFS 不是简单固定时间片轮转,而是尽量让可运行任务按权重公平分享 CPU。它用 vruntime 记录任务已经获得的加权运行时间,nice 值越低、权重越高,vruntime 增长越慢,因此更容易获得运行机会。内核模块中的普通 kthread 如果不设置实时策略,也会按 CFS 规则竞争 CPU。
SCHED_FIFO 和 SCHED_RR 属于实时调度类,优先级高于普通 CFS 任务。FIFO 线程会一直运行到主动阻塞、让出或被更高优先级实时线程抢占;RR 在线程同优先级之间轮转。模块里如果滥用实时优先级,可能导致普通任务长期得不到运行,影响系统响应甚至造成可用性问题。只有明确低延迟需求且执行时间可控时才适合使用。
内核线程没有工作时应该睡眠,而不是忙等。常见模式是使用 wait_event、completion、schedule_timeout 或设置当前状态后调用 schedule。生产者更新条件后 wake_up,等待线程重新变成 TASK_RUNNING 并入队。实现时要避免条件检查和入睡之间丢失唤醒,优先使用内核提供的等待宏族而不是手写复杂流程。
调度器决定切换时,会通过 schedule 路径选出下一个任务,context_switch 保存当前寄存器、栈指针、调度状态并恢复下一个任务。内核线程通常不需要切换独立用户地址空间,但仍有内核栈、寄存器、缓存局部性和调度开销。模块不应为大量细碎任务创建过多线程,必要时可以考虑 workqueue、per-CPU worker 或批处理。
内核线程处于进程上下文,通常可以睡眠;但持有自旋锁、关闭抢占、处于中断或软中断上下文时,不能调用可能导致 schedule 的接口。需要睡眠等待时应使用 mutex、semaphore、completion、wait queue 等适合进程上下文的机制。很多内核并发 bug 都来自在不可睡眠边界里调用了会阻塞的函数。
从调度器核心视角看,它们都对应 task_struct,都可以进入运行队列并被调度类管理。区别在于用户线程通常有用户态地址空间,属于某个线程组;内核线程通常只在内核态运行,没有独立用户地址空间。上下文切换时,用户线程可能涉及地址空间切换,内核线程通常更简单,但仍需要切换栈和寄存器上下文。
固定时间片很难兼顾公平、优先级权重和交互响应。CFS 用 vruntime 表示任务已获得的加权 CPU 时间,优先运行 vruntime 较小的任务。nice 权重会影响 vruntime 增长速度,使高权重任务长期获得更大 CPU 份额。
常见情况包括等待事件、等待 IO、等待锁、等待 completion、定时睡眠、调用 cond_resched 或主动 schedule。良好的内核线程没工作时应进入等待,等生产者唤醒,而不是空转占用 CPU。线程函数通常循环检查退出条件和工作条件。
自旋锁用于短临界区,其他执行路径拿不到锁时会忙等。如果持有自旋锁的线程睡眠,锁长时间不释放,其他 CPU 可能一直自旋,甚至引发死锁。需要睡眠等待时应使用 mutex、semaphore、completion 或 wait queue 等允许调度的同步机制。
不一定。性能取决于任务是否能并行、是否受 CPU 或 IO 限制、锁竞争是否严重、缓存局部性是否良好。线程过多会增加调度开销、上下文切换、内核栈内存占用和缓存失效。短小异步任务可能更适合 workqueue,per-CPU 数据可能更适合 per-CPU worker。