[TOC]
## 多任务操作系统的两种形式:
1. 抢占式多任务操作系统
2. 协同式多任务操作系统
## 进程分为:
1. I/O繁忙
"I/O"意味着任意类型的阻塞资源。例如键盘输入或者网络I/O,而不仅仅是磁盘I/O。大多数的GUI应用是高I/O的,即使他们并没有花费很多的时间在磁盘读写上,它们大部分时间花费在等待用户的键盘或鼠标输入上。
2. CPU繁忙
高CPU意味着花费大量的时间在执行代码。它们一直运行直到被抢占。例如一个执行无线循环的程序,ssh-keygen,MATLAB.
Linux,旨在提供很好的交互响应和桌面表现,优化了进程响应,因此相较于CPU忙的进程更偏爱I/O忙的进程。
## 进程优先级
基于优先级的调度是一种典型的调度算法。即拥有高优先级的进程比低优先级的进程先执行,相同优先级的进程间round-robin(轮训)。
Linux内核实现了两种不同的优先级范围:
- nice value:从-20到+19的整数。
- 更小的值拥有更高的优先级
- linux系统使用nice值控制时间片比例。其他的基于Unix的系统,如Mac OS X,则是基于nice值来调整绝对时间片的分配数量。
- ps -el命令可以查看进程的nice值。
- Real-time 优先级:范围从0-99
- 数值更大优先级更高
- 所有的实时进程比普通进程拥有更高的优先级
- ps -eo state,uid,pid,ppid,rtprio,time,comm 命令可以查看进程的实时优先级。“-”表示该进程不是实时进程。
Linux系统上的时间片:
Linux的CFS调度没有直接给进程分配时间片,而是进程在cpu上执行时间的比例。进程的执行时间比例受每一个进程的nice值影响。nice值如同一个权值,动态调整进程获得的cpu时间。nice值越大的进程优先级越低,因而获得的执行时间越少。
## 调度策略
```
/*
* Scheduling policies
*/
#define SCHED_NORMAL 0
#define SCHED_FIFO 1
#define SCHED_RR 2
#define SCHED_BATCH 3
/* SCHED_ISO: reserved but not implemented yet */
#define SCHED_IDLE 5
#define SCHED_DEADLINE 6
```
## Linux的进程调度算法
### 调度类
不同的调度类实现了不同的调度算法,用于满足不同类型的进程。
内核里面包含了一下几种调度类实现:
> dl_sched_class
fair_sched_class
idle_sched_class
rt_sched_class
stop_sched_class
普通进程注册CFS调度类,通过SCHED_NORMAL宏。
```c
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,
.task_waking = task_waking_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为其近似无线小的调度周期设置了一个target。这个target被称为调度延迟(targeted latency)。CFS设置了一个CPU执行时间的最小粒度,默认为1毫秒。即存在无线个进程,每一个的最小执行时间也是1毫秒。
## Linux内核调度实现
CFS的实现代码在kernel/sched_fair.c文件中。主要实现CFS的4个组成部分:
- 时间统计
- 进程选择
- 调度进入点
- 休眠和唤醒
### Time Accounting
CFS使用调度实体结构体,struct sched_entity,来记录time accounting,结构体在<linux/sched.h>文件中定义。
```c
struct sched_entity {
struct load_weight load; /* for load-balancing */
struct rb_node run_node;
struct list_head group_node;
unsigned int on_rq;
u64 exec_start;
u64 sum_exec_runtime;
u64 vruntime;
u64 prev_sum_exec_runtime;
u64 nr_migrations;
#ifdef CONFIG_FAIR_GROUP_SCHED
struct sched_entity *parent;
/* rq on which this entity is (to be) queued: */
struct cfs_rq *cfs_rq;
/* rq "owned" by this entity/group: */
struct cfs_rq *my_q;
#endif
/*
* Load-tracking only depends on SMP, FAIR_GROUP_SCHED dependency below may be
* removed when useful for applications beyond shares distribution (e.g.
* load-balance).
*/
#if defined(CONFIG_SMP) && defined(CONFIG_FAIR_GROUP_SCHED)
/* Per-entity load-tracking */
struct sched_avg avg;
#endif
RH_KABI_USE(1, struct sched_statistics *statistics)
/* reserved for Red Hat */
RH_KABI_RESERVE(2)
RH_KABI_RESERVE(3)
RH_KABI_RESERVE(4)
};
```
调度实体作为进程结构体(struct task_struct)中的一个成员变量se。
优先级nice值与权值得映射关系定义在*prio_to_weight*中。计算公式为:weight=1024/(1.25)^(nice)。
**virtual runtime**
vruntime存储一个进程的虚拟运行时间,即实际运行时间由可运行的进程数量加权计算获得。可用来衡量一个进程运行了多长时间以及多久未运行。
函数*update_curr()*用来计算当前进程的执行时间,定义在kernel/sched_fair.c文件中。
```
/*
* Update the current task's runtime statistics.
*/
static void update_curr(struct cfs_rq *cfs_rq)
{
struct sched_entity *curr = cfs_rq->curr;
u64 now = rq_clock_task(rq_of(cfs_rq));
u64 delta_exec;
if (unlikely(!curr))
return;
delta_exec = now - curr->exec_start;
if (unlikely((s64)delta_exec <= 0))
return;
curr->exec_start = now;
schedstat_set(curr->statistics->exec_max,
max(delta_exec, curr->statistics->exec_max));
curr->sum_exec_runtime += delta_exec;
schedstat_add(cfs_rq, exec_clock, delta_exec);
curr->vruntime += calc_delta_fair(delta_exec, curr);
update_min_vruntime(cfs_rq);
if (entity_is_task(curr)) {
struct task_struct *curtask = task_of(curr);
trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);
cpuacct_charge(curtask, delta_exec);
account_group_exec_runtime(curtask, delta_exec);
}
account_cfs_rq_runtime(cfs_rq, delta_exec);
}
```
update_curr()计算当前进程的执行时间存储在*delta_exec*中。然后传递给__update_curr(),其使用可运行的进程数量进行加权计算。最后当前进程的vruntime值为之前的值再加上加权计算的值。
```
/*
* Update the current task's runtime statistics. Skip current tasks that
* are not in our scheduling class.
*/
static inline void
__update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr,
unsigned long delta_exec)
{
unsigned long delta_exec_weighted;
schedstat_set(curr->exec_max, max((u64)delta_exec, curr->exec_max));
curr->sum_exec_runtime += delta_exec;
schedstat_add(cfs_rq, exec_clock, delta_exec);
delta_exec_weighted = calc_delta_fair(delta_exec, curr);
curr->vruntime += delta_exec_weighted;
update_min_vruntime(cfs_rq);
}
```
update_curr()被系统定时器调用,以及当一个进程变为可执行或者阻塞或者挂起的时候。因此,vruntime可以最为一个进程运行时间和下一个执行进程的精确衡量标准。
### 进程选择
CFS balance一个进程的vruntime使用一下简单的规则:当决定下一个执行进程的时候,挑选具有最小vruntime的进程执行。
CFS使用红黑树管理可执行进程,内核红黑树定义在include/linux/rbtree.h,lib/rbtree.c。
```
static struct sched_entity *__pick_next_entity(struct sched_entity *se)
{
struct rb_node *next = rb_next(&se->run_node);
if (!next)
return NULL;
return rb_entry(next, struct sched_entity, run_node);
}
```
__pick_next_entity(),不同的内核版本对于该函数的实现不太一样,我使用的内核版本直接遍历了红黑树,而有些版本是缓存在rb_leftmost变量里的。这里遍历红黑树的代价并不高,红黑树遍历的算法复杂度为O(logN),N是节点数量。
该函数的返回值是CFS下一个将要执行的进程。如果返回NULL,表明最左边的节点为空。这种情况说明没有可运行的进程,因此CFS会调度空闲任务。
### 添加一个进程
当一个进程变成可执行状态(唤醒)或者通过fork()首次创建的时候,CFS会调用*enqueue_entity()*将其添加到红黑树中。
```
static void
enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
/*
* Update the normalized vruntime before updating min_vruntime
* through callig update_curr().
*/
if (!(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_WAKING))
se->vruntime += cfs_rq->min_vruntime;
/*
* Update run-time statistics of the 'current'.
*/
update_curr(cfs_rq);
enqueue_entity_load_avg(cfs_rq, se, flags & ENQUEUE_WAKEUP);
account_entity_enqueue(cfs_rq, se);
update_cfs_shares(cfs_rq);
if (flags & ENQUEUE_WAKEUP) {
place_entity(cfs_rq, se, 0);
if (schedstat_enabled())
enqueue_sleeper(cfs_rq, se);
}
check_schedstat_required();
if (schedstat_enabled()) {
update_stats_enqueue(cfs_rq, se);
check_spread(cfs_rq, se);
}
if (se != cfs_rq->curr)
__enqueue_entity(cfs_rq, se);
se->on_rq = 1;
if (cfs_rq->nr_running == 1) {
list_add_leaf_cfs_rq(cfs_rq);
check_enqueue_throttle(cfs_rq);
}
}
```
该函数会调用实际的插入操作函数*__enqueue_entity()*。
```
/*
* Enqueue an entity into the rb-tree:
*/
static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
struct rb_node **link = &cfs_rq->tasks_timeline.rb_node;
struct rb_node *parent = NULL;
struct sched_entity *entry;
int leftmost = 1;
/*
* Find the right place in the rbtree:
*/
while (*link) {
parent = *link;
entry = rb_entry(parent, struct sched_entity, run_node);
/*
* We dont care about collisions. Nodes with
* the same key stay together.
*/
if (entity_before(se, entry)) {
link = &parent->rb_left;
} else {
link = &parent->rb_right;
leftmost = 0;
}
}
/*
* Maintain a cache of leftmost tree entries (it is frequently
* used):
*/
if (leftmost)
cfs_rq->rb_leftmost = &se->run_node;
rb_link_node(&se->run_node, parent, link);
rb_insert_color(&se->run_node, &cfs_rq->tasks_timeline);
}
```