进程调度(二)

紧接上一文!!!!

​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 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(). * * 通过调用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 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.
     *
     * 当更新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的类中选择下一个可运行进程。

你可能感兴趣的:(进程调度,rbtree的操作)