第一次作业:基于Linux 2.6.22的进程模型和O(1)调度器

时间:2022-07-21 16:43:18

1.简介

  本文主要基于Linux kernel v2.6.22的源代码,分析该版本的进程模型以及CFS调度器算法。

  Linux kernel v2.6.22源代码的链接地址:https://elixir.bootlin.com/linux/v2.6.22/source/kernel

2.进程

2.1进程的含义

    进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

    在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。

    程序是指令、数据及其组织形式的描述,进程是程序的实体。第一次作业:基于Linux 2.6.22的进程模型和O(1)调度器

    只要我们打开电脑的任务管理器,我们就可以看到各种各样的进程,每个进程都关联着一项任务。

第一次作业:基于Linux 2.6.22的进程模型和O(1)调度器

    在Linux操作系统下,我们通过ps指令实现进程的查看

第一次作业:基于Linux 2.6.22的进程模型和O(1)调度器

 

2.2进程的组成与特征

(1)在Linux系统中进程由以下三部分组成:进程控制块PCB ;数据段 ;程序段。

(2)(i)动态性(ii)并发性(iii)独立性(iv)异步性(iv)结构性

2.3进程的组织 

      所有的进程都被放在一个叫做进程控制块( PCB )的数据结构中,可以理解为进程属性的集合,该控制块由操作系统创建和管理。

每个进程在内核中都有一个进程控制块来维护进程相关的信息,Linux内核的进程控制块是 task_struct 结构体,每个进程都

把他的信息放在 task_struct 这个数据结构里,并且可以在include/linux/sched.h 这个目录结构中找到它。所有运行在

系统里的进程都以 task_struct 链表的形式存在内核当中。该结构体包含以下内容:

2.3.1PCB的作用

  (1)当操作系统要调度某进程执行时,要从该进程的PCB中查看其当前状态和优先级。

  (2)当调度某进程后,要根据PCB中保存的CPU状态信息,设置进程恢复运行的现场,并根据PCB中程序和数据的内存地址找到程序和数据。

  (3)当进程阻塞或挂起时,要将其断点的CPU环境保存在PCB中。

     (4)进程之间实现同步、通信、访问文件时,需要访问PCB。
 
2.3.2PCB的内容

1.进程标示符(PID)

    这是描述本进程的唯一标示符,用来区分其他进程。其中父进程id(PPID)

pid_t pid;    //进程号
pid_t tgid;   //进程组号

     当CONFIG_BASE_SMALL等于0时,PID的取值范围是0到0x8000-1,其中0x8000代表16进制下的32768,

即PID的取值范围是0~32767。也就是说,在Linux操作系统中可以有32768个进程。

 /* linux-2.6.20/include/linux/threads.h */  
 #define PID_MAX_DEFAULT (CONFIG_BASE_SMALL ? 0x1000 :0x8000)

    在Linux系统中,一个线程组中的所有线程使用和该线程组的领头线程相同的PID,并被存放在tgid成员中。

只有线程组的领头线程的PID成员才会被设置为与tgid相同的值。

2.进程状态

   Linux定义了一个长整形变量,来保存进程的状态。至于在 state 变量前面添加 volatile 这个关键词是为了告诉

编译器不要对其优化。编译器有一个缓存优化的习惯,比如说:第一次在内存取数,编译器发现后面还要用这个变量,

于是把这个变量的值就放在寄存器中。这个关键词就是要求编译器不要进行优化,每次都让CPU到内存中取数,

以确保进程的状态的变化能够及时地反映出来。

volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */

  接下来简单介绍一下进程的状态:

 1 #define TASK_RUNNING        0
 2 #define TASK_INTERRUPTIBLE    1
 3 #define TASK_UNINTERRUPTIBLE    2
 4 #define TASK_STOPPED        4
 5 #define TASK_TRACED        8
 6 /* in tsk->exit_state */
 7 #define EXIT_ZOMBIE        16
 8 #define EXIT_DEAD        32
 9 /* in tsk->state again */
10 #define TASK_NONINTERACTIVE    64
11 #define TASK_DEAD        128

(1) TASK_RUNNING  表示进程要么正在执行,要么正在准备执行。

(2) TASK_INTERRUPTIBLE 表示进程被阻塞(睡眠),只有当某个条件是TRUE时,其状态相应的设置为 TASK_RUNNING  。它可以被信号和 wake_up() 唤醒。

(3) TASK_UNINTERRUPTIBLE 表示进程被阻塞(睡眠),只有当某个条件是TRUE时,其状态相应的设置为 TASK_RUNNING  。但是它只能被 wake_up() 唤醒。

(4) TASK_STOPPED 表示进程被停止执行。

(5) TASK_TRACED  表示进程被 debugger 等进程监视着。

(6) EXIT_ZOMBIE  表示进程的执行被终止,但是其父进程还没有使用 wait() 等系统调用来获知它的终止信息。

(7) EXIT_DEAD  表示进程的最终状态。

3.进程的优先级

在Linux内核下为每个进程分配了时间片并根据其优先级进行调度。当进程被创建时,在 task_struct 里包含了以下几个进程。

 int prio, static_prio, normal_prio;

 除此之外,在内核头文件 include/linux/sched.h 中定义了如下宏

#define MAX_USER_RT_PRIO    100
#define MAX_RT_PRIO        MAX_USER_RT_PRIO
#define MAX_PRIO        (MAX_RT_PRIO + 40)

    内核中规定进程的优先级范围为[0,MAX_PRIO-1]。其中实时任务的优先级范围是[0,MAX_RT_PRIO-1],

非实时任务的优先级范围是[MAX_RT_PRIO,MAX_PRIO-1]。优先级值越小,意味着级别越高,任务先被内核调度。

(1) prio 指的是任务当前的动态优先级,其值影响任务的调度顺序。

(2) normal_prio 指的是任务的常规优先级,其值基于 static_prio 和调度策略计算。

(3) static_prio 指的是任务的静态优先级,在进程创建时分配,该值会影响分配给任务的时间片的长短和非实时任务的动态优先级的计算。

3.进程的状态转换图

    不同的操作系统对进程的状态解释不同,但是最基本的状态都是一样的。

(1)运行态:进程占用CPU,并在CPU上运行;

(2)就绪态:进程已经具备运行条件,但是CPU还没有分配过来;

(3)阻塞态:进程因等待某件事发生而暂时不能运行;

  进程在一生中,都处于这3种状态之一。

第一次作业:基于Linux 2.6.22的进程模型和O(1)调度器

 

运行-->就绪:这是有调度引起的,主要是进程占用CPU的时间过长;

就绪-->运行:运行的进程的时间片用完,调度就转到就绪队列中选择合适的进程分配CPU;

运行-->阻塞:发生了I/O请求或等待某事件的发生;

阻塞-->就绪:进程所等待的事件发生,就进入就绪队列。

第一次作业:基于Linux 2.6.22的进程模型和O(1)调度器

 上图就是Linux操作系统的进程转换图。

4.O(1)调度器的实现

 4.1概述

    要谈到O(1)调度器,就从它的前身说起,也就是O(n)调度器。

      调度器采用基于优先级的设计,它对 Runqueue 中所有进程的优先级依次进行比较,选择最高优先级的进程作为

下一个被调度的进程。( Runqueue 是Linux内核中保存所有就绪进程的对列)就是说,在每次进程切换时,内核

扫描可运行进程的链表,计算优先级,然后选择“最佳”进程来运行。

      但是,该调度器的可扩展性不好,调度器选择进程时需要遍历整个 Runqueue 队列,在从中选出最佳进程,因此

该算法的执行时间与进程数成正比。导致系统整体的性能下降。

     从名字就可以看出来O(1)调度器主要解决了O(n)版本中的扩展性问题。O(1)调度算法所花费的时间为常数,

与当前系统中的进程个数无关。O(1)调度器跟踪运行队列中可运行的任务(实际上,每个优先级水平有两个运行队列,

一个用于活动任务,一个用于过期任务),这意味着要确定接下来执行的任务,调度器只需按优先级将下一个任务从特定活动的运行队列中取出即可

4.2调度器运用到的数据结构

4.2.1Runqueue队列

    rq结构是每个CPU上的主要的运行队列数据结构。

     主要的结构是优先级数组。

struct rq {
    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.
     */
    unsigned long nr_running;
    unsigned long raw_weighted_load;
#ifdef CONFIG_SMP
    unsigned long cpu_load[3];
    unsigned char idle_at_tick;
#ifdef CONFIG_NO_HZ
    unsigned char in_nohz_recently;
#endif
#endif
    unsigned long long nr_switches;
    /*
     * 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:
     */
    unsigned long nr_uninterruptible;

    unsigned long expired_timestamp;
    /* Cached timestamp set by update_cpu_clock() */
    unsigned long long most_recent_timestamp;
    struct task_struct *curr, *idle;
    unsigned long next_balance;
    struct mm_struct *prev_mm;
    struct prio_array *active, *expired, arrays[2];
  ......
};

 

4.2.2优先级数组

    该结构体中有一个用来表示进程动态优先级的数组queue,它包括了每一种优先级进程所形成的链表。

#define MAX_USER_RT_PRIO	100
#define MAX_RT_PRIO MAX_USER_RT_PRIO #define MAX_PRIO (MAX_RT_PRIO + 40)
struct prio_array {
    unsigned int nr_active;
    DECLARE_BITMAP(bitmap, MAX_PRIO+1); /* include 1 bit for delimiter */
    struct list_head queue[MAX_PRIO];
};

    因为进程优先级的最大值为139,因此 MAX_PRIO 的最大值取140(普通进程使用100到139的优先级,实时进程使用0到99的优先级)。

因此, queue 数组中包括140个可执行状态的进程链表,每一条优先级链表上的进程都具有同样的优先级,而不同进程链表上的进程都拥有不同的优先级。

struct bitmap {
    struct bitmap_page *bp;
    unsigned long pages; /* total number of pages in the bitmap */
    unsigned long missing_pages; /* number of pages not yet allocated */
    mddev_t *mddev; /* the md device that the bitmap is for */
    int counter_bits; /* how many bits per block counter */
    ......
  };

    除此之外, prio_array 结构中还包含一个优先级位图 bitmap 。该位图使用一个位(bit)来代表一个优先级,起初该位图中的

全部位置被置零,当某个优先级的进程处于可执行状态时,该优先级所相应的位就被置1。因此O(1)算法中查找系统优先级最高的

就转化成查找优先级位图中第一个被置1的位。

4.2.3活动进程和过期进程

     当处于执行态的进程用完时间片后就会处于就绪态。此时调度程序再从就绪态的进程中选择一个作为即将要执行的进程,在Linux中,就绪态和执行态统称为可执行态。

     对于可执行状态的进程,我们可以分为三类:首先是正处于执行状态的进程;其次,有一部分处于可执行状态的进程但还没使用完他们的时间片,它们等待被执行;最后,剩下的进程已经用完了自己的时间片,在其他进程还没使用完他们的时间片之前,它们不能在被执行。

     所以,活动进程就是指还没使用完时间片的进程;过期进程就是指已经用完时间片的进程。因此,调度程序的工作就是在活动进程集合中选取一个最佳优先级的进程,假设该进程时间片恰好用完,就将该进程放入过期进程集合当中。

     在可执行队列结构中,arrays数组的两个元素分别用来表示刚才所述的活动进程集合和过期进程集合, active 和 expired 两个指针分别直接指向这两个集合。

第一次作业:基于Linux 2.6.22的进程模型和O(1)调度器

4.3时间片的计算

      O(n)调度算法在每次进程切换时,内核依次扫描就绪队列上的进程,并计算每个进程的优先级,在选择出优先级最高的进程来执行,可知其时间复杂度为O(n)。

    但是O(1)调度算法能够在恒定的时间内为每一个进程又一次分配号时间片,并且在恒定的时间内能够选取一个最高优先级的进程,重要的是这两个过程都与系统中可执行的进程数无关。

   O(1)算法采用过期进程数组和活跃进程数组解决以往调度算法所带来的O(n)复杂度问题。过期数组中的进程都已经用完了时间片。而活跃数组的进程还拥有时间片。当一个进程用完自己的时间片后,它就被移动到过期进程数组中。同一时候这个过期进程在被移动之前就已经计算好了新的时间片。可以看出来O(1)调度算法采用分散计算时间片的方法。

   这时,只要活跃进程数组中没有可执行进程了,说明所有可执行进程都用完了他们的时间片,那么此时仅仅需要交换以下两个数组就可以讲过期进程切换为活跃进程。以下代码说明了两个数组间的交换:

if (unlikely(!array->nr_active)) {
        /*
         * Switch the active and expired arrays.
         */
        schedstat_inc(rq, sched_switch);
        rq->active = rq->expired;
        rq->expired = array;
        array = rq->active;
        rq->expired_timestamp = 0;
        rq->best_expired_prio = MAX_PRIO;
    }

     通过分散计算时间片、交换活跃和过期两个进程集合的方法能够使得O(1)算法在恒定的时间内为每一个进程有一次计算好时间片。

进程调度的本质就是在当前可执行的进程集合 中选择一个最佳的进程,这个最佳则是以进程的动态优先级为选取标准的。

    调度程序在选取最高优先级的进程时,首先利用优先级位图从高到低找到第一个被设置的位,该位对应这一条进程链表。这个链表中

的进是当前系统全部可执行进程中最高优先级的。在该优先级链表中选取第一个进程,它拥有最高的优先级。即为调度程序立即要执行的进程。

    上述进程的选取过程可用下述代码描述:

asmlinkage void __sched schedule(void)
{
    struct task_struct *prev, *next;
    struct prio_array *array;
    struct list_head *queue;
    unsigned long long now;
    unsigned long run_time;
    int cpu, idx, new_prio;
    long *switch_count;
    struct rq *rq;
        ......
        prev = current;
        array = rq->active;
    idx = sched_find_first_bit(array->bitmap);
    queue = array->queue + idx;
    next = list_entry(queue->next, struct task_struct, run_list);
        ......
        if (likely(prev != next)) {
        next->timestamp = next->last_ran = now;
        rq->nr_switches++;
        rq->curr = next;
        ++*switch_count;

        prepare_task_switch(rq, next);
        prev = context_switch(rq, prev, next);
        barrier();
        /*
         * this_rq must be evaluated again because prev may have moved
         * CPUs since it called schedule(), thus the 'rq' on its stack
         * frame will be invalid.
         */
        finish_task_switch(this_rq(), prev);
    }

    sched_find_first_bit()用于在位图中高速查找第一个被设置的位。假设prev和next不是一个进程。那么此时进程切换就开始运行。

通过上述的内容能够发现。在恒定的时间又一次分配时间片和选择一个最佳进程是Q(1)算法的核心。

 5.看法

    操作系统是管理计算机系统的全部硬件资源包括软件资源及数据资源;控制程序运行;改善人机界面;为其他应用软件提供支持等,使计算机系统所有资源最大限度地发挥作用,为用户提供方便的、有效的、友善的服务界面。

       进程管理即是操作系统对CPU的管理,为了提升CPU利用率,使用多道编程,便有了多进程维护管理。操作系统已实现了各管理功能,硬件CPU及一系列进程资源抽象成为了进程的概念,可以说进程算是操作系统的“无中生有”,应用程序的编程人员直接利用进程的机制,达到让应用程序高效利用硬件资源的目的。操作系统的学习过程中,学习和体会前人在解决问题的思路, 再具体到细节时,会发现数据结构以及算法的实现,对这两者的学习和认知也会很有帮助。可以说,操作系统的学习,不同的层次,由浅及深,都会有不同层次的收获。

6.参考资料     

https://blog.csdn.net/a2796749/article/details/47101533

https://blog.csdn.net/sdoyuxuan/article/details/69938743

https://blog.csdn.net/u010985058/article/details/75196295