进程调度
进程调度也是进程的关键内容之一,与之前的进程描述符中的多个字段息息相关。
本章分为三个部分,第一部分介绍Llinux系统的调度的架构,第二部分介绍不同的调度算法的数据结构和算法。第三部分介绍相关系统调用
相关章节
进程、中断、时钟等
调度策略
进程优先级:
调度标志位 : TIF_NEED_RESCHED
调度节点: 时钟中断
调度类型: FIFO(先进先出)、RR(时间片轮转),NORMAL(普通分时)
普通进程调度
静态优先级100~139, nice(), setpriority()调用设置。 通常来说,优先级越小则获取时间片越长。
PS :nice,可以理解为愿意礼让的程度,越小则越强横。
动态优先级: max(100, min(静态优先级 -bonus +5,139)) ,核心 bonus和5的关系,也就是进程平均睡眠的时间
这本书中的这一段的讲解我觉得已经有点过时,不做过多的描述了。
实时进程
sched_setparam() and sched_setscheduler()用于设置进程的实时优先级
需要注意的是,实时进程的轮转时间片长度与进程的静态优先级有关。
数据结构
运行队列,running queue。原文中描述runqueue是调度程序中最重要的数据结构
调度程序
scheduler_tick()
/*更新timestamp_last_tck()更新时间*/
sched_clock()
/*如果空闲且存在非空闲进程 则空闲进程调度*/
TIF_NEED_RESCHED
/*检查当前是否已经过期
如果已经过期(current->array不指向active活动列表),那么进行调度
否则,获取this_rq->lock(),并递减current的时间片
*/
/*用于核间队列平衡 */
rebalance_tick()
实时进程的时间片更新
FIFO,啥也不更新,因为不抢占
更新时间片(指重新获得)---->设置调度位(切换进程)---->从当前run_list移除------->重新添加到队尾
普通进程时间片更新
2.6.11中的进程调度算法,已经在2.6.38中发生了大量更改,就不花时间进行了解了。
后续会从深入Linux架构设计这本书中详细补充这一段
Try_to_wake_up()
准备将进程修改为running状态。如之前一样,我们在放书上代码前,先考虑先wake_up中可能存在的问题,其中最主要的问题之一就是上一次睡眠时候的CPU和现在唤醒时的CPU不是同一个,且有可能会将task唤醒到一个全新的CPU上。需要逐步完成,从旧的CPU rq上实现目标CPU上的迁移操作。
核心函数:activate_task(rq, p, en_flags);
try_to_wake_up()
/*获取当前CPU,禁止本地抢占,并返回当前CPU ID */
this_cpu = get_cpu();
smp_wmb();
/*获取task的p的之前对应的rq,禁止本地中断并返回中断flags的状态*/
rq = task_rq_lock(p, &flags);
/* 检查p的状态和要设置状态,指的是task的normal,stopped,traced状态 */
if (!(p->state & state)) goto out;
/* on_rq会在enqueue_task和dequeu的时候被设置,on表示执行中*/
if (p->se.on_rq) goto out_running;
/*获取原始的CPU号*/
cpu = task_cpu(p);
orig_cpu = cpu;
/*检查当前p是否正好在运行*/
if (unlikely(task_running(rq, p))) goto out_activate;
p->state = TASK_WAKING;
/*根据SD_BALANCE_WAKE,检查是否需要更换CPU*/
cpu = select_task_rq(rq, p, SD_BALANCE_WAKE, wake_flags);
cpu = p->sched_class->select_task_rq(rq, p, sd_flags, wake_flags);
//如果CPU更换,则
set_task_cpu()
//切换完成可能到新的rq,获取新的rq的锁
rq->lock()
/*此时rq已经在一个确定存在的CPU上*/
rq = cpu_rq(cpu);
WARN_ON(task_cpu(p) != cpu);
WARN_ON(p->state != TASK_WAKING);
/*先将task放到running queue上*/
ttwu_activate(p, rq, wake_flags & WF_SYNC, orig_cpu != cpu,
cpu == this_cpu, en_flags);
/* 将p在rq上唤醒,此时rq为之前的cpu或者为balance后的CPU*/
activate_task(rq, p, en_flags);
success = 1;
/*善后操作,实际调度类的task_woken参数,如果没有设置sync,则尝试抢占*/
ttwu_post_activation(p, rq, wake_flags, success);
check_preempt_curr()
if (p->sched_class->task_woken) p->sched_class->task_woken(rq, p);
if ((p->flags & PF_WQ_WORKER) && success) wq_worker_waking_up(p, cpu_of(rq));
/*释放rq的锁*/
task_rq_unlock(rq, &flags);
/*释放当前CPU*/
put_cpu();
关于进程调度的算法已经在2.6.38上做了新的设计,建议大家去看新的管理流程。
权重
每个任务加入时会根据p->se.load.weight加入到整体的rq权重中,同时更新相关计数器。每一个优先级会尝试多获取10%左右的CPU占用,实际是提供了权重值25%, 因为(1.25)/(1+1.25) = 5/9 约等于55%,对比原本1:1,提高了10%
CFS完全公平调度类
根据虚拟时钟,度量等待进程在完全公平系统中所能得到的CPU的时间,可以根据实际时钟和进程相关负荷权重推算,更新在update_curr上
CFS调度类,位于rq的CFS子类中,结构如下
struct cfs_rq {
struct load_weight load;
unsigned long nr_running;
u64 exec_clock;
u64 min_vruntime;
struct rb_root tasks_timeline;
struct rb_node *rb_leftmost;
...
};
虚拟时间更新update_curr
从rq获取当前的clock时钟,对比curr上次的时钟获得delta,并更新到curr和rq结构体中,最后更新curr的时间节点,注意这里的curr指的是调度entity,而不是task本身。
需要注意的是,对于加权前后的delta时间都需要更新。
void update_curr(struct cfs_rq *cfs_rq)
{
struct sched_entity *curr = cfs_rq->curr;
now = rq_of(cfs_rq)->clock_task;
delta_exec = (unsigned long)(now - curr->exec_start);
__update_curr(cfs_rq, curr, delta_exec);
/* 对调度实体的rq 和curr 进行已运行时间更新 */
curr->exec_start = now;
/*如果调度实例是进程,当然大概率是,则执行一下代码*/
struct task_struct *curtask = task_of(curr);
cpuacct_charge(curtask, delta_exec);
account_group_exec_runtime(curtask, delta_exec);
}
CFS核心按照调度实体的vruntime和rq的vruntime进行差值排序,换句话说,就是最应该要运行的,最先运行
return se->vruntime - cfs_rq->min_vruntime, 注意rq的vruntime会随着进程调度而进行更新(实际是当前队列中所有节点的最小的vruntime,当最小的vruntime被抽调执行的时候,则rq上的最小增大,从而rq的min_vruntime增大), vruntime则仅仅在进程执行后更新。
vruntime与权重有关,nice越小,权重越大,则vruntime更新越慢。
rq的min_vruntime和当前进程的min_runtime进行对比,需要注意的是,当进程被唤醒的时候,进程的最小值被赋值为当前的rq的min_vruntime。
时间片分配也是一样,按照权重进行分配,具体公式不再列出。
CFS的核心操作及相关逻辑
延迟跟踪:表示在一定的周期内,保证所有的当前进程全部都执行一遍。(执行时间当然和权重有关系)
enqueue
如果当前已经在队列上,则不执行。否则将se加入队列。本质调用函数:enqueue_entity,并最终调红黑树函数__enqueue_entity
pick_next_task_fair
简单来说,选择最左,注意的是,选择以后从红黑树删除,但是被curr指针指向
并且会更新prev_sum_exec_runtime(上一次调度时的已经执行的时总和),与sum_exec_runtime的差值决定了当前函数实际运行的时间。
需要注意的是,sum_exec_runtime的更新时间为__update_curr,每一个时钟周期
entity_tick
周期检查函数,一如既往的update_curr来更新统计量,并使用check_preempt_tick做抢占。
如果检查到curr的运行时间超过应该运行时间,也就是分配的时间片长度,则触发rsched_task()
唤醒抢占
唤醒实时进程,立即抢占。唤醒BATCH进程,不抢占。唤醒普通进程,需要确保当前进程已经运行了足够小的时间限额避免多次切换。
新进程
与原文不同,实际最新的函数为task_fork_fair(),本质上,更新update_curr,place_entity其中flag=1表示新插入。
本质上,将新的se(子)增加一个应该分配的时间片,从而完成最初的vruntime初始化,并将其插入队列
根据flag通常子进程优先于父进程执行,所以将curr(父)和se(子)的runtime对调,如上文,子进程的runtime增加了一个time_slice,所以应该要更大,交换后则子进程优先执行,并对当前curr执行 resched_task操作
实时调度进程
实时比较简单,需要注意的就是优先级链表用于保存所有优先级的实时进程,全部按照实时时间来计算,因为实时性有绝对的优先级设置。
调度器增强
SMP调度
CPU负荷共享,亲和性设置(affinity),进程CPU迁移(但是需要注意缓存问题)
调度器类需要增加两个额外接口: load_balance, move_one_task。 较新的代码已经修改为了如下几个接口
int (*select_task_rq)(struct rq *rq, struct task_struct *p,
int sd_flag, int flags);
void (*pre_schedule) (struct rq *this_rq, struct task_struct *task);
void (*post_schedule) (struct rq *this_rq);
void (*task_waking) (struct rq *this_rq, struct task_struct *task);
void (*task_woken) (struct rq *this_rq, struct task_struct *task);
void (*set_cpus_allowed)(struct task_struct *p,
const struct cpumask *newmask);
void (*rq_online)(struct rq *rq);
void (*rq_offline)(struct rq *rq);
核心函数为select_task_rq,为当前的p选择一个新的task的rq队列,也就是CPU ID。
本质上,load_balance通过计算不同的队列上的繁忙程度,选择当前繁忙的队列上的task到其他的空闲的CPU上去运行。
需要说明的是,其触发方式为软中断。
sched_init函数中,通过注册软中断 open_softirq(SCHED_SOFTIRQ, run_rebalance_domains);来进行rebalance函数注册。
scheduler_tick时钟函数会触发trigger_load_balance,经过一定周期后,触发软中断raise_softirq(SCHED_SOFTIRQ);
迁移线程migration_thread用于轮询当前是否需要进行balance计算,注意还记得吗?rq的load_weight存放了当前的running的权重和,所以检查这个参数即可。
SD : sched_domain,可以有效的对用户组和group的概念上进行计算
注意,执行exec的函数和sched_migrate_task。
内核抢占和低延迟
对于多核抢占危险边界,需要使用自旋锁。
对于可能访问同一个临界区的核心态操作,需要停止内核抢占。
preempt_count, 0 表示可抢占,小于0则此时不能抢占。
内核在重新启用抢占前,必须要保证已经离开了所有的临界区
preempt_disable, preempt_enable,重新启用enable的时候通常需要调用preempt_check_resched检查重新调度
总的来说,irq_disable的时候以及preempt_disable的时候都不会进行抢占,并且在恢复时尝试进行切换。
如果进行的是内核抢占恢复时检查,则可以进行preempt_active置位,表示是因为内核抢占导致的调度,则跳过将当前进程deactivate的操作。
低延迟
耗时长的操作,应该通过插入cond_reched来尝试进行调度从而保证了其他的进程可以得到不错的内核响应时间。