一. 调度器概述(内核3.18)
1.调度时机:
a.调用cond_resche()时。
b.显式调用schedule()时。
c.从系统调用或者异常中断返回用户空间时。
d.从中断上下文返回到用户空间时。
2.struct task_group 调度组
linux可以以以下两种方式进行进程的分组:
- 用户ID:按照进程的USER ID进行分组,在对应的/sys/kernel/uid/目录下会生成一个cpu.share的文件,可以通过配置该文件来配置用户所占CPU时间比例。
- cgourp(control group):生成组用于限制其所有进程,比如我生成一个组(生成后此组为空,里面没有进程),设置其CPU使用率为10%,并把一个进程丢进这个组中,那么这个进程最多只能使用CPU的10%,如果我们将多个进程丢进这个组,这个组的所有进程平分这个10%。
3.struct sched_entity 调度实体
a.调度实体可以是一个进程或一个进程组,比如:struct rb_node
vruntime= 实际运行时间 *(NICE_0_LOAD/权重)
4.struct sched_class 调度类
task_struct—>sched_class(调度器每次调度处理时,就通过当前进程的调度类函数进程操作,大大提高了可移植性和易修改性)
二. 初始化
1.sched_init:
在start_kernel中对调度器进行初始化的函数就是sched_init,其主要工作为
- 对相关数据结构分配内存
- 初始化root_task_group
- 初始化每个CPU的rq队列(包括其中的cfs队列和实时进程队列)
- 将init_task进程转变为idle进程
三.新进程加入
1.sched_fork()
copy_process()里面有一个函数专门用于进程调度的初始化,就是sched_fork().
在sched_fork()函数中,主要工作如下:
•获取当前CPU号
•禁止内核抢占(这里基本就是关闭了抢占,因为执行到这里已经是内核态,又禁止了被抢占)
•初始化进程p的一些变量(实时进程和普通进程通用的那些变量)
•设置进程p的状态为TASK_RUNNING(这一步很关键,因为只有处于TASK_RUNNING状态下的进程才会被调度器放入队列中)
•根据父进程和clone_flags参数设置进程p的优先级和权重。
•根据进程p的优先级设置其调度类(实时进程优先级:0~99 普通进程优先级:100~139)
•根据调度类进行进程p类型相关的初始化(这里就实现了实时进程和普通进程独有的变量进行初始化)
•设置进程p的当前CPU为此CPU。
•初始化进程p禁止内核抢占(因为当CPU执行到进程p时,进程p还需要进行一些初始化)
•使能内核抢占
2. task_fork_fair()
在实时进程的调度类中是没有特定的task_fork()函数的,而普通进程使用cfs策略时会调用到task_fork_fair()函数.
task_fork_fair()函数中主要就是设置进程p的虚拟运行时间和所处的cfs队列.
3.wake_up_new_task()
do_fork()中的wake_up_new_task(p)将进程加入到队列中。
4.enqueue_task_fair()
在wake_up_new_task()函数中,将进程加入到运行队列的函数为activate_task(),而activate_task()函数最后会调用到新进程调度类中的enqueue_task指针所指函数.在enqueue_task_fair()函数中又使用了enqueue_entity()函数进行操作.
需要注意的几点:
- 新创建的进程先会进行调度相关的结构体和变量初始化,其中会根据不同的类型进行不同的调度类操作,此时并没有加入到队列中。
- 当新进程创建完毕后,它的父进程会将其运行状态置为TASK_RUNNING,并加入到运行队列中。
- 加入运行队列时系统会根据CPU的负载情况放入不同的CPU队列中。
四.运行
1. 系统定时器
在内核中,会使用strut clock_event_device结构描述硬件上的定时器,每个硬件定时器都有其自己的精度,会根据精度每隔一段时间产生一个时钟中断。而系统会让每个CPU使用一个tick_device描述系统当前使用的硬件定时器(因为每个CPU都有其自己的运行队列),通过tick_device所使用的硬件时钟中断进行时钟滴答(jiffies)的累加(只会有一个CPU负责这件事),并且在中断中也会调用调度器,而我们在驱动中常用的低精度定时器就是通过判断jiffies实现的。而当使用高精度定时器(hrtimer)时,情况则不一样,hrtimer会生成一个普通的高精度定时器,在这个定时器中回调函数是调度器,其设置的间隔时间同时钟滴答一样。
所以在系统中,每一次时钟滴答都会使调度器判断一次是否需要进行调度。
2. 时钟中断
a.tick_handle_periodic()
此函数主要工作是执行tick_periodic()函数,然后判断时钟中断是单触发模式还是循环触发模式,如果是循环触发模式,则直接返回,如果是单触发模式,则执行如下操作:
- 计算下一次触发时间
- 设置下次触发时间
- 如果设置下次触发时间失败,则根据timekeeper等待下次tick_periodic()函数执行时间。
- 返回第一步
b.tick_periodic()
# tick_device 周期性调用此函数
# 更新jffies和当前进程
# 只有一个CPU是负责更新jffies的,其他的CPU只会更新当前自己的进程
注意:
1. check_preempt_tick()函数就是用来判断进程是否需要被调度的,其判断的标准有两个:
先判断当前进程的实际运行时间是否超过CPU分配给这个进程的CPU时间,如果超过,则需要调度。
再判断当前进程的vruntime是否大于下个进程的vruntime,如果大于,则需要调度。
2. 在pick_next_task()中完全体现了进程优先级的概念,首先会先判断是否所有进程都处于cfs队列中,如果不是,则表明有比普通进程更高优先级的进程(包括实时进程)。内核中是将调度类重优先级高到低进行排列,然后选择时从最高优先级的调度类开始找是否有进程需要调度,如果没有会转到下一优先级调度类.