调度方案综述

进程调度问题概述

在多道程序环境下,进程的数目往往多于处理机数目。这就要求系统能按照某种算法,动态的把处理机分配给就绪队列中的进程,使之执行。因此,处理机调度是操作系统设计的中心问题之一。进程调度问题的核心就是采用什么样的算法把处理机分配给进程。进程调度算法也是在任何操作系统中必须配置的一级调度。好的进程调度算法将有效的提高系统中各种资源利用率,减少处理机的空闲时间,避免部分作业长期得不到处理机响应等情况的发生。

动态优先级进程调度算法介绍

动态优先权调度算法,以就绪队列中各个进程的优先权作为进程调度的依据。各个进程的优先权在创建进程时所赋予,随着进程的推进或其等待时间的增加而改变。进程的优先权利用某一范围内的整数来表示。有的系统数值越小优先权越高,如Unix系统,有的系统则反之。采用该算法时,每次总是在就绪队列中选择一个优先权最高的进程进行调度,并将处理机分配给该进程。动态优先权调度算法又分为抢占式和非抢占式两种。本文采用 C 语言对非抢占式动态优先权调度算法进行了设计和实现。

数据结构设计

进程控制块 PCB
typedef struct pcb {
    unsigned int id;                // 进程 ID 号
    unsigned int static_priority;                // 静态优先数
unsigned int dynamic_priority;                // 动态优先数
    int state;                // 进程状态标识符
} pcb;

在 PCB 中,我们将优先级分为两部分,静态优先级和动态优先级。静态优先级在进程创建时则确定,而动态优先级则在进程的运行或等待过程中发生改变。在下文中,我们将静态优先级和动态优先级之和统称为优先级。

为避免违背系统、用户目的,我们规定动态优先数不能为负数,即动态优先数最小为 0,以免过多地降低进程总体优先级,导致重要进程等待时间过长。

进程有三种状态,分别为就绪态、阻塞态和完成态,通过常量体现在state属性上。

就绪队列
typedef struct sub_queue {
    struct sub_queue* previous;     // 指向前驱任务
    struct sub_queue* next;     // 指向后继任务
    int pid;                            // 当前任务 PID
} sub_queue;

typedef struct ready_queue {
    int max_priority;                       // 系统最高优先数
    sub_queue sub_queue_array[MAX_PRIO + 1];    // 就绪队列数组
    int priority_bit_map[MAX_PRIO + 1];          // 优先级位图 
} ready_queue;

我们定义就绪队列为 ready_queue 结构体,其由系统最高优先数、255个子就绪队列、优先级位图三部分组成;每个子就绪队列为 sub_queue 结构体,其由前驱任务指针、后继任务指针、当前任务PID指针组成。

其中的 sub_queue_array 存储了255个优先级的子就绪队列,其下标即为优先级,从 1 至 255 级,sub_queue_array[0] 存储了系统中的进程数量 n_proc 值。优先级位图 priority_bit_map 则存储了该下标优先级是否有进程,如有则为 1,反之为0值。

子就绪队列 sub_queue_array 的下标为 0 元素内容为当前系统进程数量,其余下标 n > 1 的元素内容为,优先级为n的就绪进程链表。例如优先级为3的就绪进程则有 prc_e、prc_g 和 prc_k 三个。

子就绪队列 sub_queue 为双向链表

上图代表系统中某个优先级的就绪队列。可以看到,子就绪队列是一个双向链表,任务 1,任务 2 都包含一个双向链表,就绪的任务与任务之间也是用双链表连接在一起。需要注意的是,任务 2 和优先级的头节点也是相连的。

如果一个优先级对应的子就绪队列中有多个任务,系统调度时总会选择链表头节点指向的第一个任务作为最高优先级任务先去运行,如上图中最高优先级的就是 task_a 任务。

在这里,我们的子就绪队列采用了双向链表,而不是采用了传统的单向链表,目的是加快任务插入到尾部的速度。

程序算法设计

算法设计思路
  1. 优先数增减原则

参考 Linux 的实现方式,经过简化,我们规定优先数改变的原则如下:

  1. 进程每运行一个时间片,动态优先数减 3

其次,PCB 的数据结构及操作采用数组方式,将系统的 N 个进程 PCB 信息保存到一个长度为 N 的 pcbs 数组中。

  1. 系统初始条件

若系统中有若干个进程,每个进程产生时间,优先级各不相同。

利用进程控制块 PCB 来描述各个进程,进程控制块PCB包括以下字段:

  1. 进程标识数 ID
  2. 静态优先数 STATIC_PRIORITY,规定优先数越大的进程,其优先级越高
  3. 动态优先数 DYNAMIC_PRIORITY,规定优先数越大的进程,其优先级越高
  4. 进程状态 STATE ,包括三种状态:就绪态、阻塞态、完成态

CPU 处理进程是从就绪队列中选择当前各进程中优先权最大的进程开始的。由于采用的是非抢占式调度算法,则当前进程执行完一个时间片之后有以下几种情况:

  1. 当前进程结束则退出系统,否则排到就绪队列尾或进入阻塞队列尾。
  2. 考虑阻塞队列中的进程是否能移入就绪队列中,若有则移入。
  3. 若就绪队列中有进程存在,则按优先权原则选择进程执行,否则 CPU 处于等待状态。

进程调度算法实现

执行进程优先级的选取
// 在优先级位图中,选择优先级最高的子就绪队列
int current_priority = -1;
for (int j = ready_queue.highest_priority; j > 0; --j)
    if (ready_queue.priority_bit_map[j] == 1) {
        current_priority = j;
        break;
}
if (currrent_prioriy == -1) 
processor_wait();

优先级位图 priority_bit_map 存储了该下标优先级是否有进程,如有则为 1,反之为 0 值。虽然系统也可以通过子队列数组 sub_queue_array 逐个查询链表头为空,但效率较低,且必须顺序查询,不可随机进行。在下述“执行进程的选取与运行”中,若子就绪队列为空,则置对应位为 0;在有新进程进入就绪队列时,应维护对应优先数位图值相匹配。

执行进程的选取与运行

我们来看一下,执行进程的选取与运行流程图,本部分流程图是上文全局流程图中“处理进程”的子流程。
执行进程的选取与运行流程图
具体代码实现思路如下:

crt_static_prio = pcbs[current_task_pid].static_priority;
crt_dynamic_prio = pcbs[current_task_pid].dynamic_priority;
// 判断当前进程优先级和当前系统优先级是否相等
if (crt_static_prio + crt_dynamic_prio == current_priority) {
    // 如程优先级和系统优先级相等,则继续运行当前进程,减少调度开销
run(current_task_pid);
} else {
// 获取子就绪队列中,排在第一位的进程ID
new_task_pid = ready_queue.sub_queue_array[current_priority].next.pid;
// 在就绪队列中删除该进程
ready_queue.sub_queue_array[current_priority].delete_first_node();
// 更新优先级位图信息
if (ready_queue.sub_queue_array[current_priority].next == null) {
    // 如该优先级子就绪队列无进程,则置相应位为0
    priority_bit_map[current_priority] = 0;
}
//交由处理机处理该进程
run(new_task_pid);
}

在这里需要明确的是,run 函数在结束前,需要完成对该任务状态标识符 state 的修改。例如任务执行结束,则置为 PROCESS_STATE_COMPLETE 状态。

按规定原则修改 PCB 内容

我们来看一下,修改 PCB 内容流程图,本部分流程图是上文全局流程图中“修改进程状态”的子流程。

修改进程状态流程图

// 若该进程动态优先级 <= 3 则直接置为0
if (pcbs[current_task_pid].dynamic_priority <= 3)
    pcbs[current_task_pid].dynamic_priority = 0;
else
pcbs[current_task_pid].dynamic_priority -= 3;    
// 修改该进程 PCB 的进程状态标识符
pcbs[current_task_pid].state = PROCESS_STATE_RUN;
按规定原则修改就绪队列

在该进程用完时间片后,有多种情况:

  1. 进程执行完毕,删除pcbs中的进程控制块,处理机继续执行下一进程;
  2. 进程变为阻塞状态,进入阻塞队列,处理机继续执行下一进程;
  3. 进程还需下一时间片,则优先级按规则调整,处理机继续执行下一进程;
  4. 处理机继续执行下一进程,但就绪队列为空,处理机空闲等待。
// 进程执行完毕
if (pcbs[current_task_pid].state == PROCESS_STATE_COMPLETE) {
    delete_pcb(current_task_pid);
}
// 进程为阻塞状态
if (pcbs[current_task_pid].state == PROCESS_STATE_WAIT) {
    insert_into_wait_queue(current_task_pid);
}
// 进程为就绪状态,还需下一时间片
if (pcbs[current_task_pid].state == PROCESS_STATE_READY) {
    // 获取当前任务进程修改后的优先数
    new_priority = get_priority(current_task_pid);
    // 插入到该优先级子就绪队列的末尾
    ready_queue.sub_queue_array[new_priority].add_to_last();
}
进程调度模拟结果

实例的就绪进程集参数如下表所示,其中 id 为进程标识号,static_priority 为进程静态优先级,dynamic_priority 为进程动态优先级,state 为进程状态,time_slice 为该进程需要的时间片数。

| process_name | id | static_prioriy | dynamic_priority | state | time_slice |
| :----: | :----:| :----:| :----:| :----:| :----:|
| proc_a | 1 | 10 | 0 | ready | 5 |
| proc_b | 2 | 10 | 5 | ready | 5 |
| proc_c | 3 | 30 | 5 | ready | 5 |
| proc_d | 4 | 10 | 20 | ready | 5 |

下面,我们来模拟该实例的运行过程。

实例进程集的运行过程

从表中我们可以看到:

  1. 在时间片 3 时,进程 C 的动态优先级为 2,小于等于 3,因此其动态优先级直接置为 0,静态优先级不改变,则后续该进程优先级保持为 30,这样的机制可以保证系统进程或用户重要进程不被过多降低优先级,保证其优先、及时地被执行;
  2. 在时间片 9 时,进程 D 的优先级和进程 B 相等,在这种情况下,系统优先选择不改变当前进程的方式继续执行,这样的机制可以降低进程调度的开销;
  3. 在时间片 11 时,进程 B 的情况与第 1 条一致;
  4. 在时间片 13 时,由于进程 A 的初始动态优先级即为 0,因此在后续执行过程中该进程的优先级一直保持为静态优先级。

结束语

本文使用 C 语言对动态优先权的进程调度算法进行了设计和模拟实现。程序可实现动态的进行各个进程相关信息的录入,如 STATIC_PRIORITY、DYNAMIC_PRIORITY、STATE 等信息。并充分考虑了进程在执行过程中可能发生的多种情况,更好的体现了进程的就绪态、执行态、阻塞态三者之间的关系以及相互的转换。程序的运行过程清晰的体现了动态优先权的调度算法的执行过程,有利于加深对算法的理解和掌握。由于抢占式调度算法与硬件密切相关,由软件实现较困难,所以本方案实现的是非抢占式的动态优先权进程调度算法,抢占式的动态优先权进程调度算法的模拟实现有待于进一步研究。

版权信息

原创文章


KoreyLee
27 声望6 粉丝

Those who were seen dancing were thought to be insane by those who could not hear the music.