任务
在内核层面并没有对进程与线程做显示的区分,二者统一都叫做任务(Task),区别体现在对资源的共享程度上:进程本质上是对资源的一种抽象与隔离,包括内存地址空间、文件、信号量等,Linux在通过系统调用 clone 创建一个新的 task 时,如果所有的资源都不共享,那么对用户而言就是创建了一个新的进程,如果除了函数调用栈(Stack)不共享、其他所有资源都共享,则创建的便是一个线程。
Linux系统通过可运行队列来管理可运行的任务,不同的调度有不同的可运行队列。
进程组和进程调度
进程调度
操作系统运行进程的时候,是按 时间片 来运行的。时间片 是指一段很短的时间段(如20毫秒),操作系统会为每个进程分配一些时间片。当进程的时间片用完后,操作系统将会把当前运行的进程切换出去,然后从进程队列中选择一个合适的进程运行,这就是所谓的 进程调度。
进程组
进程组是一组相关的进程的集合,它们可以被统一地控制和管理。每个进程组都有一个唯一的进程组ID,同时也有一个领头进程(也称为组长进程)。进程组ID(PGID)即为组长进程的进程ID(PID)。在 Linux 系统启动时,会创建一个根进程组 init_task_group。可以用cgroup的CPU子系统来创建新的进程组。创建子进程默认是父进程的进程组。进程调用setpgid系统调用也可将自己划分到新的进程组。进程组内可以是进程,也可以同时存在进程组。
进程分组调度(CFS调度算法)
- 获取当前CPU的可运行队列
- 把当前运行的进程放回到运行队列
- 从根进程组开始从可运行队列中获取最优的可运行实体,如果是进程组,继续迭代该进程组从可运行队列中获取,直到取到进程。
可运行队列
每个CPU都有一个可运行队列,其数据结构裁剪如下:
struct rq {
struct cfs_rq cfs; /* CFS runqueue 的实现 */
struct rt_rq rt; /* RT runqueue 的实现 */
struct dl_rq dl; /* Deadline runqueue 的实现 */
struct task_struct __rcu *curr; /* 当前 runqueue 中正在运行的任务 */
struct task_struct *idle; /* Idle 调度类的任务 */
struct task_struct *stop; /* Stop 调度类的任务 */
...
};
调度类和调度
linux设计了几种不同的调度类,用户定义不同的调度逻辑。调度类又有不同的调度策略。
stop_sched_classStop 是特殊的调度类,内核使用该调度类来停止 CPU. 该调度类用来强行停止CPU 上的其他任务,由于该调度类的优先级最高,因此一旦生效就将抢占任何当前正在运行的任务,并且在运行过程中自己不会被抢占。该调度类只有在SMP架构的系统中存在,内核使用该调度类来完成负载均衡与CPU热插拔等工作。dl_sched_class有些任务必须在指定时间窗口内完成。例如视频的编码与解码,CPU 必须以特定频率完成对应的数据处理;这类任务是优先级最高的用户任务,CPU 应该首先满足。 Deadline 调度类用来调度这类任务,dl 便是单词 Deadline 的缩写,因此该调度类的优先级仅仅低于 Stop 调度类。rt_sched_class在本节开头我们提到,实时任务(Real-time Task)对响应时间要求更高,例如编辑器软件,它可能由于等待用户输入长期处于睡眠之中,但一旦用户有输入动作,我们就期望编辑器能够立马响应,而不是等系统完成其它任务之后才开始反应,这一点对用户体验十分重要。RT 调度类用来调度这类任务,该调度类的优先级低于 DL.fair_sched_classFair 调度类用来调度绝大多数用户任务,CFS 实现的就是这种调度类,其核心逻辑是根据任务的优先级公平地分配 CPU 时间。我们会在后续章节中详细讨论 CFS 的实现细节。idle_sched_class与 Stop 类似,Idle 调度类也是仅供内核使用的特殊调度类,其优先级最低,只有在没有任何用户任务时才会用到。内核会为每个 CPU 绑定一个内核线程(kthread)来完成该任务,该线程会在队列无事可做的情况下启动该任务,并将 CPU 的功耗降到最低。
调度的过程就是调度器按照顺序对具体的调度类进行遍历,依次调用每个调度类的 pick_next_task 方法从指定 rq 中寻找下一个可执行任务,如果返回非空,则将该任务返回。
调度类又有如下的调度策略:
-
Stop调度类 中只有一个任务可供执行,不需要定义任何调度策略。
-
DL(Deadline)调度类 只实现了一种调度策略:
SCHED_DEADLINE, 用来调度优先级最高的用户任务。 -
RT(Real-Time)调度类 提供了两种调度策略:
SCHED_FIFO与SCHED_RR,- 对于使用
SCHED_FIFO的任务,其会一直运行到主动放弃CPU; - 而对于
SCHED_RR的任务,如果多个任务的优先级相同,则大家会按照一定的时间配额来交替运行,即使一个任务一直处于可运行状态,在使用完自己的时间切片之后也会被抢占,然后被放入队列的尾巴等待下次机会。
- 对于使用
-
CFS调度类 实现了三种调度策略:
SCHED_NORMAL: 被用于绝大多数用户进程SCHED_BATCH: 适用于没有用户交互行为的后台进程,用户对该类进程的响应时间要求不高,但对吞吐量要求较高,因此调度器会在完成所有SCHED_NORMAL的任务之后让该类任务不受打扰地跑上一段时间,这样能够最大限度地利用缓存。SCHED_IDLE: 这类调度策略被用于系统中优先级最低的任务,只有在没有任何其他任务可运行时,调度器才会将运行该类任务。
-
Idle 调度类也没有实现调度策略(注意不要将这类调度类与 CFS 中的
SCHED_IDLE混淆)。
CFS调度算法
CFS(Completely Fair Scheduler)完全公平调度器。CFS使用红黑树结构,来存储要调度的任务队列,每个节点代表了一个要调度的任务,节点的key即为虚拟时间(vruntime),虚拟时间由这个任务的运行时间计算而来,key越小,也就是vruntime越小的话,红黑树对应的节点就越靠左。CFS scheduler每次都挑选最左边的节点(vrutime最小的任务)作为下一个要运行的任务, 这个节点是“缓存的”——由一个特殊的指针指向;不需要进行O(logn)遍历来查找。也因此,CFS搜索的时间是O(1)。
vruntime
对于新任务,vruntime是 0 ,vruntime的增量计算方式 vruntime = (wall_time * ((NICE_0_LOAD * 2^32) / weight)) >> 32;其中wall_time是该任务已经运行的vruntime,NICE_0_LOAD=1024,weight是通过nice值查表得到的。当nice=0时weight恰好是1024。