如何选择和切换下一个进程 —— Linux进程调度原理与代码全解


如何选择和切换下一个进程 —— Linux进程调度原理与代码全解

文章链接支持新书:《Yocto项目实战教程:高效定制嵌入式Linux系统》
京东购买链接


目录

  1. 进程调度的基础概念
  2. 进程、线程、任务结构体的关系梳理
  3. sched_entity:调度的核心粒度
  4. 进程选择(挑选下一个进程)的核心逻辑
  5. schedule() 函数源码流程解析
  6. 进程切换的全过程
  7. 调度器的主流算法(以CFS为例)
  8. 内核抢占与调度点
  9. 重要结构体/函数剖析
  10. 实战调试与打印技巧
  11. 常见场景与误区
  12. 小结与延伸阅读

1. 进程调度的基础概念

操作系统的本质就是“在有限的CPU资源下,合理高效地运行多个进程/线程”。而调度器(scheduler)就是操作系统内核中,负责决定“哪个进程/线程上CPU、何时切换、怎样切换”的关键机制。

调度包括两个核心问题:

  • 选择(Select):在众多可运行的进程中,挑选一个最合适的“下一个”。
  • 切换(Switch):完成从当前进程到下一个进程的上下文切换。

Linux 内核通过一整套高效的数据结构、调度算法,以及软硬件协同手段,实现了公平、实时、低延迟的多任务调度。


2. 进程、线程、任务结构体的关系梳理

2.1 任务的本质:task_struct

Linux中,“进程/线程”统一用task_struct结构体表示。每个正在运行或可运行的进程(线程)都有一个唯一的task_struct实例。

核心内容包括:

  • 进程ID(PID)、状态
  • 进程优先级、调度策略
  • 进程地址空间、文件描述符表
  • 调度实体(sched_entity)(重点)

2.2 调度实体 sched_entity

sched_entity 结构体不是独立存在的,而是作为 task_struct 的成员。其设计是为了支撑“调度算法”的实现。

  • 一个 task_struct 一定包含一个 sched_entity
  • sched_entity 记录了与调度相关的权重、vruntime、执行时长等信息
  • 多线程(如CFS调度组)时,group也用sched_entity,体现层级关系

简化结构:

struct task_struct {
    ...
    struct sched_entity se;
    ...
};

3. sched_entity:调度的核心粒度

sched_entity 承载着调度算法的“可公平比对”信息。以CFS为例,每个进程都对应一个sched_entity,它被组织在红黑树(rb_tree)中。

关键成员:

  • vruntime:当前实体的虚拟运行时间(衡量公平性的核心)
  • exec_startsum_exec_runtime:累积的执行时间
  • load_weight:权重,影响进程被选中的频率
  • run_node:红黑树节点

作用:

  • 让调度器用统一、抽象的“实体”来比较、排序、挑选任务
  • 屏蔽了task_struct中与调度无关的内容

4. 进程选择(挑选下一个进程)的核心逻辑

4.1 选择下一个进程,实际上是选下一个sched_entity

以CFS调度器为例,所有处于TASK_RUNNING状态的进程,其sched_entity会被插入红黑树中。
调度器每次调度时,都会:

  1. 在红黑树上寻找vruntime最小的sched_entity
  2. 找到后,定位其对应的task_struct(即要切换的进程)

流程简述:

  • 红黑树排序,最“公平”(vruntime最少)的排最左
  • 选中最左侧节点,对应进程“被调度上CPU”

4.2 pick_next_task() 的作用

  • pick_next_task() 就是实现“挑选下一个进程”的主力函数

  • 典型代码(位于kernel/sched/fair.c):

    static struct task_struct *pick_next_task_fair(struct rq *rq) {
        struct sched_entity *se = pick_next_entity(cfs_rq);
        // se对应的task_struct就是要切换的进程
        return task_of(se);
    }
    
  • 多调度器共存(CFS/RT/Deadline),由sched_class多态分发


在这里插入图片描述

5. schedule() 函数源码流程解析

schedule()是调度器的核心入口函数,位于 kernel/sched/core.c,每当需要进行调度时都会调用。

精简流程如下:

  1. 关闭抢占(preempt_disable),保证调度期间不会被再次抢占
  2. 调用__schedule(),进入调度主循环
  3. __schedule()里会遍历调度类,执行pick_next_task()选择下一个进程
  4. 若当前与下一个进程不同,则调用context_switch()完成上下文切换
  5. 恢复抢占(preempt_enable)

源码片段简析:

asmlinkage __visible void __sched schedule(void)
{
    preempt_disable();
    __schedule(SM_NONE);
    preempt_enable();
}

__schedule()里核心就是pick_next_task() + context_switch()


6. 进程切换的全过程

进程切换(Context Switch)并不是一行代码就能搞定,而是一个完整的CPU资源归还与重分配过程:

  1. 保存当前进程的上下文(CPU寄存器、栈指针、PC等)
  2. 更新当前task_struct的状态
  3. 切换到下一个task_struct(切换页表、切换栈等)
  4. 恢复下一个进程的上下文
  5. CPU开始执行新进程

Linux通过switch_to(prev, next, last)完成底层切换,底层多是汇编代码。
整个切换过程由schedule() -> __schedule() -> context_switch() -> switch_to()贯穿实现。


7. 调度器的主流算法(以CFS为例)

CFS(Completely Fair Scheduler)完全公平调度器:

  • 以vruntime为核心公平度指标
  • 所有进程调度实体组织成红黑树
  • 每次调度挑选vruntime最小的进程(最“亏”的进程)
  • 保证所有进程“获得CPU时间总量大致公平”
  • 权重影响vruntime增速,优先级高的进程“变慢”

常见调度类:

  • CFS(普通进程)
  • RT(实时进程)
  • DEADLINE(实时调度)

8. 内核抢占与调度点

内核抢占(Preemption):

  • 在关键信号量、锁保护区域,或关中断期间禁止抢占,保证数据一致性
  • preempt_disable()/preempt_enable()手动控制临界区
  • 通常在do_exit()sys_fork()、定时器中断、I/O中断等“调度点”主动触发调度

调度点(Schedule Point):

  • 某些内核API、系统调用、内核定时器、用户主动让出CPU(如sched_yield())都会触发调度

9. 重要结构体/函数剖析

9.1 task_struct

进程/线程的描述符,贯穿生命周期的核心对象。包含进程状态、调度信息、资源占用等几乎所有重要属性。

9.2 sched_entity

调度算法核心的数据结构。承担公平度计算、优先级、累计运行时间等功能,是调度粒度的具体化。

9.3 pick_next_task()

选择下一个task_struct,分调度器多态分发(CFS、RT、Deadline)。

9.4 context_switch()

负责底层上下文切换,最终实现CPU寄存器、内核栈、页表切换。

9.5 need_resched()

判断当前是否需要调度(被抢占、主动让出等)


10. 实战调试与打印技巧

10.1 打印调度流程

  • schedule()pick_next_task()context_switch()等关键位置增加pr_info打印:

    pr_info("[SCHED] schedule(): cpu=%d, prev=%s, next=%s\n",
        smp_processor_id(), prev->comm, next->comm);
    
  • 打印vruntime、PID等,观察调度公平性

10.2 跟踪fork和进程切换

  • kernel_clone/copy_process中添加打印,记录父子进程、栈地址等:

    pr_info("[FORK] Parent: %s(%d), Child: %s(%d)\n",
        current->comm, current->pid, child->comm, child->pid);
    

10.3 使用 ftrace/perf/strace 追踪调度

  • ftrace支持调度事件(sched_switchsched_wakeup等),可用来分析调度行为

11. 常见场景与误区

  • “用户进程和内核进程切换”:本质都是task_struct,调度器只关心“可运行的任务”,不区分“用户/内核进程”。
  • “为什么需要 sched_entity?”:用于抽象和统一调度算法,支撑多层级(如调度组)。
  • “能否只靠task_struct做调度?”:task_struct包含太多与调度无关信息,不便于算法抽象和性能优化。
  • “选定sched_entity是否等价于选定进程?”:完全等价,sched_entity和task_struct是一一对应的。
  • “抢占期间调度安全吗?”:必须通过preempt_disable/enable保证临界区调度安全,防止数据竞争。

12. 小结与延伸阅读

12.1 小结

  • 进程调度的实质:选中下一个sched_entity,其对应的task_struct被切换到CPU
  • 关键路径:schedule() → pick_next_task() → context_switch()
  • 核心结构体:task_struct、sched_entity
  • 调度算法:CFS红黑树公平调度,RT、Deadline等多调度类协作
  • 调试利器:pr_info打印、ftrace、perf、源码instrument
  • 常见误区:调度只认“可运行的任务”,task_struct与sched_entity一一对应

12.2 延伸阅读与建议

  • 推荐阅读Linux内核源码kernel/sched/core.ckernel/sched/fair.c
  • 结合实际硬件场景,动手在开发板/虚拟机上实验、打印、调试
  • 推荐书籍:《Linux内核设计与实现》《深入理解Linux内核》《Yocto项目实战教程:高效定制嵌入式Linux系统》

文章链接支持新书:《Yocto项目实战教程:高效定制嵌入式Linux系统》
京东购买链接

视频教程请关注 B 站:“嵌入式 Jerry”


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值