写在前面
前两天写了一篇关于进程调度的博客.
准确来说是读书笔记.参考的是<<深入理解linux内核>>
本来还是蛮顺畅的.当我去尝试了解CFS的时候 ,发现事情有点不对劲. 后来在网上查啊查,找啊找,最后还是想不通. 最后迫于无奈,又去看了各种各样的书. 昨天我买的<<linux内核深度解析>>邮过来了.画了一天时间,对进程调度又做了一点了解.
本篇博客也是一篇读书笔记., << linux内核深度解析>>第二章
相对于上一篇博客:我自认为是有一点进一步认识的.
码龄很短, 很多东西我还是有点看不懂,总结一个大概的步骤.以后勤翻书
回顾
上一篇博客中 专门讲述了一下调度器,调度类 相关算法
进程优先级
- 实时进程的优先级: 1~ 99 优先级数值越大,表示优先级越高
- 普通进程的静态优先级: 100 ~ 139 : 数值越大优先级越小
include/linux/sched.h
struct task_struct {
...
int prio, static_prio, normal_prio;
unsigned int rt_priority;
..
}
①PRIO: 调度优先级
大多数情况下 prio=normal_prio
①STAIC_PRIO: 静态优先级,只和普通进程有关
限期进程和实时进程该值都为0
①NORMAL_PRIO: 正常优先级
- 限期进程的正常优先级为 -1
- 实时进程: normal_prio= 99 - rt_priority
- 普通进程 normal_prio= staic_prio
①RT_PRIORITY: 实时优先级,只和实时进程有关
进程调度策略
- 限期进程:使用限期调度策略 ( SCHED_DEADLINE)(“死线”)
SCHED_DEADLINE:
- 实时进程:① 先来先服务(SCHED_FIFO)②时间片轮转(SCHED_RR)
- 普通进程: 标准分时调度(SCHED_NORMAL) ----> CFS完全公平调度策略
调度类
linux 抽象了一个调度类的 sched_class
kernel/sched/sched.h
struct sched_class {
const struct sched_class *next;
void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
void (*yield_task) (struct rq *rq);
bool (*yield_to_task) (struct rq *rq, struct task_struct *p, bool preempt);
void (*check_preempt_curr) (struct rq *rq, struct task_struct *p, int flags);
struct task_struct * (*pick_next_task) (struct rq *rq);
void (*put_prev_task) (struct rq *rq, struct task_struct *p);
#ifdef CONFIG_SMP
int (*select_task_rq)(struct task_struct *p, int task_cpu, int sd_flag, int flags);
void (*migrate_task_rq)(struct task_struct *p, int next_cpu);
void (*pre_schedule) (struct rq *this_rq, struct task_struct *task);
void (*post_schedule) (struct rq *this_rq);
RH_KABI_DEPRECATE_FN(void, task_waking, 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);
#endif
void (*set_curr_task) (struct rq *rq);
void (*task_tick) (struct rq *rq, struct task_struct *p, int queued);
void (*task_fork) (struct task_struct *p);
/*
* The switched_from() call is allowed to drop rq->lock, therefore we
* cannot assume the switched_from/switched_to pair is serliazed by
* rq->lock. They are however serialized by p->pi_lock.
*/
void (*switched_from) (struct rq *this_rq, struct task_struct *task);
void (*switched_to) (struct rq *this_rq, struct task_struct *task);
void (*prio_changed) (struct rq *this_rq, struct task_struct *task,
int oldprio);
unsigned int (*get_rr_interval) (struct rq *rq,
struct task_struct *task);
#ifdef CONFIG_FAIR_GROUP_SCHED
void (*task_move_group) (struct task_struct *p, int on_rq);
#endif
RH_KABI_EXTEND(void (*update_curr) (struct rq *rq))
RH_KABI_EXTEND(void (*task_dead) (struct task_struct *p))
};
根据这个抽象的调度类 sched_class 实现了 5种调度类
这五种调度类也有调度顺序: 停机调度类 —>限期调度类 —>实时调度类 —> 公平调度类—> 空闲调度类
某个调度类的具体实现: kernel/sched/stop_task 停机调度类
*/
const struct sched_class stop_sched_class = {
.next = &dl_sched_class,
.enqueue_task = enqueue_task_stop,
.dequeue_task = dequeue_task_stop,
.yield_task = yield_task_stop,
.check_preempt_curr = check_preempt_curr_stop,
.pick_next_task = pick_next_task_stop,
.put_prev_task = put_prev_task_stop,
#ifdef CONFIG_SMP
.select_task_rq = select_task_rq_stop,
#endif
.set_curr_task = set_curr_task_stop,
.task_tick = task_tick_stop,
.get_rr_interval = get_rr_interval_stop,
.prio_changed = prio_changed_stop,
.switched_to = switched_to_stop,
.update_curr = update_curr_stop,
};
停机调度类
停机调度类是优先级最高的进程,可以抢占任何其他进程. 停机的意思是指使处理器停下来 , 不是关机
停机调度类 没有时间片,如果它不主动让出处理器,那么他将一直霸占处理器
迁移线程 属于 停机调度类 迁移线程 优先级为 99
迁移线程的优先级必须必实时进程的优先级高
限期调度类
限期调度类 使用 最早期限优先算法,使用红黑树,把进程按照绝对截止期限从小到大排序,每次调度室选择绝对截止期限最小的进程
kernel/sched/deadline.c
static struct sched_dl_entity *pick_next_dl_entity(struct rq *rq,
struct dl_rq *dl_rq)
{
struct rb_node *left = dl_rq->rb_leftmost; 红黑树
if (!left)
return NULL;
return rb_entry(left, struct sched_dl_entity, rb_node);
}
实时调度类
实时调度类为每一个调度优先级维护一个优先级队列
kernel/sched/rt.c
实时调度实体
kernel/sched/sched.h
- bitmap用来快速查找第一个非空队列
- 数组 queue的下表代表进程优先级
实时进程每次调度选取优先级最高的第一个非空队列.
SCHED_FIFO:先来先服务, 一直霸占
SCHED_RR:时间片轮转, 用完时间片之后 加入队列的尾部
实时进程的优先度 是一定大于普通进程的: 实时进程抢占普通进程
公平调度类
公平调度类使用 完全公平调度算法(暨CFS)
公平调度算法 服务的是普通进程
const struct sched_class fair_sched_class = {
.next = &idle_sched_class,
.enqueue_task = enqueue_task_fair,
.dequeue_task = dequeue_task_fair,
.yield_task = yield_task_fair,
.yield_to_task = yield_to_task_fair,
.check_preempt_curr = check_preempt_wakeup,
.pick_next_task = pick_next_task_fair,
.put_prev_task = put_prev_task_fair,
#ifdef CONFIG_SMP
.select_task_rq = select_task_rq_fair,
#ifdef CONFIG_FAIR_GROUP_SCHED
.migrate_task_rq = migrate_task_rq_fair,
#endif
.rq_online = rq_online_fair,
.rq_offline = rq_offline_fair,
#endif
.set_curr_task = set_curr_task_fair,
.task_tick = task_tick_fair,
.task_fork = task_fork_fair,
.prio_changed = prio_changed_fair,
.switched_from = switched_from_fair,
.switched_to = switched_to_fair,
.get_rr_interval = get_rr_interval_fair,
.update_curr = update_curr_fair,
#ifdef CONFIG_FAIR_GROUP_SCHED
.task_move_group = task_move_group_fair,
#endif
};
CFS核心
优秀博客:http://www.wowotech.net/process_management/447.html
完全公平调度算法引入了虚拟运行时间的概念(暨vruntime) ,将进程优先级这个概念弱化,而是强调进程的权重。一个进程的权重越大,则说明这个进程更需要运行,因此它的虚拟运行时间就越小,这样被调度的机会就越大
vruntime
vruntime记录着进程已经运行的时间,但是并不是直接记录,而是要根据进程的权重将运行时间放大或者缩小一个比例
vruntime= 实际运行时间 * 1024/ 进程的权重
const u32 sched_prio_to_wmult[40] = {
/* -20 */ 48388, 59856, 76040, 92818, 118348,
/* -15 */ 147320, 184698, 229616, 287308, 360437,
/* -10 */ 449829, 563644, 704093, 875809, 1099582,
/* -5 */ 1376151, 1717300, 2157191, 2708050, 3363326,
/* 0 */ 4194304, 5237765, 6557202, 8165337, 10153587,
/* 5 */ 12820798, 15790321, 19976592, 24970740, 31350126,
/* 10 */ 39045157, 49367440, 61356676, 76695844, 95443717,
/* 15 */ 119304647, 148102320, 186737708, 238609294, 286331153,
};
vruntime 决定着 到底调用哪个普通进程
但是vruntime 并不决定如何分配cpu时间片
vruntime是一个累积的值
时间片
时间片计算公式:
进程时间片=(调度周期 * 进程的权重/运行队列中所有进程的权重的总和)
什么是调度周期
在某个时间长度可以保证队列中的每个进程至少运行一次, 我们把这个时间长度成为调度周期
if 运行对垒中的进程数量 > 8
调度周期==最小运行粒度(0.75ms)*进程数量
else
调度周期=6ms
关于cfs底层的红黑树
cfs使用红黑树,把进程按照虚拟运行时间从小到大排序,每次选择虚拟运行时间最小的进程
调度实体
红黑树的根通过rb_root元素通过cfs_rq结构(kernel/sched.c)引用。红黑树的叶子不包含信息,但是内部节点代表一个或者多个可运行的task。红黑树的每个节点都用rb_node表示,包含子引用和父对象的颜色。re_node包含在sched_entity结构中,该结构包含rb_node引用、负载权重以及各种统计数据。最重要的是,sched_entity包含vruntime(64位字段),它表示任务运行的时间量,并作为红黑树的索引。 最后,task_struct位于顶端,它完整地描述任务并包含 sched_entity 结构。
空闲调度类
每个处理器上都有一个空闲线程,及0号线程 .空闲调度类的优先级最低. 只有当其他进程都可以调度的时候 才会使用空闲线程
后续施工ing,要熄灯断电了
调度实体
调度器调度的对象成为调度实体
调度实体包含: 进程和任务组
每一个task_struct里面都会有三个调度实体
dl: 限期调度实体
rt:实时调度实体
sched_entity: 公平调度类调度实体
调度时机
进程调度的核心函数时 __ schedule()
static void __sched __schedule(void)
{
struct task_struct *prev, *next;
unsigned long *switch_count;
struct rq *rq;
int cpu;
need_resched:
preempt_disable();
cpu = smp_processor_id();
rq = cpu_rq(cpu);
rcu_note_context_switch(cpu);
prev = rq->curr;
schedule_debug(prev);
if (sched_feat(HRTICK))
hrtick_clear(rq);
/*
* Make sure that signal_pending_state()->signal_pending() below
* can't be reordered with __set_current_state(TASK_INTERRUPTIBLE)
* done by the caller to avoid the race with signal_wake_up().
*
* The membarrier system call requires a full memory barrier
* after coming from user-space, before storing to rq->curr.
*/
raw_spin_lock_irq(&rq->lock);
smp_mb__after_spinlock();
switch_count = &prev->nivcsw;
if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
if (unlikely(signal_pending_state(prev->state, prev))) {
prev->state = TASK_RUNNING;
} else {
deactivate_task(rq, prev, DEQUEUE_SLEEP);
prev->on_rq = 0;
/*
* If a worker went to sleep, notify and ask workqueue
* whether it wants to wake up a task to maintain
* concurrency.
*/
if (prev->flags & PF_WQ_WORKER) {
struct task_struct *to_wakeup;
to_wakeup = wq_worker_sleeping(prev, cpu);
if (to_wakeup)
try_to_wake_up_local(to_wakeup);
}
}
switch_count = &prev->nvcsw;
}
pre_schedule(rq, prev);
if (unlikely(!rq->nr_running))
idle_balance(cpu, rq);
put_prev_task(rq, prev);
next = pick_next_task(rq);
clear_tsk_need_resched(prev);
rq->skip_clock_update = 0;
if (likely(prev != next)) {
rq->nr_switches++;
rq->curr = next;
/*
* The membarrier system call requires each architecture
* to have a full memory barrier after updating
* rq->curr, before returning to user-space.
*
* Here are the schemes providing that barrier on the
* various architectures:
* - mm ? switch_mm() : mmdrop() for x86, s390, sparc, PowerPC.
* switch_mm() rely on membarrier_arch_switch_mm() on PowerPC.
* - finish_lock_switch() for weakly-ordered
* architectures where spin_unlock is a full barrier,
* - switch_to() for arm64 (weakly-ordered, spin_unlock
* is a RELEASE barrier),
*/
++*switch_count;
context_switch(rq, prev, next); /* unlocks the rq */
/*
* The context switch have flipped the stack from under us
* and restored the local variables which were saved when
* this task called schedule() in the past. prev == current
* is still correct, but it can be moved to another cpu/rq.
*/
cpu = smp_processor_id();
rq = cpu_rq(cpu);
} else
raw_spin_unlock_irq(&rq->lock);
post_schedule(rq);
sched_preempt_enable_no_resched();
if (need_resched())
goto need_resched;
}
_schedule()函数关键两步
- pick_next_next()选择下一个进程
- context_switch()切换进程
那么调度实体在何时被调度呢?
调度时机
- 主动调度: 主动调用schedule()
①直接调用_schedule()
②调用有条件重调度函数 cond_resched()
③如果需要等待某个资源,则把今晨刚设置为睡眠状态,然后调用schedule() - 周期调度:周期想的调度,抢占当前进程,强迫当前进程让出处理器
- 唤醒时抢占:被唤醒的实体可能会抢占
①如果被唤醒实体和当前实体属于相同调度类. 那么调用调度类的check_preempt_curr方法检查是否可以抢占
②如果被唤醒的进程所属的调度类的优先级高于当前进程所属的调度类,则给当前进程设置需要重新调度的标志 - 创建新进程的时候抢占
- 内核抢占