Multitasking
多任务操作系统一般有两种,一种是抢占式另一种是非抢占式。在抢占式操作系统中,由调度器决定什么时候一个进程开始运行和终止运行。在非抢占式操作系统中,一个进程一旦开始运行就只能由它自己自愿地终止。linux和现在大多数的操作系统都是可抢占的多任务操作系统。
Linux's Process Scheduler
linux调度器的发展:
O(1)调度器 => RSDL调度器 => CFS(完全公平调度器)
Policy
1.I/O-Bound Versus Processor-Bound Processes
进程可以被分为I/O密集型和计算密集型两种。调度策略必须满足两个互相矛盾的目标:
1.低延迟(更快的进程反应时间) 2.高吞吐量(最大化系统利用率)。
2.Process Priority and Timeslice
一种常见的调度方式是基于优先级的调度,优先级高的进程一般优先执行并且可以获得更多的时间片。
linux内核实现了两种分离的优先级范围:
1. nice值: -20 ~ 19 ,初始化为 0 ,值越高优先级越低。该值也决定了时间片的比例。可以使用命令ps -el查看每个进程NI(nice值)
2. 实时优先级:0 ~ 99 所有的实时进程都比普通进程的优先级高,值越高优先级越高。
长的时间片会导致系统交互性的降低,因此linux的CFS调度器只赋予进程一个处理器占用的比例,而不是一个直接的时间。
在一般的系统中,进程是否可以获得处理机是由的优先级和是否由时间片来决定的,在linux的CFS调度策略中,则取决于该进程消耗的处理机使用比,消耗的少的进程优先获得处理机。
3.The Scheduling Policy in Action
这一段看了两遍还是迷糊,去看翻译版的一下就懂了,英语还是渣啊。
我们是这样考虑的,一般来说,文本处理所占的处理机时间肯定是比视频解码少的,假设系统给两个进程分别赋予了50%的处理机使用比。那么一旦系统接受到键盘的中断信号并进行处理后,便会发现,虽然系统给它的处理机使用比是50%,但是实际使用的比率却是相当的低的,为了保证公平,调度器优先调度文本处理进程运行。
想象一下公平调度的所谓公平其实是基于“处理器赋予进程的处理机使用比”以及“进程实际消耗的处理机使用比”这两者来实现的。
The Linux Scheduling Algorithm
linux的调度算法是模块化的,使得调度器可以用不同的调度算法来调度不同的进程。
每一个这样的模块叫做一个调度器类(scheduler class),并且也有一个优先级。
实际的代码是通过按优先级的顺序遍历调度器类,优先级高的并且存在可运行进程的调度器类决定获得处理机的进程。
1.Process Scheduling in Unix Systems
Unix系统通过nice值来决定优先级和时间片,这样做存在一些缺陷:
作者这一节花了大量篇幅来介绍Unix在这方面的弊端,读起来劳累的不行T_T。
总之就是通过nice值映射固定的时间片这个做法不合理!不恰当!图样!
然后下面就开始介绍碉堡了的Linux调度器实现,也就是 ——分配一个处理机使用比率的CFS调度。。
2.Fair Scheduling
上面几小节不停的在说要分配一个比例,而不是一个值。那么好,我们现在就分配一个比例,但是现在存在一个问题,光有比例是不行的,实际运行中还是需要一个实际的时间片。CFS用了一个叫做tageted latency来表示最小的值,想象一旦这个值很小的话,系统在不停的进程间切换就看起来好像真是在“并行”了。
但是这就产生了下面两个问题
1.进程切换是有开销的,tageted latency越小,花在进程切换上的时间就越大。
2.一旦进程数量变多,tageted latency被进程平均分配。可能不是很小tageted latency也会消耗很多的时间在进程切换上。
所以CFS设了最小时间片叫做minimum granularity,默认为1ms。
The Linux Scheduling Implementation
CFS调度的四个主要元素:
1. Time Accounting
像Unix系统一样,给每个进程赋予一个时间片,时钟tick一下,时间片就递减一下,一旦时间片降为0,该进程就被其他时间片非0且可运行的进程抢占。
这里可能又要有问题了,不是说不要用时间片吗?为什么又要了?
实际上虽然CFS没有时间片的概念,但是还是需要维护一个每个进程运行的”时间记账“。
引入嵌入在task_struct中的一个数据结构,:
struct sched_entity {
struct load_weight load;
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 last_wakeup;
u64 avg_overlap;
u64 nr_migrations;
u64 start_runtime;
u64 avg_wakeup;
};
看到这里关于vruntime怎么计算的概念很模糊,也就是最核心的部分,暂且略过这一段,以后再来,代码先放这。
static void update_curr(struct cfs_rq *cfs_rq)
{
struct sched_entity *curr = cfs_rq->curr;
u64 now = rq_of(cfs_rq)->clock_task;
unsigned long delta_exec;
if (unlikely(!curr))
return;
/*
* Get the amount of time the current task was running
* since the last time we changed load (this cannot
* overflow on 32 bits):
*/
delta_exec = (unsigned long)(now - curr->exec_start);
if (!delta_exec)
return;
__update_curr(cfs_rq, curr, delta_exec);
curr->exec_start = now;
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);
}
}
/*
* 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);
}
2. Process Selection
当CFS决定哪个进程运行的时候,选择vruntime最小的那个。
那么具体怎么实现的呢?
CFS使用红黑树来维护一组可运行进程,CFS选择树上最左边的那个叶子节点进程。
static struct sched_entity *__pick_next_entity(struct cfs_rq *cfs_rq)
{
struct rb_node *left = cfs_rq->rb_leftmost;
if (!left)
return NULL;
return rb_entry(left, struct sched_entity, run_node);
}
好吧我发现了奇怪的地方,cfs_rq->rb_leftmost..我去这是啥玩意儿?不是应该递归搜索到最左边的节点么?
实际上最左边的节点已经被cfs_rq缓存了,所以只要这一步。
rb_entry()的返回值就是接下来要运行的进程。
接下来关注一下新进程创建时是如何添加到红黑树上的以及cfs_rq是怎么缓存这个最左边叶节点的。
#define ENQUEUE_WAKEUP 1
#define ENQUEUE_MIGRATE 2
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_MIGRATE))
se->vruntime += cfs_rq->min_vruntime;
/*
* Update run-time statistics of the 'current'.
*/
update_curr(cfs_rq);
account_entity_enqueue(cfs_rq, se);
if (flags & ENQUEUE_WAKEUP) {
place_entity(cfs_rq, se, 0);
enqueue_sleeper(cfs_rq, se);
}
update_stats_enqueue(cfs_rq, se);
check_spread(cfs_rq, se);
if (se != cfs_rq->curr)
__enqueue_entity(cfs_rq, se);
}
/*
* 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;
s64 key = entity_key(cfs_rq, se);
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 (key < entity_key(cfs_rq, 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);
}
rb_insert_color()是红黑树维护平衡的基本操作。
要特别注意leftmost标记以及维护缓存的方法。
下面是删除进程的函数,发生在进程阻塞或者终止的时候:
static void
dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int sleep)
{
/*
* Update run-time statistics of the 'current'.
*/
update_curr(cfs_rq);
update_stats_dequeue(cfs_rq, se);
if (sleep) {
#ifdef CONFIG_SCHEDSTATS
if (entity_is_task(se)) {
struct task_struct *tsk = task_of(se);
if (tsk->state & TASK_INTERRUPTIBLE)
se->sleep_start = rq_of(cfs_rq)->clock;
if (tsk->state & TASK_UNINTERRUPTIBLE)
se->block_start = rq_of(cfs_rq)->clock;
}
#endif
}
clear_buddies(cfs_rq, se);
if (se != cfs_rq->curr)
__dequeue_entity(cfs_rq, se);
account_entity_dequeue(cfs_rq, se);
update_min_vruntime(cfs_rq);
/*
* Normalize the entity after updating the min_vruntime because the
* update can refer to the ->curr item and we need to reflect this
* movement in our normalized position.
*/
if (!sleep)
se->vruntime -= cfs_rq->min_vruntime;
}
和插入类似,更新runtime,然后接下来才是删除节点的操作:
static void __dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
if (cfs_rq->rb_leftmost == &se->run_node) {
struct rb_node *next_node;
next_node = rb_next(&se->run_node);
cfs_rq->rb_leftmost = next_node;
}
rb_erase(&se->run_node, &cfs_rq->tasks_timeline);
}
过程主要是对缓存的更新,如果删除的恰好是缓存的节点,那么选择下一个中序遍历的节点作为新的缓存节点。
3. The Scheduler Entry Point
主要是介绍了一个函数,选择最高优先级调度器类中最高优先级的那个进程。如果最高优先级调度器类中没有可运行的进程,那么就选择次高的,这样迭代寻找下去。
/*
* Pick up the highest-prio task:
*/
static inline struct task_struct *
pick_next_task(struct rq *rq)
{
const struct sched_class *class;
struct task_struct *p;
/*
* Optimization: we know that if all tasks are in
* the fair class we can call that function directly:
*/
if (likely(rq->nr_running == rq->cfs.nr_running)) {
p = fair_sched_class.pick_next_task(rq);
if (likely(p))
return p;
}
class = sched_class_highest;
for ( ; ; ) {
p = class->pick_next_task(rq);
if (p)
return p;
/*
* Will never be NULL as the idle class always
* returns a non-NULL p:
*/
class = class->next;
}
}
函数分为两个部分,第一个部分是一个优化,即如果所有的可运行进程都在当前调度器类中(rq->nr_running == rq->cfs.nr_running,那么直接选择该调度器类中优先级最高的进程返回(fair_sched_class.pick_next_task(rq))。如果不存在返回NULL或者两者不相等,那么继续下一步:
以优先级降低的顺序遍历每个调度器类,找到存在可运行进程的调度器类并返回其中优先级最高的进程。
4. Sleeping and Waking UP
这段的意思就是说进程状态的改变会对等待队列和上面所提到的红黑树产生影响,一个进程一旦从sleep状态wake up ,那么就把它从等待队列中dequeue,然后插入到红黑树中,而进程一旦阻塞的话就进行相反的操作。
下面介绍等待队列:
等待队列在内核中一般被表示为wait_queue_head_t(等待队列头),可以通过DECLARE_WAITQUEUE()静态的创建或者init_waitqueue_head()动态的创建。
下面进队的操作
DEFINE_WAIT(wait);
add_wait_queue( q, &wait);
while (!condition) {
prepare_to_wait(&q,&wait,TASK_INTERRUPTIBLE);
if (signal_pending(current))
/* handle signal */
schedule();
}
finish_wait(&q,&wait);
第一行创建一个等待队列的列表项,第三行将其进队,条件condition代表需要的资源,
while循环是这样表示的:
1.如果需要的资源不存在,那么将进队,并将进程状态改为TASK_INTERRUPTIBLE。
2.检查进程是否处于TASK_INTERRUPTIBLE状态,如果是,则唤醒进程(伪唤醒),被伪唤醒的进程进行信号处理:检查condition是否为真,如果为真就退出循环(这边有一个疑问是怎么做到退出循环而不去用schedule()),否则继续执行schedule();
3.退出循环后,通过finish_wait()函数设置进程状态为TASK_RUNNING并从等待队列中删除进程。
看了好久网上查了点资料才理解,signal_pending(current)检查当前进程是否有信号处理,如果有则返回真,否则返回0.
关于 wake_up():
该函数唤醒所有在给定等待队列上的进程,设置状态为TASK_RUNNING,并把它添加到红黑树上。
通常情况下如有某段代码可能使等待条件满足,那么都应该调用wake_up()进行唤醒。
Preemption and Context Switching
这一段涉及了两个问题:1.schedule()在什么时候执行。2.context_switch()执行时发生了什么。
针对问题1。内核提供了一个need_resched标记来指明是否要执行schedule(),通过两个方法可以设置这个标记,通过scheduler_tick()(当一个进程应该被抢占的时候)或者try_to_wake_up() (当发现存在一个进程有比当前进程更高的优先级时)。当进程从内核态返回到用户态或者从中断返回时,会检查标记位以此来决定是否执行schedule()。
针对问题2。context_switch()执行分两部,先调用switch_mm()来切换虚拟地址的映射,再调用switch_to()更新和保存处理机状态(栈,寄存器,等等)。
1.User Preemption and Kernel Preemption
传统的Unix系统支持用户抢占,linux从2.6内核开始支持内核抢占。
用户抢占可发生在:1.从系统调用返回到用户态。2.从中断处理返回到用户态。
内核抢占可发生在:1.中断处理程序退出,返回到内核态之前。2.内核代码变成可抢占的(当前进程的所有锁都释放时)。3.内核中的进程显示调用schedule()。4.某个进程在内核中阻塞。
Real-Time Scheduling Policies
linux存在两个实时调度策略 SCHED_FIFO 和 SCHED_RR。使用SCHED FIFO的进程除非被阻塞或者主动释放处理机都将一直运行。
SCHED RR 是分配了时间片的SCHED FIFO。
所有实时调度策略都使用静态优先级。实时调度的优先级从0-99,普通调度优先级为100-139。