2.6 完全公平调度CFS
- 核心思想:度量每个进程的虚拟运行时间,调度运行时间最少的进程运行
- 虚拟运行时间:由物理时钟、进程优先级确定,优先级越高,虚拟时钟越慢,也就是进程越容易被调度到
2.6.1 数据结构
- CFS的就绪队列
vruntime:位于sched_entity,记录进程在虚拟时钟上流逝的数量min_vruntime:是单调递增的,跟踪队列上所有进程的最小虚拟运行时间,比最左边的树结点的vruntime大些
/* CFS-related fields in a runqueue */
struct cfs_rq {
struct load_weight load; //就绪队列上进程的累积负荷值
unsigned long nr_running; //队列上可运行进程的数目
u64 min_vruntime; //所有进程的最小虚拟运行时间
struct rb_root tasks_timeline; //管理红黑树所有进程
struct rb_node *rb_leftmost; //指向红黑树最左边的节点(需要被调度的进程)
struct sched_entity *curr; //当前执行进程的可调度实体
......
};
2.6.2 CFS虚拟时间
1. 虚拟时钟计算
-
虚拟时钟:完全公平调度算法依赖虚拟时钟,度量进程能得到的CPU时间
- 依据:实际时钟、进程相关的负荷权重(进程优先级)
- 工作:更新当前进程执行的实际时钟、计算对应的虚拟时钟、更新队列的min_vruntime
// 核心函数:更新进程的物理时钟和虚拟时钟
static void update_curr(struct cfs_rq *cfs_rq) {
//curr表示就绪队列当前执行的进程
struct sched_entity *curr = cfs_rq->curr;
//now表示就绪队列的真实时钟
u64 now = rq_of(cfs_rq)->clock;
unsigned long delta_exec;
......
//计算 当前就绪队列时钟 和 上一次就绪队列时钟 的时间差delta_exec
delta_exec = (unsigned long)(now - curr->exec_start);
//委托给__update_curr,更新当前进程在CPU花费的物理时间和虚拟时间
__update_curr(cfs_rq, curr, delta_exec);
curr->exec_start = now;
......
}
static inline void
__update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr,
unsigned long delta_exec)
{
unsigned long delta_exec_weighted;
u64 vruntime;
......
//更新当前进程的执行总时间(物理时间)
curr->sum_exec_runtime += delta_exec;
......
//根据负荷权重计算当前进程增加的虚拟时钟(虚拟时间)
delta_exec_weighted = delta_exec;
if (unlikely(curr->load.weight != NICE_0_LOAD)) {
delta_exec_weighted = calc_delta_fair(delta_exec_weighted,
&curr->load);
}
curr->vruntime += delta_exec_weighted;
//对CFS队列更新min_vruntime(内核确保值单调增加)
if (first_fair(cfs_rq)) {
//如果红黑树有最左边结点(有进程在等待调度),vruntime=最左边结点进程的vruntime
vruntime = min_vruntime(curr->vruntime,
__pick_next_entity(cfs_rq)->vruntime);
} else {
//如果红黑树是空的,vruntime=当前进程的虚拟运行时间
vruntime = curr->vruntime;
}
cfs_rq->min_vruntime =
max_vruntime(cfs_rq->min_vruntime, vruntime);
}
其中,虚拟时钟的增长量delta_exec_weighted根据公式计算,取四舍五入:
- 根据表
prio_to_weight,nice=0的负荷权重load_weight=1024- nice=0(prio=120):delta_exec_weighted = delta_exec1024/1024 = delta_exec*
- nice=-10(prio=110):delta_exec_weighted = delta_exec1024/9548 = 0.107delta_exec
【总结】
- 对于
nice=0的进程,虚拟时间=物理时间;对于其他优先级,根据负荷权重load_weight重新衡定时间。 - 进程优先级越高,
nice值越小,进程负荷权重load_weight越大,虚拟时钟跑得越慢- 进程运行时,
vruntime稳定增加,越重要的进程虚拟时钟跑得越慢,在红黑树中位置更靠左,越容易被调度到 - 进程进入睡眠,
vruntime保持不变。每个队列min_vruntime增加,睡眠进程醒来则在红黑树中位置更靠左,更容易被调度
- 进程运行时,
2. 进程调度时间计算
- CFS没有时间片的概念,某个调度实体分配到的时间由以下公式决定:
- 其中就绪队列的权重就是所有调度实体权重之和,参见
update_load_add()
/* 计算某个调度实体分配到的时间 */
static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se) {
u64 slice = __sched_period(cfs_rq->nr_running);
slice *= se->load.weight;
do_div(slice, cfs_rq->load.weight);
return slice;
}
/* 计算就绪队列调度的总时间 */
static u64 __sched_period(unsigned long nr_running) {
//默认延迟周期为20ms
u64 period = sysctl_sched_latency;
//就绪队列上活动进程数目,默认5
unsigned long nr_latency = sched_nr_latency;
//如果队列上数目超过默认值,总时间要拉长(sysctl_sched_latency*nr_running/nr_latency)
if (unlikely(nr_running > nr_latency)) {
period *= nr_running;
do_div(period, nr_latency);
}
return period;
}
- 等价的虚拟时间在
__sched_vslice函数给出,公式:
- 进程调度时间计算涉及到三个内核参数:用于跟踪CFS的调度延迟(物理时间)
sysctl_sched_latency:CPU 密集型任务的目标抢占延迟,默认值20ms。每个调度实体一定在20ms内会被调度到(注意:和时间片不一样,时间片是固定的)sysctl_sched_min_granularity:CPU 密集型任务的最小抢占粒度,默认4ms。设的越小切换越频繁(个人理解为这4ms都是你的,不能被抢走)sched_nr_latency:控制一个延迟周期中处理的最大活动进程数目。如果活动进程数目超过该上限,延迟周期成比例地线性扩展(20ms最多处理5个任务,每个任务4ms)
/*
* Targeted preemption latency for CPU-bound tasks:
* (default: 20ms * (1 + ilog(ncpus)), units: nanoseconds)
*
* NOTE: this latency value is not the same as the concept of
* 'timeslice length' - timeslices in CFS are of variable length
* and have no persistent notion like in traditional, time-slice
* based scheduling concepts.
*
* (to see the precise effective timeslice length of your workload,
* run vmstat and monitor the context-switches (cs) field)
*/
unsigned int sysctl_sched_latency = 20000000ULL;
/*
* Minimal preemption granularity for CPU-bound tasks:
* (default: 4 msec * (1 + ilog(ncpus)), units: nanoseconds)
*/
unsigned int sysctl_sched_min_granularity = 4000000ULL;
/*
* is kept at sysctl_sched_latency / sysctl_sched_min_granularity
*/
static unsigned int sched_nr_latency = 5;
2.6.3 队列操作
1. 进程加入就绪队列
- 由
enqueue_task_fair完成- 进程最近在运行,更新vruntime后,直接加入就绪队列红黑树中
- 进程此前在睡眠,可能其vruntime与队列的vruntime差的很远,所以要先调整后再加到红黑树里,确保进程不会一直执行并更优先被调度到
- 如果进程是新fork的,分两种情况
- 父进程先执行,加入到就绪队列最后面,保证进程最后才被调度到
- 子进程先执行(Linux 2.6.24默认):保证子进程虚拟时钟小于父进程,即子进程先执行(2.6.7节),并加入到就绪队列最后面,进程最后被调度到
/* 进程加入队列,wakeup表示入队的进程是否此前在睡眠,当前被唤醒并转换为运行态 */
static void enqueue_task_fair(struct rq *rq, struct task_struct *p, int wakeup) {
struct cfs_rq *cfs_rq;
struct sched_entity *se = &p->se;
for_each_sched_entity(se) {
//已经在就绪队列中,直接返回
if (se->on_rq)
break;
cfs_rq = cfs_rq_of(se);
enqueue_entity(cfs_rq, se, wakeup);
wakeup = 1;
}
}
/* 核心函数,将调度实体加入队列 */
static void enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int wakeup)
{
//更新当前CFS队列时间
update_curr(cfs_rq);
if (wakeup) {
//如果进程此前在睡眠,调整进程的虚拟时间
place_entity(cfs_rq, se, 0);
enqueue_sleeper(cfs_rq, se);
}
......
if (se != cfs_rq->curr)
//内核函数,将调度实体置入红黑树中
__enqueue_entity(cfs_rq, se);
//就绪队列活动进程数cfs_rq->nr_running++;
account_entity_enqueue(cfs_rq, se);
}
/*更新新进程的vruntime值,以便把他插入红黑树 */
static void place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
{
u64 vruntime;
//以当前队列的min_vruntime作为基准时间
vruntime = cfs_rq->min_vruntime;
......
//是新进程被加到系统中,那么新进程的vruntime需要加上整个调度周期的虚拟实践,保证最后才被调度到
if (initial)
vruntime += sched_vslice_add(cfs_rq, se);
//进程此前在睡眠,扣掉sysctl_sched_latency,保证进程唤醒后能够更早被调度到
if (!initial) {
......
vruntime -= sysctl_sched_latency;
//取se->vruntime和vruntime的最大值
vruntime = max_vruntime(se->vruntime, vruntime);
}
//更新进程的vruntime
se->vruntime = vruntime;
}
- 其中
__enqueue_entity将进程置入红黑树中。加入的位置由entity_key确定。- 当前进程的vruntime越小,则加入的位置越靠左,越容易被选到(2.6.4节)
static inline s64 entity_key(struct cfs_rq *cfs_rq, struct sched_entity *se) {
return se->vruntime - cfs_rq->min_vruntime;
}
2.6.4 选择下一个进程
- 选择红黑树最靠左的进程作为下一个进程
static struct sched_entity *pick_next_entity(struct cfs_rq *cfs_rq) {
struct sched_entity *se = NULL;
if (first_fair(cfs_rq)) {
//选择最靠左的进程作为下一个进程
se = __pick_next_entity(cfs_rq);
//将进程取出标记为运行进程
set_next_entity(cfs_rq, se);
}
return se;
}
/* 选择进程后需要做一些工作,标记为运行进程 */
static void set_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
......
//从就绪队列中移除最左边的进程
if (se->on_rq) {
__dequeue_entity(cfs_rq, se);
}
//就绪队列标记当前进程,并更新进程运行的时间
update_stats_curr_start(cfs_rq, se);
cfs_rq->curr = se;
......
se->prev_sum_exec_runtime = se->sum_exec_runtime;
}
2.6.5 处理周期性调度器
- 内核按频率
HZ执行周期性调度。由task_tick_fair负责,实际工作由entity_tick完成- 更新就绪队列实际时钟、虚拟时钟统计量
- 如果当前进程运行时间过长,则重新调度
static void entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr) {
//更新统计量
update_curr(cfs_rq);
if (cfs_rq->nr_running > 1 || !sched_feat(WAKEUP_PREEMPT))
//看看进程运行的时间是否过长,需要重新调度
check_preempt_tick(cfs_rq, curr);
}
static void check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr) {
unsigned long ideal_runtime, delta_exec;
//计算当前进程的调度时间
ideal_runtime = sched_slice(cfs_rq, curr);
delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;
//如果当前进程的运行时间过长,超过了调度时间额度,需要重新调度
if (delta_exec > ideal_runtime)
resched_task(rq_of(cfs_rq)->curr);
}
- 重新调度
resched_task()设置标志TIF_NEED_RESCHED。进程在适当时机(比如系统调用返回、中断返回)发起schedule()
asmlinkage void __sched schedule(void)
{
need_resched:
......
prev = rq->curr;
......
prev->sched_class->put_prev_task(rq, prev); //将当前进程重新放回就绪队列中
next = pick_next_task(rq, prev); //选择下一个进程调度执行
......
if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))
goto need_resched;
}
2.6.6 唤醒抢占
- 在
try_to_wake_up和wake_up_new_task中唤醒进程时,内核使用check_preempt_curr看看是否新进程可以抢占当前进程 - 完全公平调度器使用
check_preempt_wakeup实现check_preempt_curr- 如果是实时进程,会立即抢占CFS进程
- 如果是批处理进程,不抢占其他进程
- CFS进程抢占,要保证进程运行
sysctl_sched_wakeup_granularity(默认4ms),避免频繁切换
/*
* Preempt the current task with a newly woken task if needed:
*/
static void check_preempt_wakeup(struct rq *rq, struct task_struct *p)
{
struct task_struct *curr = rq->curr;
struct cfs_rq *cfs_rq = task_cfs_rq(curr);
struct sched_entity *se = &curr->se, *pse = &p->se;
unsigned long gran;
//如果是实时进程,会立即抢占CFS进程
if (unlikely(rt_prio(p->prio))) {
update_rq_clock(rq);
update_curr(cfs_rq);
resched_task(curr);
return;
}
//如果是批处理进程,不抢占其他进程
if (unlikely(p->policy == SCHED_BATCH))
return;
......
//CFS进程被新CFS进程抢占,需要至少运行4ms。注意这里4ms要转成对应的虚拟时间
gran = sysctl_sched_wakeup_granularity;
if (unlikely(se->load.weight != NICE_0_LOAD))
gran = calc_delta_fair(gran, &se->load);
//超过4ms则重新调度
if (pse->vruntime + gran < se->vruntime)
resched_task(curr);
}
2.6.7 处理新进程
- 由
sysctl_sched_child_runs_first控制fork进程后是父进程先执行还是子进程先执行- Linux 2.6.24默认值为1,表示子进程先执行。后续内核有修订
- 过程:更新虚拟时钟、确保子进程先执行、子进程加入就绪队列、重新调度(注意:这里子进程加入就绪队列后不一定会立即执行)
long do_fork(unsigned long clone_flags, //标志集合,SIGCHLD表示fork后发送SIGCHLD信号给父进程
unsigned long stack_start, //用户状态下栈的起始地址
struct pt_regs *regs, //指向寄存器集合的指针
unsigned long stack_size, //用户状态下栈的大小,通常为0
int __user *parent_tidptr, //指向父进程PID
int __user *child_tidptr) //指向子进程PID
{
......
// 将子进程加入调度器队列,由调度器选择它运行
if (!(clone_flags & CLONE_STOPPED))
wake_up_new_task(p, clone_flags);
......
}
//唤醒子进程
void fastcall wake_up_new_task(struct task_struct *p, unsigned long clone_flags)
{
//初始化子进程优先级
p->prio = effective_prio(p);
......
//子进程调度时间初始化
p->sched_class->task_new(rq, p);
inc_nr_running(p, rq);
......
}
//完全公平调度走task_new_fair
static void task_new_fair(struct rq *rq, struct task_struct *p) {
struct cfs_rq *cfs_rq = task_cfs_rq(p);
struct sched_entity *se = &p->se, *curr = cfs_rq->curr;
int this_cpu = smp_processor_id();
sched_info_queued(p);
//更新虚拟时钟,更新se->vruntime。注意第三个参数initial=1
update_curr(cfs_rq);
place_entity(cfs_rq, se, 1);
//这里保证子进程先执行
if (sysctl_sched_child_runs_first && this_cpu == task_cpu(p) &&
curr && curr->vruntime < se->vruntime) {
/*
* Upon rescheduling, sched_class::put_prev_task() will place
* 'current' within the tree based on its new key value.
*/
swap(curr->vruntime, se->vruntime);
}
//加入队列,重新请求调度
enqueue_task_fair(rq, p, 0);
resched_task(rq->curr);
}
- 代码中
swap()的作用:如果父进程虚拟运行时间curr->vruntime小于子进程运行时间se->vruntime,表示父进程先于子进程执行。所以通过交换虚拟时钟的方式,保证子进程先执行。