根据Linux2.6.26源码分析进程模型

1.关于进程

1.1进程的概念

进程是正在运行的程序实体,并且包括这个运行的程序中占据的所有系统资源,比如说CPU(寄存器),IO,内存,网络资源等。很多人在回答进程的概念的时候,往往只会说它是一个运行的实体,而会忽略掉进程所占据的资源。比如说,同样一个程序,同一时刻被两次运行了,那么他们就是两个独立的进程。linux下查看系统进程的命令是ps。
 
1.2进程在内核中的描述

在linux中,有一个结构体task_struct,被专门用来描述进程的信息。在2.4版本及其以前,task_struct与内核堆栈是放在同一个4K的页面中,如下: union task_union{ struct task_struct task; unsigned long stack [INIT_TASK_SIZE/sizeof(log)]; }; 这样有一个好处,就是在内核中运行的时候,任何时候都可以通过指针得到当前运行进程的task_struct。尽管这给进程管理带来了好处,不过也有很大的隐患,如果task_struct越来越大,或者内核堆栈压的太多(函数调用层次太多),就不行了。所以从linux2.6开始,就有所改变了。首先是将这个4K的页面增加到8K:#defineTHREAD_SIZE (8192),然后又这样一句代码:#define alloc_thread_info(tsk) kmalloc(THREAD_SIZE,GFP_KERNEL)。然后还有一个很大的改变,那就是把task_struct从这部分空间中移走,然后抽象出一个thread_info的结构(里面是经常被访问的变量): struct thread_info{ struct task_struct *task; //main task structure ...... //还有几个其他的选项 } 这样,thread_info就代替了原来task_struct的位置,和内核堆栈共享8K的空间。从上面的描述中也可以看到thread_info里面有一个指针,指向了task_struct。通过这两个方式,就可以解决上面提到的隐患问题。
 
1.3进程的状态转换

在linux中,有两个变量用来表示内核的状态,volatile long state 用来表示进程的可运行性,long exit_state用来表示进程退出时的状态。此外,关于进程的详细状态,参见下面的宏定义: #define TASK_RUNNING 0 //正在运行的进程; #define TASK_INTERRUPTIBLE 1 //等待资源的进程,当等待的资源有效时被唤醒,也可由其他进程或内核用信号中断、唤醒后进入就绪状态 #define TASK_UNINTERRUPTIBLE 2 //同上,但是不能被其他进程或者内核中断 #define TASK_STOPPED 4 //进程被暂停,一般是收到了SIGSTOP/SIGTSTP/SIGTTIN/SIGTTOU信号,通过其他进程的信号才能被唤醒 #define TASK_TRACED 8 //进程被跟踪,一般在调试的时候会用的 //in tsk->state again #define EXIT_ZOMBIE 16 //僵死状态,虽然释放了内存文件等资源,但内核中仍然保存task_struct,等待父进程调用wait4()或者waitpid()函数来回收 #define EXIT_DEAD 32 //进程消亡前最后一个状态,父进程已调用了wait4()或waitpid() //in tsk->state again #define TASK_NONINTERACTIVE 64 //不是交互式进程,调度的时候会考虑到 至于进程间的转换,这里就不描述了。
 
1.4进程标志位

为了对每个进程进行更细粒度的控制,在task_struct中还有一些变量flags: unsigned long flags ;//per process flags,defined blow 这个flags可以是下面的一些标志的组合: 
#define PF_ALIGNWARN 0x00000001 /* Print alignment warningmsgs*/
 #define PF_STARTING 0x00000002 /* being created */ 
#define PF_EXITING 0x00000004 /* getting shut down */ 
#define PF_DEAD 0x00000008 /* Dead */ 
#define PF_FORKNOEXEC 0x00000040 /* forked but didn't exec */ 
#define PF_SUPERPRIV 0x00000100 /* used super-user privileges */ 
#define PF_DUMPCORE 0x00000200 /* dumped core */ 
#define PF_SIGNALED 0x00000400 /* killed by a signal */ 
#define PF_MEMALLOC 0x00000800 /* Allocating memory */ 
#define PF_FLUSHER 0x00001000 /* responsible for disk writeback */ 
#define PF_USED_MATH 0x00002000 /* if unset the fpu must be initialized before use */ 
#define PF_FREEZE 0x00004000 /* this task is being frozen for suspend now */ 
#define PF_NOFREEZE 0x00008000 /* this thread should not be frozen */ 
#define PF_FROZEN 0x00010000 /* frozen for system suspend */ 
#define PF_FSTRANS 0x00020000 /* inside a filesystem transaction */ 
#define PF_KSWAPD 0x00040000 /* I am kswapd */ 
#define PF_SWAPOFF 0x00080000 /* I am in swapoff */ 
#define PF_LESS_THROTTLE 0x00100000 /* Throttle me less: I clean memory */ 
#define PF_SYNCWRITE 0x00200000 /* I am doing a sync write */ 
#define PF_BORROWED_MM 0x00400000 /* I am a kthread doing use_mm */ 
#define PF_RANDOMIZE 0x00800000 /* randomize virtual address space */
1.5进程的调度策略

在task_struct中与进程调度相关的变量是:unsigned long policy,有三种调度策略: #define SCHED_NORMAL 0 #define SCHED_FIFO 1 #define SCHED_RR 2 不过这里有点问题,我下的2.6源码中就这三种调度策略,但是我看到有的书上介绍2.6内核的时候说到此外还有一种调度策略:SCHED_BATCH,说该调度策略一般是用于后台处理进程,没有交互性。FIFO与RR属于实时调度,所以优先级高于另外两种。 进程的调度优先级: int prio,static_prio; unsigned long rt_priority; prio是进程的动态优先级,随着进程的运行而改变,调度器有时候还会根据进程的交互性、平均睡眠时间而进行奖惩。默认情况下,实时进程(FIFO,RR)的动态优先级为0到99,另外两种是100到139,0最高,139最低。static_prio是普通进程的静态优先级,默认是120,rt_priority是实时进程的静态优先级。

2.进程的特征

    动态性:进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的。

    并发性:任何进程都可以同其他进程一起并发执行

    独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位;

    异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推 进

    结构特征:进程由程序、数据和进程控制块三部分组成;

    多个不同的进程可以包含相同的程序:一个程序在不同的数据集里就构成不同的进程,能得到不同的结果;  但是执行过程中,程序不能发生改变。

3.进程的三种基本状态
     a,就绪状态:进程已获得除处理器外的所需资源,等待分配处理器资源;只要分配了处理器进程就可执行。就绪进程可以按多个优先级来划分队列。例如,当一个进程由于时间片用完而进入就绪状态                                 时,排入低优先级队列;当进程由I/O操作完成而进入就绪状态时,排入高优先级队列。

     b,运行状态:进程占用处理器资源;处于此状态的进程的数目小于等于处理器的数目。在没有其他进程可以 执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。

     c,阻塞状态:由于进程等待某种条件(如I/O操作或进程同步),在条件满足之前无法继续执行。该事件发生前即使把处理机分配给该进程,也无法运行。

 

   

  进程状态转换图:

                                      

                

4.进程控制
     创建进程
         引起创建进程的事件:
             1) 用户登录
             2) 作业调度
             3) 提供服务
             4) 应用请求
          进程的创建过程
             一旦操作系统发现了要求创建新进程的事件后,便调用进程创建原语Creat()按下述步骤创建一个新 进程。
                  1) 申请空白PCB。为新进程申请获得唯一的数字标识符,并从PCB集合中索取一个空白PCB。
                  2) 为新进程分配资源。
                  3) 初始化进程控制块。PCB的初始化包括:
                          ①初始化标识信息,将系统分配的标识符和父进程标识符,填入新的PCB中。
                          ②初始化处理机状态信息,使程序计数器指向程序的入口地址,使栈指针指向栈顶。
                          ③初始化处理机控制信息,将进程的状态设置为就绪状态或静止就绪状态,对于优先级,通常是将它设置为最低优先级,除非用户以显式的方式提出高优先级要求。
                  4) 将新进程插入就绪队列,如果进程就绪队列能够接纳新进程,便将新进程插入到就绪队列中。


     
    进程终止
        引起进程终止的事件
            1)正常结束
            2)异常结束 
            3)外界干预
        进程的终止过程
            如果系统发生了上述要求终止进程的某事件后,OS便调用进程终止原语,按下述过程去终止指定的进程。
                 1)根据被终止进程的标识符,从PCB集合中检索出该进程的PCB,从中读出该进程状态。
                 2)若被终止进程正处于执行状态,应立即终止该进程的执行,并置调度标志为真。用于指示该进程被终止后应重新进行调度。
                 3)若该进程还有子孙进程,还应将其所有子孙进程予以终止,以防他们成为不可控的进程。
                 4)将被终止的进程所拥有的全部资源,或者归还给其父进程,或者归还给系统。
                 5)将被终止进程(它的PCB)从所在队列(或链表)中移出,等待其它程序来搜集信息。


    阻塞唤醒
        1.引起进程阻塞和唤醒的事件
             1)请求系统服务
             2)启动某种操作
             3)新数据尚未到达
             4)无新工作可做
        2.进程阻塞过程
             正在执行的进程,当发现上述某事件后,由于无法继续执行,于是进程便通过调用阻塞原语block把自己阻塞。可见,进程的阻塞是进程自身的一种主动行为。进入block过程后,由于此时该进程还处于执                行状态,所以应先立即停止执行,把进程控制块中的现行状态由执行改为阻塞,并将PCB插入阻塞队列。如果系统中设置了因不同事件而阻塞的多个阻塞队列,则应将本进程插入到具有相同事件的阻塞(等待)队列。最后,转调度程序进行重新调度,将处理机分配给另一就绪进程,并进行切换,亦即,保留被阻塞进程的处理机状态(在PCB中),再按新进程的PCB中的处理机状态设置CPU环境。
        3.进程唤醒过程
             当被阻塞的进程所期待的事件出现时,如I/O完成或者其所期待的数据已经到达,则由有关进程(比如,              用完并释放了该I/O设备的进程)调用唤醒原语wakeup(),将等待该事件的进程唤醒。唤醒原语执行 的过程是:首先把被阻塞的进程从等待该事件的阻塞队列中移出,将其PCB中的现行状态由阻塞改为就             绪,然后再将该PCB插入到就绪队列中。

5.进程调度的算法及思想

 

5.1先来先服务调度算法

  先来先服务(FCFS)调度算法是一种最简单的调度算法,该算法既可用于作业调度,也可用于进程调度。当在作业调度中采用该算法时,

每次调度都是从后备作业队列中选择一个或多个最先进入该队列的作业,将它们调入内存,为它们分配资源、创建进程,然后放入就绪

队列。在进程调度中采用FCFS算法时,则每次调度是从就绪队列中选择一个最先进入该队列的进程,为之分配处理机,使之投入运行。

该进程一直运行到完成或发生某事件而阻塞后才放弃处理机。

 
来看一个例子,假设有三个进程和它们各自执行时间(以毫秒为单位)如下表:
 
 
那么如果三个进程按照P1, P2, P3的顺序启动的话,按照先到先服务的调度算法,执行过程如下:
 
 
  平均等待时间就是(0 + 24 + 27) / 3 = 17毫秒。FCFS算法是非抢占式的,一旦内核将CPU分配给一个进程就不会被释放
了,除非进程结束或者请求I/O阻塞。这也是我们之前学习的多任务系统的特点。

 

5.2基于优先级调度 (Priority Scheduling)

  在优先级调度算法中,每个进程都关联一个优先级,内核将CPU分配给最高优先级的进程。具有相同优先级的进程,按照
先来先服务的原则进行调度。假设进程的执行时间和优先级如下,并且这些进程同时存在于内存中时,内核基于优先级的
调度过程如下:
 
 
 
  采取基于优先级调度算法要考虑进程饿死的问题,因为高优先级的进程总是会被优先调度,具有低优先级的进程可能永远
都不会被内核调度执行。Aging是对于这个问题的一个解决方案,所谓Aging就是指逐渐提高系统中长时间等待的进程的
优先级。比如如果优先级的范围从127到0(127表示最低优先级),那么我们可以每15分钟将等待进程的优先级加1。最终
经过一段时间,即便是拥有最低优先级127的进程也会变成系统中最高优先级的进程,从而被执行。
 
  优先级调度可以抢占式或者非抢占式的。当一个进程在Ready队列中时,内核将它的优先级与正在CPU上执行的进程的优先级
进行比较。当发现这个新进程的优先级比正在执行的进程高时:对于抢占式内核,新进程会抢占CPU,之前正在执行的进程
转入Ready队列;对于非抢占式内核,新进程只会被放置在Ready队列的头部,不会抢占正在执行的进程。

 

 

5.3短进程优先(SCBF--Shortest CPU Burst First)

  最短CPU运行期优先调度算法(SCBF--Shortest CPU Burst First)
该算法从就绪队列中选出下一个“CPU执行期最短”的进程,为之分配处理机
最短作业优先调度是优先级调度的特例。在优先级调度中我们根据进程的优先级来进行调度,在最短作业优先调度中我们
根据作业的执行时间长短来调度。通过下面的例子来看看SJF是怎样调度的。
 
 
 
  进程1首先执行了1毫秒,当执行时间更短的进程2进入Ready队列时发生抢占。进程3在进程2执行1毫秒后到来,但是进程3的
执行时间比进程2长。同理进程4的到来也没法抢占进程2,所以进程2可以一直执行到结束。之后是执行时间第二短的进程4
执行,最后是进程1(要看剩余执行时间,还剩7毫秒)和进程3。
 
  SJF调度是最优的调度,但难点在于如何预测进程的执行时间(Burst Time)。对于批处理系统中的长期调度来说,可以将用户
提交进程时输入的执行时间上限作为依据。但对于短期调度来说,没有办法能够提前得知下一个要被分配CPU的进程的执行
时间长短。我们只能通过历史数据来进行预测,公式如下:
 
 
α可以取0.5,公式前半部分表示最近一次Burst Time,而后半部分表示过去历史平均的Burst Time。
该算法虽可获得较好的调度性能,但难以准确地知道下一个CPU执行期,而只能根据每一个进程的执行历史来预测。
 

5.4轮转法 (Round-Robin Scheduling) (RR)

  前几种算法主要用于批处理系统中,不能作为分时系统中的主调度算法,在分时系统中,都采用时间片轮转法。
简单轮转法:系统将所有就绪进程按FIFO规则排队,按一定的时间间隔把处理机分配给队列中的进程。这样,就绪
队列中所有进程均可获得一个时间片的处理机而运行。多级队列方法:将系统中所有进程分成若干类,每类为一级。
  RR调度算法转为分时系统设计,它与FCFS很像,但是加入了抢占。具体调度过程是:内核从Ready队列中选取第一个进程,
将CPU资源分配给它,并且设置一个定时器在一个时间片后中断该进程,调度Ready队列中的下一进程。很明显,RR调度
算法是抢占式的,并且在该算法的调度下,没有一个进程能够连续占用CPU超过一个时间片,从而达到了分时的目的。
 
来看下面的例子,假设一个时间片的长度为4毫秒:
 
 
 
 

5.5高响应比优先调度算法

  (1) 如果作业的等待时间相同,则要求服务的时间愈短,其优先权愈高,因而该算法有利于短作业.
  (2) 当要求服务的时间相同时,作业的优先权决定于其等待时间,等待时间愈长,其优先权愈高,因而它实现的是先来先服务.
  (3) 对于长作业,作业的优先级可以随等待时间的增加而提高,当其等待时间足够长时,其优先级便可升到很高, 从而也可获得处理机.
  该算法照顾了短作业,且不会使长作业长期得不到服务

 

6,linux内核分析之调度算法

inux上主要有两大类调度算法,CFS(完全公平调度算法)和实时调度算法。宏SCHED_NOMAL主要用于CFS调度,而SCHED_FIFO和SCHED_RR主要用于实时调度。如下面的宏定义:

/* 
 * Scheduling policies 
 */  
 /*支援Real-Time Task的排程,包括有SCHED_FIFO與SCHED_RR.  
 */  
   
 /*(也稱為SCHED_OTHER): 主要用以排程 
 一般目的的Task.*/  
#define SCHED_NORMAL        0  
#define SCHED_FIFO      1  
/*task預設的 Time Slice長度為100 msecs*/  
#define SCHED_RR        2  
/*主要用以讓Task可以延長執行的時間 
(Time Slice),減少被中斷發生Task Context-Switch 
的次數.藉此可以提高 Cache的利用率  
(每次Context-Switch都會導致Cache-Flush). 比 
較適合用在固定週期執行的Batch Jobs任 
務主機上,而不適合用在需要使用者互 
動的產品 (會由於Task切換的延遲,而 
感覺到系統效能不佳或是反應太慢).*/  
#define SCHED_BATCH     3  
/* SCHED_ISO: reserved but not implemented yet */  
/*為系統中的Idle Task排程.*/  
#define SCHED_IDLE      5  

linux调度算法实现的高层数据结构主要有运行实体、调度类、运行队列,下面我们主要看看这几个数据结构的字段和意义。

运行实体,rq结构体每个cpu有一个,主要存储一些基本的用于调度的信息,包括实时调度的和CFS调度的调度类,sched_class为对模块编程的上层支持,对于每个linux新添加进来的调度算法都需要有自己的调度类实例。


 /*每个处理器都会配置一个rq*/  
struct rq {  
    /* runqueue lock: */  
    spinlock_t lock;  
  
    /* 
     * nr_running and cpu_load should be in the same cacheline because 
     * remote CPUs use both these fields when doing load calculation. 
     */  
     /*用以记录目前处理器rq中执行task的数量*/  
    unsigned long nr_running;  
    #define CPU_LOAD_IDX_MAX 5  
    /*用以表示处理器的负载,在每个处理器的rq中 
    都会有对应到该处理器的cpu_load参数配置,在每次 
    处理器触发scheduler tick时,都会呼叫函数 
    update_cpu_load_active,进行cpu_load的更新。在系统初始化的时候 
    会呼叫函数sched_init把rq的cpu_load array初始化为0. 
    了解他的更新方式最好的方式是通过函数update_cpu_load,公式如下澹? 
    cpu_load[0]会直接等待rq中load.weight的值。 
    cpu_load[1]=(cpu_load[1]*(2-1)+cpu_load[0])/2 
    cpu_load[2]=(cpu_load[2]*(4-1)+cpu_load[0])/4 
    cpu_load[3]=(cpu_load[3]*(8-1)+cpu_load[0])/8 
    cpu_load[4]=(cpu_load[4]*(16-1)+cpu_load[0]/16 
    呼叫函数this_cpu_load时,所返回的cpu load值是cpu_load[0] 
    而在进行cpu blance或migration时,就会呼叫函数 
    source_load target_load取得对该处理器cpu_load index值, 
    来进行计算*/  
    unsigned long cpu_load[CPU_LOAD_IDX_MAX];  
#ifdef CONFIG_NO_HZ  
    unsigned long last_tick_seen;  
    unsigned char in_nohz_recently;  
#endif  
    /* capture load from *all* tasks on this cpu: */  
    /*load->weight值,会是目前所执行的schedule entity的 
    load->weight的总和,也就是说rq的load->weight越高, 
    也表示所负责的排程单元load->weight总和越高 
    表示处理器所负荷的执行单元也越重*/  
    struct load_weight load;  
    /*在每次scheduler tick中呼叫update_cpu_load时, 
    这个值就增加一,可以用来反馈目前cpu 
    load更新的次数*/  
    unsigned long nr_load_updates;  
    /*用来累加处理器进行context switch的次数,会在 
    函数schedule呼叫时进行累加,并可以通过函数 
    nr_context_switches统计目前所有处理器总共的context switch 
    次数,或是可以透过查看档案/proc/stat中的ctxt位得知目前 
    整个系统触发context switch的次数*/  
    u64 nr_switches;  
      
    u64 nr_migrations_in;  
    /*为cfs fair scheduling class 的rq*/  
    struct cfs_rq cfs;  
    /*为real-time scheduling class 的rq*/  
    struct rt_rq rt;  
      
/*用以支援可以group cfs tasks的机制*/  
#ifdef CONFIG_FAIR_GROUP_SCHED  
    /* list of leaf cfs_rq on this cpu: */  
    /*在有设置fair group scheduling 的环境下, 
    会基于原本cfs rq中包含有若干task的group 
    所成的排程集合,也就是说当有一个group a 
    就会有自己的cfs rq用来排程自己所属的tasks, 
    而属于这group a的tasks所使用到的处理器时间 
    就会以这group a总共所分的的时间为上限。 
    基于cgroup的fair group scheduling 架构,可以创造出 
    有阶层性的task组织,根据不同task的功能群组化 
    在配置给该群主对应的处理器资源,让属于 
    该群主下的task可以透过rq机制排程。使用属于 
    该群主下的资源。 
    这个变数主要是管理CFS RQ list,操作上可以透过函数 
    list_add_leaf_cfs_rq把一个group cfs rq加入到list中,或透过 
    函数list_del_leaf_cfs_rq把一个group cfs rq移除,并可以 
    透过for_each_leaf_cfs_rq把一个rq上得所有leaf cfs_rq走一遍 
    */  
    struct list_head leaf_cfs_rq_list;  
#endif  
/*用以支援可以group real-time tasks的机制*/  
#ifdef CONFIG_RT_GROUP_SCHED  
    /*类似leaf_cfs_rq_list所扮演的角色,只是这里 
    是针对属于real-time的task,在实际操作上可以 
    透过函数list_add_leaf_rt_rq,list_del_leaf_rt_rq或 
    巨集for_each_leaf_rt_rq*/  
    struct list_head leaf_rt_rq_list;  
#endif  
  
    /* 
     * This is part of a global counter where only the total sum 
     * over all CPUs matters. A task can increase this counter on 
     * one CPU and if it got migrated afterwards it may decrease 
     * it on another CPU. Always updated under the runqueue lock: 
     */  
     /*一般来说,linux kernel 的task状态可以为TASK_RUNNING 
     TASK_INTERRUPTIBLE(sleep), 
     TASK_UNINTERRUPTIBLE(Deactivate Task,此时Task会从rq中 
     移除)或TASK_STOPPED. 
     透过这个变数会统计目前rq中有多少task属于 
     TASK_UNINTERRUPTIBLE的状态。当呼叫函数 
     active_task时,会把nr_uninterruptible值减一,并透过 该函数 
    enqueue_task把对应的task依据所在的scheduling class 
    放在 对应的rq中,并把目前rq中nr_running值加一*/  
    unsigned long nr_uninterruptible;  
    /*curr:指向目前处理器正在执行的task; 
    idle:指向属于idle-task scheduling class 的idle task; 
    stop:指向目前最高等级属于stop-task scheduling class 
    的task;*/  
    struct task_struct *curr, *idle;  
    /*基于处理器的jiffies值,用以记录下次进行处理器 
    balancing 的时间点*/  
    unsigned long next_balance;  
    /*用以存储context-switch发生时,前一个task的memory management 
    结构并可用在函数finish_task_switch中,透过函数mmdrop释放前一个 
    task的记忆体资源*/      
    struct mm_struct *prev_mm;  
    /*用以记录目前rq的clock值,基本上该值会等于透过sched_clock_cpu 
    (cpu_of(rq))的回传值,并会在每次呼叫scheduler_tick时透过 
    函数update_rq_clock更新目前rq clock值。 
    在实作部分,函数sched_clock_cpu会透过sched_clock_local或 
    ched_clock_remote取得对应的sched_clock_data,而处理的sched_clock_data 
    值,会透过函数sched_clock_tick在每次呼叫scheduler_tick时进行更新; 
    */  
    u64 clock;  
    /*用以记录目前rq中有多少task处于等待i/o的sleep状态 
    在实际的使用上,例如当driver接受来自task的调用,但处于等待i/o 
    回复的阶段时,为了充分利用处理器的执行资源,这时 
    就可以在driver中呼叫函数io_schedule,此时 
    就会把目前rq中的nr_iowait加一,并设定目前task的io_wait为1 
    然后触发scheduling 让其他task有机会可以得到处理器执行时间*/  
    atomic_t nr_iowait;  
  
#ifdef CONFIG_SMP  
    /*root domain是基于多核心架构下的机制, 
    会由rq结构记住目前采用的root domain,其中包括了 
    目前的cpu mask(包括span,online rt overload), reference count 跟cpupri 
    当root domain有被rq参考到时,refcount 就加一,反之就减一。而cpu 
    mask span表示rq可挂上的cpu mask,noline为rq目前已经排程的 
    cpu mask cpu上执行real-time task.可以参考函数pull_rt_task,当一个rq中属于 
    real-time的task已经执行完毕,就会透过函数pull_rt_task从该 
    rq中属于rto_mask cpu mask 可以执行的处理器上,找出是否有一个处理器 
    有大于一个以上的real-time task,若有就会转到目前这个执行完成 
    real-time task 的处理器上 
    而cpupri不同于Task本身有区分140個(0-139) 
    Task Priority (0-99為RT Priority 而 100-139為Nice值 -20-19).  
    CPU Priority本身有102個Priority (包括,-1 為Invalid, 
    0為Idle,1為Normal,2-101對應到Real-Time Priority 0-99). 
    參考函式convert_prio, Task Priority如果是 140就會對應到 
    CPU Idle,如果是大於等於100就會對應到CPU Normal, 
    若是Task Priority介於0-99之間,就會對應到CPU Real-Time Priority 101-2之間.)  
    在實際的操作上,例如可以透過函式cpupri_find 
    帶入一個要插入的Real-Time Task,此時就會依據cpupri中 
    pri_to_cpu選擇一個目前執行Real-Time Task且該Task 
    的優先級比目前要插入的Task更低的處理器, 
    並透過CPU Mask(lowest_mask)返回目前可以選擇的處理器Mask. 
    實作的部份可以參考檔案kernel/sched_cpupri.c. 
    在初始化的過程中,會透過函式sched_init呼叫函式init_defrootdomain, 
    對Root Domain與 CPU Priority機制進行初始化. 
    */  
    struct root_domain *rd;  
    /*Schedule Domain是基於多核心架構下的機制. 
    每個處理器都會有一個基礎的Scheduling Domain, 
    Scheduling Domain可以有階層性的架構,透過parent 
    可以找到上一層的Domain,或是透過child找到 
    下一層的 Domain (NULL表示結尾.).並可透過span 
    栏位,表示這個Domain所能涵蓋的處理器範圍. 
    通常Base Domain會涵蓋系統中所有處理器的個數, 
    而Child Domain所能涵蓋的處理器個數不超過它的 
    Parent Domain. 而當在進行Scheduling Domain 中的Task Balance 
    時,就會以該Domain所能涵蓋的處理器為最大範圍. 
    同時,每個Schedule Domain都會包括一個或一個以上的 
    CPU Groups (結構為struct sched_group),並透過next變數把 
    CPU Groups串連在一起(成為一個單向的Circular linked list), 
    每個CPU Group都會有變數cpumask來定义這個CPU Group 
    所涵蓋的處理器範圍.並且CPU Group所包括的處理器 
    範圍,必需涵蓋在所屬的Schedule Domain處理器範圍中. 
    當進行Scheduling Domain的Balancing時,會以其下的CPU Groups 
    為單位,根據cpu_power (會是該Group所涵蓋的處理器 
    Tasks Loading的總和)來比較不同的CPU Groups的負荷, 
    以進行Tasks的移動,達到Balancing的目的. 
    在有支援SMP的架構下,會在函式sched_init中,呼叫open_softirq, 
    註冊 SCHED_SOFTIRQ Software IRQ与其对应的 Callback函式  
    run_rebalance_domains. 並會在每次呼叫函式scheduler_tick時, 
    透過函式trigger_load_balance确认是否目前的jiffies值已經 
    大於RunQueue下一次要觸發Load Balance的next_balance時間值, 
    並透過函式raise_softirq觸發SCHED_SOFTIRQ Software IRQ.  
    在Software IRQ觸發後,就會呼叫函式run_rebalance_domains, 
    並在函式rebalance_domains中,進行后续處理器上的 
    Scheduling Domain Load Balance動作. 
    有關Scheduling Domain進一步的內容,也可以參考 
    Linux Kernel文件 Documentation/scheduler/sched-domains.txt. 
    */  
    struct sched_domain *sd;  
    /*這值會等於函式idle_cpu的返回值,如果為1表示 
    目前CPU RunQueue中執行的為Idle Task. 反之為0, 
    則表示處理器執行的不是Idle Task (也就是說 
    處理器正在忙碌中.).*/  
    unsigned char idle_at_tick;  
    /* For active balancing */  
    /*若這值不為0,表示會有在Schedule排程動作 
    結束前,要呼叫的收尾函式. (实作為inline函式 
    post_schedule in kernel/sched.c),目前只有Real-Time Scheduling  
    Class有支援這個機制(會呼叫函式has_pushable_tasks  
    in kernel/sched_rt.c).*/  
    int post_schedule;  
    /*當RunQueue中此值為1,表示這個RunQueue正在進行 
    Fair Scheduling的Load Balance,此時會呼叫stop_one_cpu_nowait 
    暫停該RunQueue所屬處理器的排程,並透過函式 
    active_load_balance_cpu_stop,把Tasks從最忙碌的處理器, 
    移到Idle的處理器上執行.*/  
    int active_balance;  
    /*用以儲存目前進入Idle且負責進行 Load Balance 
    流程的處理器ID. 呼叫的流程為,在呼叫函式schedule時, 
    若該處理器RunQueue的nr_running為0 (也就是目前沒有 
    正在執行的Task),就會呼叫idle_balance,並觸發後續Load  
    Balance流程.*/  
    int push_cpu;  
    /* cpu of this runqueue: */  
    /*用以儲存目前运作這個RunQueue的處理器ID*/  
    int cpu;  
    /*為1表示目前此RunQueue有在對應的處理器掛上 
    並執行.*/  
    int online;  
    /*如果RunQueue中目前有Task正在執行,這個值會 
    等於目前該RunQueue的Load Weight除以目前RunQueue 
    中Task數目的均值.  
    (rq->avg_load_per_task = rq->load.weight / nr_running;).*/  
    unsigned long avg_load_per_task;  
  
    struct task_struct *migration_thread;  
    struct list_head migration_queue;  
    /*這個值會由Real-Time Scheduling Class呼叫函式 
    update_curr_rt,用以統計目前Real-Time Task執行時間的 
    均值,在這函式中會以目前RunQueue的clock_task 
    減去目前Task執行的起始時間,取得執行時間的 
    Delta值. (delta_exec = rq->clock_task – curr->se.exec_start; ). 
    在透過函式sched_rt_avg_update把這Delta值跟原本 
    RunQueue中的rt_avg值取平均值. 以運作的週期來看, 
    這個值可反應目前系統中Real-Time Task平均被 
    分配到的執行時間值.*/  
    u64 rt_avg;  
    /*這個值主要在函式sched_avg_update更新,以笔者手中 
    的Linux Kernel 2.6.38.6的實作來說,當RunQueue Clock 
    減去age_stamp大於 0.5秒 (=sched_avg_period),就會把這值 
    累加0.5秒 (單位都是nanoseconds). 從函式scale_rt_power 
    的實作來說,age_stamp值離RunQueue Clock越遠,表示total 
    值越大,available值也越大,而函式scale_rt_power返回的 
    div_u64計算結果也越大,最終 RunQueue的cpu_power 
    與Scheduling Domain中的Scheduling Group的cpu_power 
    值也就越大. (可參考函式update_cpu_power的實作).*/  
    u64 age_stamp;  
    /*這值會在觸發Scheduling時,若判斷目前處理器 
    RunQueue沒有正在運作的Task,就會透過函式 
    idle_balance更新這值為為目前RunQueue的clock值. 
    可用以表示這個處理器是何時進入到Idle的 
    狀態*/  
    u64 idle_stamp;  
    /*會在有Task運作且idle_stamp不為0 (表示前一個 
    狀態是在Idle)時以目前RunQueue的clock減去 
    idle_stmp所計算出的Delta值為依據,更新這個值 
    . 可反應目前處理器進入Idle狀態的時間長短*/  
    u64 avg_idle;  
#endif  
  
    /* calc_load related fields */  
    /*用以記錄下一次計算CPU Load的時間,初始值 
    為目前的jiffies加上五秒與1次的Scheduling Tick的 
    間隔 (=jiffies + LOAD_FREQ,且LOAD_FREQ=(5*HZ+1))*/  
    unsigned long calc_load_update;  
    /*會等於RunQueue中nr_running與nr_uninterruptible的 
    總和.(可參考函式calc_load_fold_active).*/  
    long calc_load_active;  
  
#ifdef CONFIG_SCHED_HRTICK  
#ifdef CONFIG_SMP  
    /*在函式init_rq_hrtick初始化RunQueue High-Resolution  
    Tick時,此值預設為0. 
    在函式hrtick_start中,會判斷目前觸發的RunQueue 
    跟目前處理器所使用的RunQueue是否一致, 
    若是,就直接呼叫函式hrtimer_restart,反之就會 
    依據RunQueue中hrtick_csd_pending的值,如果 
    hrtick_csd_pending為0,就會透過函式 
    __smp_call_function_single讓RunQueue所在的另一個 
    處理器執行rq->hrtick_csd.func 所指到的函式  
    __hrtick_start. 並等待該處理器執行完畢後, 
    才重新把hrtick_csd_pending設定為1. 
    也就是說, RunQueue的hrtick_csd_pending是用來作為 
    SMP架構下,由處理器A觸發處理器B執行 
    _hrtick_start函式的一個保護機制.而有關在 
    SMP下如何由一個處理器觸發另一個處理器 
    執行函式的機制,可以參考kernel/smp.c中 
    相關smp_call_function_xxxxxxx的實作.s*/  
    int hrtick_csd_pending;  
    /*用以儲存hrtick機制中,要跨處理器執行的 
    函式結構.*/  
    struct call_single_data hrtick_csd;  
#endif  
    /*為High-Resolution Tick的结构,會透過函式 
    hrtimer_init初始化.*/  
    struct hrtimer hrtick_timer;  
#endif  
  
#ifdef CONFIG_SCHEDSTATS  
    /* latency stats */  
    /*為Scheduling Info.的統計結構,可以參考 
    include/linux/sched.h中的宣告. 例如在每次觸發 
    Schedule時,呼叫函式schedule_debug對上一個Task 
    的lock_depth進行確認(Fork一個新的Process 時, 
    會把此值預設為-1就是No-Lock,當呼叫 
    Kernel Lock時, 就會把Current Task的lock_depth加一.), 
    若lock_depth>=0,就會累加Scheduling Info.的bkl_count值, 
    用以代表Task Blocking的次數.*/  
    struct sched_info rq_sched_info;  
    /*可用以表示RunQueue中的Task所得到CPU執行 
    時間的累加值. 
    在發生Task Switch時,會透過sched_info_switch呼叫 
    sched_info_arrive並以目前RunQueue Clock值更新 
    Task 的sched_info.last_arrival時間,而在Task所分配時間 
    結束後,會在函式sched_info_depart中以現在的 
    RunQueue Clock值減去Task的sched_info.last_arrival 
    時間值,得到的 Delta作為變數rq_cpu_time的累 
    加值.*/  
    unsigned long long rq_cpu_time;  
    /* could above be rq->cfs_rq.exec_clock + rq->rt_rq.rt_runtime ? */  
  
    /* sys_sched_yield() stats */  
    /*用以統計呼叫System Call sys_sched_yield的次數.*/  
    unsigned int yld_count;  
  
    /* schedule() stats */  
    unsigned int sched_switch;  
    /*可用以統計觸發Scheduling的次數. 在每次觸發 
    Scheduling時,會透過函式schedule呼叫schedule_debug, 
    呼叫schedstat_inc對這變數進行累加.*/  
    unsigned int sched_count;  
    /*可用以統計進入到Idle Task的次數. 會在函式 
    pick_next_task_idle中,呼叫schedstat_inc對這變數進行 
    累加.*/  
    unsigned int sched_goidle;  
  
    /* try_to_wake_up() stats */  
    /*用以統計Wake Up Task的次數.*/  
    unsigned int ttwu_count;  
    /*用以統計Wake Up 同一個處理器Task的次數.*/  
    unsigned int ttwu_local;  
  
    /* BKL stats */  
    unsigned int bkl_count;  
#endif  
};  
参考文献:https://blog.csdn.net/sangliu/article/details/8476770

       https://blog.csdn.net/yujingbo1023/article/details/41924419

     https://www.cnblogs.com/hanxiaoyu/p/5576277.html

        https://blog.csdn.net/bullbat/article/details/7160246 

posted @ 2018-05-01 18:25  jmu-lcy  阅读(309)  评论(0编辑  收藏  举报