本文共 5967 字,大约阅读时间需要 19 分钟。
紧接上一文!!!!
3:进程选择
在CFS调度里面,当需要选择下一个进程的时候,将会选择最小的vruntime的进程。这个其实就是CFS调度的算法的核心。
CFS使用红黑树来组织可运行进程队列,并利用其迅速找到最小的vruntime值的进程。在Linux中,红黑树是一个子平衡的二叉搜索树。
下面我们就来看一下如何挑选下一个vruntime最小的进程。
1):挑选下一个任务
根据红黑树的原理,假设vruntime就是节点的键值,那么如果要寻找最小的vruntime的进程,就是从树的根节点开始,一直向左寻找,一直找到树的最左边的节点即为vruntime最小的节点。
下面我们来看一下如何进行选择下一个任务的函数__pick_next_entity()。定义在kernel/sched_fair.c中:
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);}
看了这段代码,很明显,并没有进行寻找,而是直接获取了运行队列中的rb_leftmost成员,其实,这就是linux中的一个用空间省时间的一个做法。他先将最小vruntime的节点保存下来,保存成rb_leftmost成员,当使用的时候,直接拿来使用就可以,但是当每次vruntime更新的时候,都会重现最一下选择,选择出最小的vruntime的节点保存到该成员中。如果找不到最小的vruntime的进程,那么CFS调度其将会选择idle任务运行。
2):向树中加入进程
当进程变为可运行状态或者是通过fork()调用第一次创建进程的时候,CFS就需要将进程加入到rbtree中。现在我们来看一下如何将进程加入到rbtree中,在函数enqueue_entity中被实现。
static voidenqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags){ /* * Update the normalized vruntime before updating min_vruntime * through callig update_curr(). * * 通过调用update_curr()函数,在更新min_vruntime之前来更新 * 规范化的vruntime */ 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_entity()函数向rbtree中插入节点,将数据真正插入到rbtree中。
下面我们看一下函数__enqueue_entity()。/* * Enqueue an entity into the rb-tree: * 向rbtree中添加一个实体 */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: * * 在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);}
如果对这些代码不太理解的话,也不用太担心,当我们后面学习了红黑树的时候,这些问题就都理解了。这里对节点的插入就不详细讲解,后面会专门讲解红黑树。
3):从树中删除进程
当进程堵塞或者是终止的时候,CFS需要将进程从rbtree中删除,下面我们来看一下CFS是如何删除进程的。
static voiddequeue_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. * * 当更新min_vruntime之后,规范化调度器实体,因为更新可以指向"->curr" * 项,我们需要在规范化的位置来反映这个变化 */ if (!sleep) se->vruntime -= cfs_rq->min_vruntime;}
根据代码,实际上是调用__dequeue_entity()函数来删除rbtree中的实体。
下面我们看一下代码:
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);}
具体的对于rbtree的操作我们后面具体详解。
从红黑树中删除节点比较容易,因为rbtree实现了rb_erase()函数,可以完成所有工作。该函数剩下的工作就是更新rb_leftmost缓存,如果删除的是最左节点,则需要遍历树,找到下一个最左节点。
4:调度器入口
调度器的主要入口是函数schedule(),定义在文件kernel/sched.h文件中。他是内核其他部分用于调用进程调度器的入口:选择哪个进程可以运行,何时将其投入运行。该函数唯一重要的事情就是调用pick_next_task()函数,该函数会以优先级为序,从高到低,依次检查每一个调度器类,并且从最高优先级调度器类中选择最高优先级的进程。
下面我们看一下这个函数:
/* * 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: * * 永远不会为NULL,因为idle类总会返回非NULL的p */ class = class->next; }}
该函数开始部分的优化比较有意思。因为CFS是普通进程的调度类,而系统的绝大部分进程是普通进程,所以这里使用该技巧来加速一下下一个CFS提供的进程。但是前提是所有的可运行进程都是CFS类的。
该函数的主要部分是for循环,从最高的优先级类开始,遍历了每一个调度类。每一个调度类都实现了pick_next_task()函数,他会返回下一个可运行进程或者是没有时返回NULL,我们会从第一个返回非NULL的类中选择下一个可运行进程。