做者:不洗碗工做室 - Marklux
出处:marklux.cn/blog/64node
版权归做者全部,转载请注明出处linux
本文是《Linux内核设计与实现》第四章的阅读笔记,代码则是摘自最新的4.6版本linux源码(github),转载请注明出处。git
Linux做为一个多任务操做系统,必须支持程序的并发执行。github
非抢占式多任务算法
除非任务本身结束,不然将会一直执行。安全
抢占式多任务(Linux)bash
这种状况下,由调度程序来决定何时中止一个进程的运行,这个强制的挂起动做即为**“抢占”。采用抢占式多任务的基础是使用时间片轮转**机制来为每一个进程分配能够运行的时间单位。数据结构
Linux从2.5版本开始引入一种名为O(1)
的调度器,后在2.6版本中将公平的的调度概念引入了调度程序,代替以前的调度器,称为CFS
算法(彻底公平调度算法)。并发
I/O消耗型进程是指那些大部分时间都在等待I/O操做的进程,处理器耗费型的进程则是指把大多数时间用于执行代码的进程,除非被抢占,他们通常都一直在运行。dom
为了保证交互式应用和桌面系统的性能,通常Linux更倾向于优先调度I/O消耗型进程。
Linux采用了两种不一样的优先级范围。
使用nice值:越大的nice值意味着更低的优先级。 (-19 ~ 20之间)
实时优先级:可配置,越高意味着进程优先级越高。
任何实时的进程优先级都高于普通的进程,所以上面的两种优先级范围处于互不相交的范畴。
时间片:Linux中并非以固定的时间值(如10ms)来分配时间片的,而是将处理器的使用比做为“时间片”划分给进程。这样,进程所得到的实际CPU时间就和系统的负载密切相关。
Linux中的抢占时机取决于新的可运行进程消耗了多少处理器使用比,
若是消耗的使用比当前进程小,则马上投入运行,不然将推迟其运行。
复制代码
举例
如今咱们来看一个简单的例子,假设咱们的系统只有两个进程在运行,一个是文本编辑器(I/O消耗型),另外一个是视频解码器(处理器消耗型)。
理想的状况下,文本编辑器应该获得更多的处理器时间,至少当它须要处理器时,处理器应该马上被分配给它(这样才能完成用户的交互),这也就意味着当文本编辑器被唤醒的时候,它应该抢占视频解码程序。
按照普通的状况,OS应该分配给文本编辑器更大的优先级和更多的时间片,但在Linux中,这两个进程都是普通进程,他们具备相同的nice值,所以它们将获得相同的处理器使用比(50%)。
但实际的运行过程当中会发生什么呢?CFS将可以注意到,文本编辑器使用的处理器时间比分配给它的要少得多(由于大多时间在等待I/O),这种状况下,要实现全部进程“公平”地分享处理器,就会让文本编辑器在须要运行时马上抢占视频解码器(每次都是如此)。
Linux的调度器是以模块的方式提供的,这样使得不一样类型的进程按照本身的须要来选择不一样的调度算法。
上面说讲到的CFS算法就是一个针对普通进程的调度器类,基础的调度器会按照优先级顺序遍历调度类,拥有一个可执行进程的最高优先级的调度器类胜出,由它来选择下一个要执行的进程。
存在的问题:
nice值必须映射处处理器的绝对时间上去,这意味着一样是瓜分100ms的两个一样优先级的进程,发生上下文切换的次数并不相同,可能会差异很大。优先级越低的进程分到的时间片单位越小,可是实际上他们每每是须要进行大量后台计算的,这样很不合理。
相对的nice值引起的问题:两个nice值不一样但差值相同的进程,分到的时间片的大小是受到其nice值大小影响的:好比nice值18和19的两个进程分到的时间片是10ms和5ms,nice值为0和1的两个进程分到的倒是100ms和95ms,这样的映射并不合理。
若是要进行nice值到时间片的映射,咱们必须可以拥有一个能够测量的“绝对时间片”(这牵扯到定时器和节拍器的相关概念)。实际上,时间片是会随着定时器的节拍而改变的,一样的nice值最终映射处处理器时间时可能会存在差别。
为了可以更快的唤醒进程,须要对新的要唤醒的进程提高优先级,可是这可能会打破“公平性”。
为了解决上述的问题,CFS对时间片的分配方式进行了根本性的从新设计,摒弃了时间片,用处理器使用比重来代替它。
出发点:进程调度的效果应该如同系统具有一个理想的多任务处理器——咱们能够给任何进程调度无限小的时间周期,因此在任何可测量范围内,能够给n个进程桐乡多的运行时间。
举个例子来区分Unix调度和CFS:有两个运行的优先级相同的进程,在Unix中多是每一个各执行5ms,执行期间彻底占用处理器,但在“理想状况”下,应该是,可以在10ms内同时运行两个进程,每一个占用处理器一半的能力。
CFS的作法是:在全部可运行进程的总数上计算出一个进程应该运行的时间,nice值再也不做为时间片分配的标准,而是用于处理计算得到的处理器使用权重。
接下来咱们考虑调度周期,理论上,调度周期越小,就越接近“完美调度”,但实际上这必然会带来严重的上下文切换消耗。在CFS中,为可以实现的最小调度周期设定了一个近似值目标,称为“目标延迟”,于此同时,为了不不可接受的上下文切换消耗,为每一个进程所能得到的时间片大小设置了一个底线——最小粒度(一般为1ms)。
在每一个进程的平均运行时间大于最小粒度的状况下,CFS无疑是公平的,nice值用于计算一个进程在当前这个最小调度周期中所应得到的处理器时间占比,这样就算nice值不一样,只要差值相同,老是能获得相同的时间片。咱们假设一个最小调度周期为20ms,两个进程的nice值差值为5:
关于上面这个推论,可能有些难以理解,因此咱们深刻一下,看看在底层nice差值到底是如何影响处处理区占比的。
首先,在底层,在实际计算一个进程的处理器占比以前,内核会先把nice值转换为一个权重值weight,这个转换的公式以下:
weight = 1024/(1.25^nice)
复制代码
举个例子,默认nice值的进程获得的权重就是1024/(1.25^0) = 1024/1 = 1024。
这个转换公式保证了咱们能够获得非负的权重值,而且nice对权重的影响是在指数上的。
好,如今假设咱们的可运行进程队列中有n个进程,他们的权重和w(1)+w(2)+...+w(n)
记为w(queue)
,那么任意一个进程i最终获得的处理器占比将是w(i)/w(queue)
。
接着,咱们不难推导出,任意两个进程i和j所分配的到的处理器占比的比例应该是w(i)/w(j)
,通过简单的数学推导就能够获得最后的结果:1.25^(nice(i)-nice(j))
,这意味着只要两个nice值的差值相同,两个进程所得到处理器占比永远是相同的比例,从而解决了上面的第3点问题。
上述的转换公式参考自:https://oakbytes.wordpress.com/2012/06/06/linux-scheduler-cfs-and-nice
总结一下,任何进程所得到的处理器时间是由它本身和全部其余可运行进程nice值的相对差值决定的,所以咱们能够说,CFS至少保证了给每一个进程公平的处理器占用比,算是一种近乎完美的多任务调度方式了。
下面咱们来看看CFS是如何实现的,通常咱们把它分为4个主要的部分来分析。
全部的调度器都必须对进程的运行时间记帐,换句话说就是要知道当前调度周期内,进程还剩下多少个时间片可用(这将会是抢占的一个重要标准)
CFS中用于记录进程运行时间的数据结构为“调度实体”,这个结构体被定义在<linux/sched.h>
中:
struct sched_entity {
/* 用于进行调度均衡的相关变量,主要跟红黑树有关 */
struct load_weight load; // 权重,跟优先级有关
unsigned long runnable_weight; // 在全部可运行进程中所占的权重
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; // 进程在切换CPU时的sum_exec_runtime,简单说就是上个调度周期中运行的总时间
u64 nr_migrations;
struct sched_statistics statistics;
// 如下省略了一些在特定宏条件下才会启用的变量
}
复制代码
注:本文中全部用到的linux源码均来自linux在github上官方的git库(2018.01)
如今咱们来谈谈上面结构体中的vruntime变量所表示的意义。咱们称它为“虚拟运行时间”,该运行时间的计算是通过了全部可运行进程总数的标准化(简单说就是加权的)。它以ns为单位,与定时器节拍再也不相关。
能够认为这是CFS为了可以实现理想多任务处理而不得不虚拟的一个新的时钟,具体地讲,一个进程的vruntime会随着运行时间的增长而增长,但这个增长的速度由它所占的权重load
来决定。
结果就是权重越高,增加越慢:所获得的调度时间也就越小 —— CFS用它来记录一个程序到底运行了多长时间以及还应该运行多久。
下面咱们来看一下这个记帐功能的实现源码(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); // 计算虚拟时间,具体的转换算法写在clac_delta_fair函数中 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); cgroup_account_cputime(curtask, delta_exec); account_group_exec_runtime(curtask, delta_exec); } account_cfs_rq_runtime(cfs_rq, delta_exec); } 复制代码
该函数计算了当前进程的执行时间,将其存放在delta_exec
变量中,而后使用clac_delta_fair
函数计算对应的虚拟运行时间,并更新vruntime
值。
这个函数是由系统定时器周期性调用的(不管进程的状态是什么),所以vruntime能够准确地测量给定进程的运行时间,并以此为依据推断出下一个要运行的进程是什么。
这里即是调度的核心部分,用一句话来梗概CFS算法的核心就是选择具备最小vruntime的进程做为下一个须要调度的进程。
为了实现选择,固然要维护一个可运行的进程队列(教科书上常说的ready队列),CFS使用了红黑树来组织这个队列。
红黑树是一种很是著名的数据结构,但这里咱们不讨论它的实现和诸多特性(过于复杂),咱们记住:红黑树是一种自平衡二叉树,再简单一点,它是一种以树节点方式储存数据的结构,每一个节点对应了一个键值,利用这个键值能够快速索引树上的数据,而且它能够按照必定的规则自动调整每一个节点的位置,使得经过键值检索到对应节点的速度和整个树节点的规模呈指数比关系。
先假设一个红黑树储存了系统中全部的可运行进程,节点的键值就是它们的vruntime,CFS如今要找到下一个须要调度的进程,那么就是要找到这棵红黑树上键值最小的那个节点:就是最左叶子节点。
实现此过程的源码以下(kernel/sched/fair.c
):
static struct sched_entity *
pick_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
struct sched_entity *left = __pick_first_entity(cfs_rq);
struct sched_entity *se;
/*
* If curr is set we have to see if its left of the leftmost entity
* still in the tree, provided there was anything in the tree at all.
*/
if (!left || (curr && entity_before(curr, left)))
left = curr;
se = left; /* ideally we run the leftmost entity */
/*
* 下面的过程主要针对一些特殊状况,咱们在此不作讨论
*/
if (cfs_rq->skip == se) {
struct sched_entity *second;
if (se == curr) {
second = __pick_first_entity(cfs_rq);
} else {
second = __pick_next_entity(se);
if (!second || (curr && entity_before(curr, second)))
second = curr;
}
if (second && wakeup_preempt_entity(second, left) < 1)
se = second;
}
if (cfs_rq->last && wakeup_preempt_entity(cfs_rq->last, left) < 1)
se = cfs_rq->last;
if (cfs_rq->next && wakeup_preempt_entity(cfs_rq->next, left) < 1)
se = cfs_rq->next;
clear_buddies(cfs_rq, se);
return se;
}
复制代码
向可运行队列中插入一个新的节点,意味着有一个新的进程状态转换为可运行,这会发生在两种状况下:一是当进程由阻塞态被唤醒,二是fork产生新的进程时。
将其加入队列的过程本质上来讲就是红黑树插入新节点的过程:
static void
enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
bool renorm = !(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_MIGRATED);
bool curr = cfs_rq->curr == se;
/*
* 若是要加入的进程就是当前正在运行的进程,从新规范化vruntime
* 而后更新当前任务的运行时统计数据
*/
if (renorm && curr)
se->vruntime += cfs_rq->min_vruntime;
update_curr(cfs_rq);
/*
* Otherwise, renormalise after, such that we're placed at the current * moment in time, instead of some random moment in the past. Being * placed in the past could significantly boost this task to the * fairness detriment of existing tasks. */ if (renorm && !curr) se->vruntime += cfs_rq->min_vruntime; /* * 更新对应调度器实体的各类记录值 */ update_load_avg(cfs_rq, se, UPDATE_TG | DO_ATTACH); update_cfs_group(se); enqueue_runnable_load_avg(cfs_rq, se); account_entity_enqueue(cfs_rq, se); if (flags & ENQUEUE_WAKEUP) place_entity(cfs_rq, se, 0); check_schedstat_required(); update_stats_enqueue(cfs_rq, se, flags); check_spread(cfs_rq, se); if (!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()
来把数据真正插入红黑树中:
static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
struct rb_node **link = &cfs_rq->tasks_timeline.rb_root.rb_node;
struct rb_node *parent = NULL;
struct sched_entity *entry;
bool leftmost = true;
/*
* 在红黑树中搜索合适的位置
*/
while (*link) {
parent = *link;
entry = rb_entry(parent, struct sched_entity, run_node);
/*
* 具备相同键值的节点会被放在一块儿
*/
if (entity_before(se, entry)) {
link = &parent->rb_left;
} else {
link = &parent->rb_right;
leftmost = false;
}
}
rb_link_node(&se->run_node, parent, link);
rb_insert_color_cached(&se->run_node,
&cfs_rq->tasks_timeline, leftmost);
}
复制代码
while()循环是遍历树以寻找匹配键值的过程,也就是搜索一颗平衡树的过程。找到后咱们对要插入位置的父节点执行rb_link_node()
来将节点插入其中,而后更新红黑树的自平衡相关属性。
从队列中删除一个节点有两种可能:一是进程执行完毕退出,而是进程受到了阻塞。
static void
dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
/*
* 更新“当前进程”的运行统计数据
*/
update_curr(cfs_rq);
/*
* When dequeuing a sched_entity, we must:
* - Update loads to have both entity and cfs_rq synced with now.
* - Substract its load from the cfs_rq->runnable_avg.
* - Substract its previous weight from cfs_rq->load.weight.
* - For group entity, update its weight to reflect the new share
* of its group cfs_rq.
*/
update_load_avg(cfs_rq, se, UPDATE_TG);
dequeue_runnable_load_avg(cfs_rq, se);
update_stats_dequeue(cfs_rq, se, flags);
clear_buddies(cfs_rq, se);
if (se != cfs_rq->curr)
__dequeue_entity(cfs_rq, se);
se->on_rq = 0;
account_entity_dequeue(cfs_rq, se);
/*
* 从新规范化vruntime
*/
if (!(flags & DEQUEUE_SLEEP))
se->vruntime -= cfs_rq->min_vruntime;
/* return excess runtime on last dequeue */
return_cfs_rq_runtime(cfs_rq);
update_cfs_group(se);
/*
* Now advance min_vruntime if @se was the entity holding it back,
* except when: DEQUEUE_SAVE && !DEQUEUE_MOVE, in this case we'll be * put back on, and if we advance min_vruntime, we'll be placed back
* further than we started -- ie. we'll be penalized. */ if ((flags & (DEQUEUE_SAVE | DEQUEUE_MOVE)) == DEQUEUE_SAVE) update_min_vruntime(cfs_rq); } 复制代码
和插入同样,实际对树节点操做的工做由__dequeue_entity()
实现:
static void __dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
rb_erase_cached(&se->run_node, &cfs_rq->tasks_timeline);
}
复制代码
能够看到删除一个节点要比插入简单的多,这得益于红黑树自己实现的rb_erase()
函数。
正如上文所述,每当要发生进程的调度时,是有一个统一的入口,从该入口选择真正须要调用的调度类。
这个入口是内核中一个名为schedule()
的函数,它会找到一个最高优先级的调度类,这个调度类拥有本身的可运行队列,而后向其询问下一个要运行的进程是谁。
这个函数中惟一重要的事情是执行了pick_next_task()
这个函数(定义在kenerl/sched/core.c
中),它以优先级为顺序,依次检查每个调度类,而且从最高优先级的调度类中选择最高优先级的进程。
static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
const struct sched_class *class;
struct task_struct *p;
/*
* 优化:若是当前全部要调度的进程都是普通进程,那么就直接采用普通进程的调度类(CFS)
*/
if (likely((prev->sched_class == &idle_sched_class ||
prev->sched_class == &fair_sched_class) &&
rq->nr_running == rq->cfs.h_nr_running)) {
p = fair_sched_class.pick_next_task(rq, prev, rf);
if (unlikely(p == RETRY_TASK))
goto again;
/* Assumes fair_sched_class->next == idle_sched_class */
if (unlikely(!p))
p = idle_sched_class.pick_next_task(rq, prev, rf);
return p;
}
// 遍历调度类
again:
for_each_class(class) {
p = class->pick_next_task(rq, prev, rf);
if (p) {
if (unlikely(p == RETRY_TASK))
goto again;
return p;
}
}
/* The idle class should always have a runnable task: */
BUG();
}
复制代码
每一个调度类都实现了pick_next_task()
方法,它会返回下一个可运行进程的指针,没有则返回NULL。调度器入口从第一个返回非NULL的类中选择下一个可运行进程。
睡眠和唤醒的流程在linux中是这样的:
schedule()
选择和执行一个其余进程。休眠在Linux中有两种状态,一种会忽略信号,一种则会在收到信号的时候被唤醒并响应。不过这两种状态的进程是处于同一个等待队列上的。
和可运行队列的复杂结构不一样,等待队列在linux中的实现只是一个简单的链表。全部有关等待队列的数据结构被定义在include/linux/wait.h
中,具体的实现代码则被定义在kernel/sched/wait.c
中。
内核使用wait_queue_head_t
结构来表示一个等待队列,它其实就是一个链表的头节点,可是加入了一个自旋锁来保持一致性(等待队列在中断时能够被随时修改)
struct wait_queue_head {
spinlock_t lock;
struct list_head head;
};
typedef struct wait_queue_head wait_queue_head_t;
复制代码
而休眠的过程须要进程本身把本身加入到一个等待队列中,这可使用内核所提供的、推荐的函数来实现。
一个可能的流程以下:
DEFINE_WAIT()
建立一个等待队列的项(链表的节点)add_wait_queue()
把本身加到队列中去。该队列会在进程等待的条件知足时唤醒它,固然唤醒的具体操做须要进程本身定义好(你能够理解为一个回调)prepare_to_wait()
方法把本身的状态变动为上面说到的两种休眠状态中的其中一种。下面是上述提到的方法的源码:
void add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
unsigned long flags;
wq_entry->flags &= ~WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&wq_head->lock, flags);
__add_wait_queue(wq_head, wq_entry);
spin_unlock_irqrestore(&wq_head->lock, flags);
}
static inline void __add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
list_add(&wq_entry->entry, &wq_head->head);
}
复制代码
void
prepare_to_wait(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry, int state)
{
unsigned long flags;
wq_entry->flags &= ~WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&wq_head->lock, flags);
if (list_empty(&wq_entry->entry))
__add_wait_queue(wq_head, wq_entry);
// 标记本身的进程状态
set_current_state(state);
spin_unlock_irqrestore(&wq_head->lock, flags);
}
复制代码
唤醒操做主要经过wake_up()
实现,它会唤醒指定等待队列上的全部进程。内部由try_to_wake_up()
函数将对应的进程标记为TASK_RUNNING
状态,接着调用enqueue_task()
将进程加入红黑树中。
wake_up()
系函数由宏定义,通常具体内部由下面这个函数实现:
/*
* The core wakeup function. Non-exclusive wakeups (nr_exclusive == 0) just
* wake everything up. If it's an exclusive wakeup (nr_exclusive == small +ve * number) then we wake all the non-exclusive tasks and one exclusive task. * * There are circumstances in which we can try to wake a task which has already * started to run but is not in state TASK_RUNNING. try_to_wake_up() returns * zero in this (rare) case, and we handle it by continuing to scan the queue. */ static int __wake_up_common(struct wait_queue_head *wq_head, unsigned int mode, int nr_exclusive, int wake_flags, void *key, wait_queue_entry_t *bookmark) { wait_queue_entry_t *curr, *next; int cnt = 0; if (bookmark && (bookmark->flags & WQ_FLAG_BOOKMARK)) { curr = list_next_entry(bookmark, entry); list_del(&bookmark->entry); bookmark->flags = 0; } else curr = list_first_entry(&wq_head->head, wait_queue_entry_t, entry); if (&curr->entry == &wq_head->head) return nr_exclusive; list_for_each_entry_safe_from(curr, next, &wq_head->head, entry) { unsigned flags = curr->flags; int ret; if (flags & WQ_FLAG_BOOKMARK) continue; ret = curr->func(curr, mode, wake_flags, key); if (ret < 0) break; if (ret && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive) break; if (bookmark && (++cnt > WAITQUEUE_WALK_BREAK_CNT) && (&next->entry != &wq_head->head)) { bookmark->flags = WQ_FLAG_BOOKMARK; list_add_tail(&bookmark->entry, &next->entry); break; } } return nr_exclusive; } 复制代码
上下文切换是指从一个可执行进程切换到另外一个可执行进程。由定义在kernel/sched/core.c
中context_switch()
实现:
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next, struct rq_flags *rf)
{
struct mm_struct *mm, *oldmm;
prepare_task_switch(rq, prev, next);
mm = next->mm;
oldmm = prev->active_mm;
/*
* For paravirt, this is coupled with an exit in switch_to to
* combine the page table reload and the switch backend into
* one hypercall.
*/
arch_start_context_switch(prev);
// 把虚拟内存从上一个内存映射切换到新进程中
if (!mm) {
next->active_mm = oldmm;
mmgrab(oldmm);
enter_lazy_tlb(oldmm, next);
} else
switch_mm_irqs_off(oldmm, mm, next);
if (!prev->mm) {
prev->active_mm = NULL;
rq->prev_mm = oldmm;
}
rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);
/*
* Since the runqueue lock will be released by the next
* task (which is an invalid locking op but in the case
* of the scheduler it's an obvious special-case), so we * do an early lockdep release here: */ rq_unpin_lock(rq, rf); spin_release(&rq->lock.dep_map, 1, _THIS_IP_); /* Here we just switch the register state and the stack. */ // 切换处理器状态到新进程,这包括保存、恢复寄存器和栈的相关信息 switch_to(prev, next, prev); barrier(); return finish_task_switch(prev); } 复制代码
上下文切换由schedule()
函数在切换进程时调用。可是内核必须知道何时调用schedule()
,若是只靠用户代码显式地调用,代码可能会永远地执行下去。
为此,内核为每一个进程设置了一个need_resched
标志来代表是否须要从新执行一次调度,当某个进程应该被抢占时,scheduler_tick()
会设置这个标志,当一个优先级高的进程进入可执行状态的时候,try_to_wake_up()
也会设置这个标志位,内核检查到此标志位就会调用schedule()
从新进行调度。
内核即将返回用户空间的时候,若是need_reshced
标志位被设置,会致使schedule()
被调用,此时就发生了用户抢占。意思是说,既然要从新进行调度,那么能够继续执行进入内核态以前的那个进程,也彻底能够从新选择另外一个进程来运行,因此若是设置了need_resched
,内核就会选择一个更合适的进程投入运行。
简单来讲有如下两种状况会发生用户抢占:
Linux和其余大部分的Unix变体操做系统不一样的是,它支持完整的内核抢占。
不支持内核抢占的系统意味着:内核代码能够一直执行直到它完成为止,内核级的任务执行时没法从新调度,各个任务是以协做方式工做的,并不存在抢占的可能性。
在Linux中,只要从新调度是安全的,内核就能够在任什么时候间抢占正在执行的任务,这个安全是指,只要没有持有锁,就能够进行抢占。
为了支持内核抢占,Linux作出了以下的变更:
thread_info
引入了preempt_count
计数器,用于记录持有锁的数量,当它为0的时候就意味着这个进程是能够被抢占的。need_resched
和preempt_count
的值,若是need_resched
被标记,而且preempt_count
为0,就意味着有一个更须要调度的进程须要被调度,并且当前状况是安全的,能够进行抢占,那么此时调度程序就会被调用。除了响应中断后返回,还有一种状况会发生内核抢占,那就是内核中的进程因为阻塞等缘由显式地调用schedule()
来进行显式地内核抢占:固然,这个进程显式地调用调度进程,就意味着它明白本身是能够安全地被抢占的,所以咱们不用任何额外的逻辑去检查安全性问题。
下面罗列可能的内核抢占状况:
schedule()