1. 前言
进程调度是操作系统中一个重要的管理任务,它决定在给定时间点哪个进程可以被调度执行。进程调度的目标是最大程度地利用系统资源、提高系统吞吐量、降低响应时间以及确保公平性和优先级的合理分配。
Linux 内核提供了强大的进程调度能力以及丰富的调度策略。让 Linux 系统可以轻松应对 CPU 密集型、实时以及 IO 密集型等各种各样的计算任务。
本文主要通过结合上文提供的流程图,简要介绍一下 Linux 内核中进程的调度逻辑。
2. 基础数据结构
2.1 进程描述符
在 Linux 中描述进程的数据结构是task_strct
,里面包含了进程描述信息、分配给进程的内存资源、进程打开的文件列表以及进程调度相关的信息。下面是节选了跟进程调度相关的字段。
c
struct task_struct {
/*
* 进程优先级
*/
int prio;
int static_prio;
int normal_prio;
unsigned int rt_priority;
/*
* 进程关联的调度实体
*/
struct sched_entity se;
struct sched_rt_entity rt;
struct sched_dl_entity dl;
/*
* 进程关联的调度类
*/
const struct sched_class *sched_class;
}
2.2 调度实体
调度实体(sched_entity),在 Linux 内核中所有可以被调度器调度的对象都会有一个对应的调度实体。
这里需要注意的是,调度实体并不是一个结构体,而是一系列结构的总称。它包括sched_entity
、sched_rt_entity
以及sched_dl_entity
等。其中sched_entity
是被CFS
调度类使用,而sched_rt_entity
则为RT
调度类使用。
2.3运行队列RQ
rq
结构体(struct rq
)是一个进程调度一个核心的数据结构,它表示一个运行队列。其用于跟踪和管理在特定 CPU 上等待运行的进程和线程。rq
结构体定义了一系列的字段,这些字段用于存储和管理关于 CPU 调度的各种信息。包括当前正在运行的任务、等待运行的任务队列、调度器统计信息、负载平衡信息等。
2.4 调度类
在 Linux 内核中调度类负责实际执行下一个要运行进程的挑选工作。sched_class
主要由一系列函数指针构成,这些函数指针由具体的调度类负责实现。
c
struct sched_class {
const struct sched_class *next; // 指向下一个调度类的指针
void (*enqueue_task)(struct rq *rq, struct task_struct *p, int flags);
void (*dequeue_task)(struct rq *rq, struct task_struct *p, int flags);
void (*check_preempt_curr)(struct rq *rq, struct task_struct *p, int flags);
struct task_struct *(*pick_next_task)(struct rq *rq, struct task_struct *prev);
...
};
- enqueue_task:将一个进程添加进队列。
- dequeue_task:将一个进程从队列里面移除。
- check_preempt_curr:判断当前进程是否可以被抢占。
- pick_next_task:选择下一个要运行的进程。
3. 调度流程
Linux 内核中进程的调度入口函数是schedule
函数,在schedule
函数的逻辑中,首先会提取出当前进程所运行的 CPU,然后从 CPU 中取出关联的 rq
运行队列。
在 Linux 内核中
rq
是一个per-CPU
变量,会为每个 CPU 保存一份单独的拷贝,避免了多个 CPU 之间的数据竞争。per-CPU
变量的实现具体取决与 CPU 的架构,有些 CPU 架构是存放与 CPU 的本地存储器(local memory)中。有些实现在是存放与一个特殊的段(Segment)中。
由于2.3小节的介绍可知,rq
是 CPU 表征运行队列的结构体。所有等待CPU调度的进程都会在rq
中排队(排队的队列可能是FIFO队列,也可能是优先级队列,具体取决于调度类)。
Linux 内核提供了多种调度策略,它们被抽象为一个个调度类。目前,内核中提供的调度类包括停机调度类(stop_class)、限期调度类(dl_class)、实时调度类(rt_class)、完全公平调度类(cfs_class)等等。
在 Linux 内核中维护了一个全局的调度类数组sched_class
,当开始挑选下一个进程上CPU运行时,内核代码会遍历此数组,分别调用这些调度类上定义的pick_next_task
函数获取下一个要运行的进程。这也意味着排在数组前面的调度类进程更有机会进入CPU运行。这也是调度类优先级实现的机制。排在数组前面的调度类优先级越高。
当挑选出下一个运行的进程之后,内核代码会进入CPU上下文切换流程调用context_switch
函数。自此,进程的调度工作已经完成。
4. 总结
本文是我第一次写 Linux 内核相关的博文,内容上难免存在纰漏和瑕疵。如能邮件指出不胜感激。
5. 参考资料
- it.0voice.com/p/t_pc/cour...
- elixir.bootlin.com/linux/lates...
- makelinux.github.io/kernel/map/
- time.geekbang.org/column/arti...