pick_next_task与红黑树浅析

Posted taocr

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了pick_next_task与红黑树浅析相关的知识,希望对你有一定的参考价值。

一、schedule()中关键步骤pick_next_task的分析

在整个函数的流程中,pick_next_task是一个十分重要的步骤,它负责找到当前cpu的运行队列中最应该运行的那个进程,那么它是如何实现的呢?
首先它的实现是依靠钩子函数,每个调度类的具体实现都是靠钩子函数来完成的,这里说下对钩子函数的理解:

钩子函数就是提前声明一个指向函数的指针,其中也声明好了关于此函数的参数以及其的返回值等等,预先留出一个位置,当你需要使用它时,再去具体实现这个功能,将这个具体函数挂在留出的位置上,这就是钩子函数。

关于pick_next_task的具体实现:

static inline struct task_struct *   
pick_next_task(struct rq *rq)

    const struct sched_class *class;
    struct task_struct *p;

首先是声明一些需要用到的数据结构

if (likely(rq->nr_running == rq->cfs.nr_running))   
        p = fair_sched_class.pick_next_task(rq);
        if (likely(p))  
            return p;
    

这一部分是一个很聪明的步骤,系统中大部分时间是没有实时进程的,因此利用这一段代码就可以不需要每次都从实时进程开始选择,节省时间
if语句中判断了当前cpu就绪队列中的进程数目是否与普通进程的就绪队列中的进程数目相同,如果相同就说明了系统中全是普通进程,直接通过cfs算法的调度类的pick_next_task_fair函数来从普通进程的就绪队列中寻找进程即可,然后判断是否找到了这个进程,如果找到了就返回这个进程,没找到就继续执行。

class = sched_class_highest; 
    for ( ; ; )   
        p = class->pick_next_task(rq);
        if (p)
            return p;
        class = class->next;
    

首先sched_class_highest指的是当前系统中优先级最高的调度类,那么系统中的调度顺序是实时、普通、空闲,所以这里即指实时进程
之后是一个死循环语句,首先第一次循环会判断实时进程中有没有可运行的进程,如果没找到,利用class=class->next即可找到下一个优先级的调度类

static const struct sched_class rt_sched_class = 
    .next           = &fair_sched_class,
        ...
    

实时进程的调度类中的next字段就是普通进程调度类,于是第二次循环判断找寻普通进程中的下一个应该运行的进程,如果没有就在第三次循环中找寻空闲进程来运行。
这个循环最后终将找出一个可执行的进程,因为空闲进程永不为空,即就算实时和普通都没有可运行的进程,也能找到空闲进程来运行。

二、关于CFS算法的运行队列红黑树

关于实时的O(1)算法中的运行队列,它利用了位图法且建立了100个不同优先级的双向链表,于是能够实现O(1)算法中的快速查找下一个进程,而CFS算法也有其独特的运行队列的组织方式,使得寻找的时间大幅度缩减,这个就是红黑树。

红黑树的数据结构:

struct rb_node

    unsigned long  rb_parent_color;
#define RB_RED      0    /*红是0*/
#define RB_BLACK    1    /*黑是1*/
    struct rb_node *rb_right; 
    struct rb_node *rb_left; 
 __attribute__((aligned(sizeof(long))));

struct rb_root

    struct rb_node *rb_node;
;

这里有两个数据结构,其中rb_root是用来定义根节点的,而rb_node是一个节点的数据结构,主要需要看下关于rb_node中的

unsigned long rb_parent_color;

它的声明是无符号长整形,因此在64位的系统中总共是64位,但是它即存储了父节点的首地址也存储了此节点的颜色.
在rb_node结构体声明的末尾有attribute((aligned(sizeof(long)))),它保证了红黑树中每个节点的首地址都是对齐的,每个首地址的第0和第1位都为0,所以将最后一位用来存储颜色,不会干扰到首地址的存在,具体如图所示

关于红黑树,其实最精髓的地方就在于其插入的实现步骤:

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;  /*父节点为0*/
    struct sched_entity *entry;
    s64 key = entity_key(cfs_rq, se); 
    int leftmost = 1;

首先声明了一个link的结构体变量用来指向根节点,并声明了其他的各种需要的变量,其中key即为用于在红黑树中进行比较的值,它的实际语句是

static inline s64 entity_key(struct cfs_rq *cfs_rq, struct sched_entity *se)

    return se->vruntime - cfs_rq->min_vruntime;

可以看出实际在红黑树中比较的key值是调度实体自身的vruntime减去就绪队列中最小的vruntime值得到的。
另外定义了一个整型变量leftmost并置为1,这是实现红黑树的一个十分重要的变量

while (*link)   
        parent = *link;   
        entry = rb_entry(parent, struct sched_entity, run_node);
        if (key < entity_key(cfs_rq, entry)) 
            link = &parent->rb_left; 
         else                         
            link = &parent->rb_right;
            leftmost = 0;
        
    
if (leftmost)
        cfs_rq->rb_leftmost = &se->run_node;

这个while循环是用于搜索新加入的节点所应该在的位置,从根节点开始判断,此节点是在其左端还是右端,如果向左leftmost不变,而向右则会将此变量置为0,最后根据leftmost来判断这个新加入的节点是否在红黑树的最左端,如果leftmost是1代表其在最左端,且是下一个最应该运行的进程。

可以看到这里已经实现了寻找下一个最应该运行的进程,因此在调度函数中pick_next_task_fair并不需要遍历整个树来查找,只需要获取rb_leftmost所指向的进程即可

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);  /*返回调度实体的首地址*/

这段代码就是pick_next_task_fair中最核心的实现获取下一个运行的进程功能的函数。

看下插入的实现语句的最后一部分:

rb_link_node(&se->run_node, parent, link);   /*初始化节点*/
    rb_insert_color(&se->run_node, &cfs_rq->tasks_timeline);  /*添加颜色*/

这是最后一部分,总共两个函数,第一个实现了对这个点的初始化,其中对这个节点的父节点也进行了赋值,而第二个函数则是为其添加颜色
于是完成了一个新的调度实体插入红黑树的步骤

以上是关于pick_next_task与红黑树浅析的主要内容,如果未能解决你的问题,请参考以下文章

二叉树与红黑树

哈希表与红黑树

二叉树与红黑树的java实现

HashMap与红黑树

Re:从零开始的DS生活 轻松从0基础写出Huffman树与红黑树

java随笔——HashMap与红黑树