一文讲解Linux进程调度器-基础
说明:
Kernel版本:4.14
ARM64处理器,Contex-A53,双核
使用工具:Source Insight 3.5, Visio
1. 概念
1.1 进程

从教科书上,我们都能知道:进程是资源分配的最小单位,而线程是CPU调度的的最小单位。
进程不仅包括可执行程序的代码段,还包括一系列的资源,比如:打开的文件、内存、CPU时间、信号量、多个执行线程流等等。而线程可以共享进程内的资源空间。
在Linux内核中,进程和线程都使用
struct task_struct
结构来进行抽象描述。进程的虚拟地址空间分为用户虚拟地址空间和内核虚拟地址空间,所有进程共享内核虚拟地址空间,没有用户虚拟地址空间的进程称为内核线程。
Linux内核使用task_struct
结构来抽象,该结构包含了进程的各类信息及所拥有的资源,比如进程的状态、打开的文件、地址空间信息、信号资源等等。task_struct
结构很复杂,下边只针对与调度相关的某些字段进行介绍。
【文章福利】小编推荐自己的Linux内核技术交流群:【749907784】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!(含视频教程、电子书、实战项目及代码)


1.2 进程状态

上图中左侧为操作系统中通俗的进程三状态模型,右侧为Linux对应的进程状态切换。每一个标志描述了进程的当前状态,这些状态都是互斥的;
Linux中的
就绪态
和运行态
对应的都是TASK_RUNNING
标志位,就绪态
表示进程正处在队列中,尚未被调度;运行态
则表示进程正在CPU上运行;
内核中主要的状态字段定义如下
1.3 scheduler 调度器

所谓调度,就是按照某种调度的算法,从进程的就绪队列中选取进程分配CPU,主要是协调对CPU等的资源使用。进程调度的目标是最大限度利用CPU时间。
内核默认提供了5个调度器,Linux内核使用struct sched_class
来对调度器进行抽象:
Stop调度器, stop_sched_class
:优先级最高的调度类,可以抢占其他所有进程,不能被其他进程抢占;Deadline调度器, dl_sched_class
:使用红黑树,把进程按照绝对截止期限进行排序,选择最小进程进行调度运行;RT调度器, rt_sched_class
:实时调度器,为每个优先级维护一个队列;CFS调度器, cfs_sched_class
:完全公平调度器,采用完全公平调度算法,引入虚拟运行时间概念;IDLE-Task调度器, idle_sched_class
:空闲调度器,每个CPU都会有一个idle线程,当没有其他进程可以调度时,调度运行idle线程;
Linux内核提供了一些调度策略供用户程序来选择调度器,其中Stop调度器
和IDLE-Task调度器
,仅由内核使用,用户无法进行选择:
SCHED_DEADLINE
:限期进程调度策略,使task选择Deadline调度器
来调度运行;SCHED_RR
:实时进程调度策略,时间片轮转,进程用完时间片后加入优先级对应运行队列的尾部,把CPU让给同优先级的其他进程;SCHED_FIFO
:实时进程调度策略,先进先出调度没有时间片,没有更高优先级的情况下,只能等待主动让出CPU;SCHED_NORMAL
:普通进程调度策略,使task选择CFS调度器
来调度运行;SCHED_BATCH
:普通进程调度策略,批量处理,使task选择CFS调度器
来调度运行;SCHED_IDLE
:普通进程调度策略,使task以最低优先级选择CFS调度器
来调度运行;
1.4 runqueue 运行队列

每个CPU都有一个运行队列,每个调度器都作用于运行队列;
分配给CPU的task,作为调度实体加入到运行队列中;
task首次运行时,如果可能,尽量将它加入到父task所在的运行队列中(分配给相同的CPU,缓存affinity会更高,性能会有改善);
Linux内核使用struct rq
结构来描述运行队列,关键字段如下:
2.5 task_group 任务分组

利用任务分组的机制,可以设置或限制任务组对CPU的利用率,比如将某些任务限制在某个区间内,从而不去影响其他任务的执行效率;
引入
task_group
后,调度器的调度对象不仅仅是进程了,Linux内核抽象出了sched_entity/sched_rt_entity/sched_dl_entity
描述调度实体,调度实体可以是进程或task_group
;使用数据结构
struct task_group
来描述任务组,任务组在每个CPU上都会维护一个CFS调度实体、CFS运行队列,RT调度实体,RT运行队列
;
Linux内核使用struct task_group
来描述任务组,关键的字段如下:
2. 调度程序
调度程序依靠几个函数来完成调度工作的,下边将介绍几个关键的函数。
主动调度 -
schedule()

schedule()
函数,是进程调度的核心函数,大体的流程如上图所示。核心的逻辑:选择另外一个进程来替换掉当前运行的进程。进程的选择是通过进程所使用的调度器中的
pick_next_task
函数来实现的,不同的调度器实现的方法不一样;进程的替换是通过context_switch()
来完成切换的,具体的细节后续的文章再深入分析。
2. 周期调度 - schedule_tick()

时钟中断处理程序中,调用
schedule_tick()
函数;时钟中断是调度器的脉搏,内核依靠周期性的时钟来处理器CPU的控制权;
时钟中断处理程序,检查当前进程的执行时间是否超额,如果超额则设置重新调度标志(
_TIF_NEED_RESCHED
);时钟中断处理函数返回时,被中断的进程如果在用户模式下运行,需要检查是否有重新调度标志,设置了则调用
schedule()
调度;
3. 高精度时钟调度 - hrtick()

高精度时钟调度,与周期性调度类似,不同点在于周期调度的精度为ms级别,而高精度调度的精度为ns级别;
高精度时钟调度,需要有对应的硬件支持;
4. 进程唤醒时调度 - wake_up_process()

唤醒进程时调用
wake_up_process()
函数,被唤醒的进程可能抢占当前的进程;
上述讲到的几个函数都是常用于调度时调用。此外,在创建新进程时,或是在内核抢占时,也会出现一些调度点。
原文作者:LoyenWang
