第一次作业:基于Linux CFS算法的部分源码分析

第一次作业:基于Linux CFS算法的部分源码分析

一.概述

​ 通常情况下,电脑的CPU会有多个进程或线程同时竞争。当CPU被一个进程占用时,其他的进程不得不等待。当CPU中的进程完成或者中断等待IO时,我们必须从等待的进程中挑选合适的进程放进CPU中处理。挑选进程的方法便是调度算法。

​ 一个好的调度算法需要考虑到三个方面。首先是公平,需要它给每个进程公平的CPU份额。其次是策略强制执行,保证规定的策略能被执行。最后是平衡,保持系统的所以部分都忙碌。协调这三点有助于提高操作系统工作的效率。本文选取了CFS(完全公平调度)算法来了解linux中是怎样完成调度的,原因主要是其他调度系统的源码资源比较匮乏。

二.进程

A)进程的组织

当一个程序加载到内存中时,它就变成了一个进程。系统会为它建立一个PCB块记录各种信息,PCB经常被系统访问,所以常驻于内存中。部分定义如

struct task_struct {  
volatile long state;  //说明了该进程是否可以执行,还是可中断等信息  
unsigned long flags;  //Flage 是进程号,在调用fork()时给出  
int sigpending;    //进程上是否有待处理的信号  
mm_segment_t addr_limit; //进程地址空间,区分内核进程与普通进程在内存存放的位置不同  
                        //0-0xBFFFFFFF for user-thead  
                        //0-0xFFFFFFFF for kernel-thread  
//调度标志,表示该进程是否需要重新调度,若非0,则当从内核态返回到用户态,会发生调度  
volatile long need_resched;  
int lock_depth;  //锁深度  
long nice;       //进程的基本时间片  
//进程的调度策略,有三种,实时进程:SCHED_FIFO,SCHED_RR, 分时进程:SCHED_OTHER  
unsigned long policy;  
struct mm_struct *mm; //进程内存管理信息  
int processor;  
//若进程不在任何CPU上运行, cpus_runnable 的值是0,否则是1 这个值在运行队列被锁时更新  
unsigned long cpus_runnable, cpus_allowed;  
struct list_head run_list; //指向运行队列的指针  
………………
pid_t pid;    //进程标识符,用来代表一个进程  
pid_t pgrp;   //进程组标识,表示进程所属的进程组  

 

B)进程状态的转换

Linux进程状态有

TASK_RUNNING : 就绪态或者运行态,进程就绪可以运行,但是不一定正在占有CPU,对应进程状态的R

TASK_INTERRUPTIBLE:睡眠态,但是进程处于浅度睡眠,可以响应信号,一般是进程主动sleep进入的状态,对应进程状态S

TASK_UNINTERRUPTIBLE:睡眠态,深度睡眠,不响应信号,典型场景是进程获取信号量阻塞,对应进程状态D

TASK_ZOMBIE:僵尸态,进程已退出或者结束,但是父进程还不知道,没有回收时的状态,对应进程状态Z

TASK_STOPED:停止,调试状态,对应进程状态T

它们之间的转换如下图:

三.CFS算法

A)CFS原理

​ CFS算法的核心是实现公平,确保每个进程尽可能得到相同的处理器资源。内核给每个进程维护了一个虚拟运行时间vruntime ,调度器基于vruntime(程序已运行的时间)来衡量哪个进程最值得调度,所以它的就绪队列是一棵以vruntime(实际上是min_vruntime)为键值的red-black tree (如下图)。占用资源时间越小的进程就越靠近树的左端。因此,调度器每次选择最左边的节点存储的进程,这个进程的已运行的时间最小。

B)CFS中重要的数据结构

cfs_rq是用来描述运行在同一个CPU上处于TASK_RUNNING状态的普通进程的各种运行信息。

struct cfs_rq {
    struct load_weight load;  //运行队列总的进程权重
    unsigned int nr_running, h_nr_running; //进程的个数
​
    u64 exec_clock;  //运行的时钟
    u64 min_vruntime; //该cpu运行队列的vruntime推进值, 一般是红黑树中最小的vruntime值
struct rb_root tasks_timeline; //红黑树的根结点
    struct rb_node *rb_leftmost;  //指向vruntime值最小的结点
    //当前运行进程, 下一个将要调度的进程, 马上要抢占的进程, 
    struct sched_entity *curr, *next, *last, *skip;
​
    struct rq *rq; //系统中有普通进程的运行队列, 实时进程的运行队列, 这些队列都包含在rq运行队列中  
    ...
};

 

​ 调度实体sched_entity 是用来记录一个进程的运行状态信息,将在下面介绍。

进程描述符task_struct ,调度实体sched_entity ,和运行队列cfs_rq的关系如下图:

 

C)vruntime

vruntime是由程序已运行的时间决定的,但又不是任何两个程序运行相同的时间后vruntime的增量都相同。它还由程序的优先级所决定,但是在CFS中,优先级的概念被弱化,而是强调进程的权重(weight),一个进程的权重越大,就说明它越需要运行,对应的vruntime也就越小。看一下

一个进程在一个调度周期里的运行时间

分配给进程的运行时间 = 调度周期 * 进程权重 / 所有进程权重之和

一个进程的实际运行时间vruntime的关系是

vruntime = 实际运行时间 * NICE_0_LOAD / 进程权重
         = 实际运行时间 * 1024 / 进程权重
​
NICE_0_LOAD = 1024, 表示nice值为0的进程权重

可以看到,进程的权重越大,运行同样的时间,它的vruntime增长的越慢,需要运行的优先级就越高。

一个进程在一个调度周期内的vruntime大小为

vruntime = 进程在一个调度周期内的实际运行时间 * 1024 / 进程权重
         = (调度周期 * 进程权重 / 所有进程总权重) * 1024 / 进程权重 
         = 调度周期 * 1024 / 所有进程总权重 

可以看出一个进程在一个调度周期内的vruntime的值不与自己的的权重有关,而是所有进程都是一样的。从上面可以看出,在一个调度周期内,每个进程将系统分配给自己的运行时间使用完后,它们的vruntime的值是一样大的,因此一个进程的vruntime值越大,说明它得到的运行时间就越多

有关优先级和权重的关系Linux 2.6.23在内核中通过prio_to_weigth 数组进行优先级nice值和权重的转换。

static const int prio_to_weight[40] = {  
 /* -20 */     88761,    71755,     56483,     46273,    36291,  
 /* -15 */     29154,    23254,     18705,     14949,    11916,  
 /* -10 */      9548,     7620,      6100,      4904,     3906,  
 /*  -5 */     3121,      2501,      1991,     1586,      1277,  
 /*   0 */     1024,       820,       655,       526,       423,  
 /*   5 */      335,       272,       215,       172,       137,  
 /*  10 */      110,        87,        70,        56,        45,  
 /*  15 */       36,        29,        23,        18,        15,  
};  

​ 从这个数组中可以获得nice值从-20至19所对应的权值。从数组中可以看到,nice值越小的进程它的执行优先级会更高(权重更大)。

​ 为了更深入的了解vruntime,我们先来看它的定义。

struct sched_entity
{
    struct load_weight      load;           /* for load-balancing负荷权重,这个决定了进程在CPU上的运行时间和被调度次数 */
    struct rb_node          run_node;
    unsigned int            on_rq;          /*  是否在就绪队列上  */
​
    u64                     exec_start;         /*  上次启动的时间*/
​
    u64                     sum_exec_runtime;
    u64                     vruntime;
    u64                     prev_sum_exec_runtime;
    /* rq on which this entity is (to be) queued: */
    struct cfs_rq           *cfs_rq;
    ...
};

在这里sum_exec_runtime是用于记录进程消耗的CPU时间,并在进程撤销时保存到prev_sum_exec_runtime中。

D)vruntime值的设置

​ 无论是新进程vruntime的设置,还是进程中断以后的更新。vruntime的值都是直接由update_curr()函数进行操作,代码如下:

static void update_curr(struct cfs_rq *cfs_rq)  
{  
    struct sched_entity *curr = cfs_rq->curr;  
    u64 now = rq_of(cfs_rq)->clock; 
    unsigned long delta_exec;  
​
    delta_exec = (unsigned long)(now - curr->exec_start);  //得到本次tick实际运行的时间值
    __update_curr(cfs_rq, curr, delta_exec);  //将本次tick实际运行的时间值更新到vruntime和实际运行时间
    curr->exec_start = now;   //设置下次tick的开始时间
    if (entity_is_task(curr)) {  
        struct task_struct *curtask = task_of(curr);  
        cpuacct_charge(curtask, delta_exec);  
        account_group_exec_runtime(curtask, delta_exec);  
    }  
}  
​
static inline void  
__update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr,  
          unsigned long delta_exec)  
{  
    unsigned long delta_exec_weighted;  
    //前面说的sum_exec_runtime就是在这里计算的,它等于进程从创建开始占用CPU的总时间  
    curr->sum_exec_runtime += delta_exec;   
    //下面变量的weighted表示这个值是从运行时间考虑权重因素换算来的vruntime,再写一遍这个公式  
    //vruntime(delta_exec_weighted) = 实际运行时间(delta_exe) * 1024 / 进程权重  
    delta_exec_weighted = calc_delta_fair(delta_exec, curr);  
    //将进程刚刚运行的时间换算成vruntime后立刻加到进程的vruntime上
    curr->vruntime += delta_exec_weighted;  
    //因为有进程的vruntime变了,因此cfs_rq的min_vruntime可能也要变化,更新它。  
    //就是先取tmp = min(curr->vruntime,leftmost->vruntime)  
    //然后cfs_rq->min_vruntime = max(tmp, cfs_rq->min_vruntime)  
    update_min_vruntime(cfs_rq);  
}  

四.浅谈操作系统进程模型

操作系统进程模型在我认为主要是用来存储和管理进程状态。如上面说的,当一个程序加载到内存时,它就变成了进程。而在我们使用计算机时会同时运行多个程序,每个程序又可能由多个进程组成,所以就要对进程进行管理,判断哪个程序中的哪个进程更需要CPU资源并分配给它。哪些优先级低,如等待IO操作等,可以让它将资源让出。使CPU资源得到充分的使用,并确保任意一个程序能尽可能的得到运行。

五.参考资料

 

 

posted @ 2018-04-28 15:35  黄连福  阅读(611)  评论(0编辑  收藏  举报