Skip to content

处理机调度

此为校内课程笔记

处理机调度层次

高级调度

高级调度又称为作业调度长程调度。其主要功能是根据某种算法,决定将外存上处于后备队列中的哪些作业调入内存,为它们创建进程、分配必要资源,并放入就绪队列。

简单来说,高级调度用于决定哪些作业可以进入内存运行

高级调度主要用于多道批处理系统,分时系统与实时系统不设置高级调度。

中级调度

中级调度又称内存调度,其主要目的是提高内存利用率和系统吞吐量,改善系统性能。

为实现这一目的,中级调度需要完成两项工作(“对换”功能):

  • 将暂不运行的进程调至外存等待

  • 将存于外存上急需运行的进程调入内存运行

低级调度

低级调度也称为进程调度短程调度。其主要功能是按照某种算法,决定就绪队列中的哪个进程(或内核级线程获得处理机资源

低级调度是操作系统中最为活跃的调度,因此其算法设计的好坏直接影响到系统的性能。

多道批处理系统、分时系统、实时系统均有应用低级调度。

进程调度

任务

进程调度的主要任务有三:

  • 保存CPU现场信息

  • 按某种算法选取进程

  • 把处理器分配给进程

调度机制

实现进程调度,应具有三个基本部分:

  • 排队器: 用于将就绪进程插入相应的就绪队列

  • 分派器: 用于将选定的进程移出就绪队列,并将其分配给CPU执行

  • 上下文切换器: 进行新旧进程之间的上下文切换

进程调度机制
进程调度机制

调度模式

  • 非抢占式调度(Non-preemptive Scheduling)

    一旦进程开始执行,就会一直运行到完成,不会被其他进程抢占。

  • 抢占式调度(Preemptive Scheduling)

    进程在执行过程中有可能基于某种原因被其他进程抢占,从而导致进程的执行被打断。

处理机调度算法

CPU Scheduling in Operating Systems | GeeksforGeeks

调度算法的目标

共同目标

  • 资源利用率: 使系统中的处理机和其他所有资源都尽可能地保持忙碌状态

  • 公平性: 确保每个进程都能获得合理的CPU时间,避免某些进程饥饿

    饥饿

    饥饿(starvation)是指进程因为某种原因长时间无法获得所需资源(特别是CPU时间),导致无法继续执行的现象

  • 平衡性: 根据资源使用情况,可将系统中的进程划分为计算密集型I/O密集型。调度算法应尽可能保持系统资源使用的平衡性,从而满足第一条目标。

    计算密集型与I/O密集型进程

    • 计算密集型进程:需要大量CPU计算资源的进程,如科学计算、图像处理等

    • I/O密集型进程:需要大量I/O操作的进程,如文件读写、网络通信等

  • 策略强制执行: 对于所制定的策略(包括安全策略等),只要需要,必须予以准确地执行,即使会造成某些工作的延迟

部分评价指标

  • CPU利用率

    \[ \text{CPU利用率} = \frac{\text{CPU有效工作时间}}{\text{CPU有效工作时间} + \text{CPU空闲等待时间}} \]
  • 周转时间

    从作业提交到系统开始到作业完成的时间间隔。

    • 平均周转时间: 所有作业的周转时间之和与作业个数之比

      \[ T = \frac{1}{n} \sum_{i=1}^{n} T_i \]
    • 带权周转时间: 权值为作业周转时间 \(T_i\) 与系统为之服务时间 \(T_s\) 之比

      \[ T_{W_i} = \frac{T_i}{T_s} \]
    • 平均带权周转时间: 所有作业的带权周转时间之和与作业个数之比

      \[ T_{W} = \frac{1}{n} \sum_{i=1}^{n} T_{W_i} = \frac{1}{n} \sum_{i=1}^{n} \frac{T_i}{T_s} \]
  • 响应时间: 从作业提交到首次产生响应的时间间隔。

  • 等待时间(进程调度): 进程在就绪队列中等待处理机的时间之和。

批处理系统的目标

  • 平均周转时间短

  • 系统吞吐量高

  • 处理机利用率高

分时系统的目标

  • 响应时间短

  • 均衡性

实时系统的目标

  • 保证满足截止时间的要求

  • 保证可预测性

调度算法

概述

调度算法按照服务对象可分为作业调度算法进程调度算法,分别对应前文提到的高级调度与低级调度。

常见的作业调度算法有:

常见的进程调度算法有:

先来先服务

定义

先来先服务调度算法(First Come First Served, FCFS是最简单的调度算法,既可以用于作业调度,也可以用于进程调度。顾名思义,其基本思想是按照作业到达的先后顺序进行调度。

// 伪代码示例
void FCFS_Scheduler() {
    while (!ready_queue.empty()) {
        Process current = ready_queue.front();  // 取队首进程
        ready_queue.pop();

        execute_process(current);  // 执行到完成
    }
}
特点与优缺点
  1. 非抢占式调度:一旦进程开始执行,就会一直运行到完成

  2. 按到达时间排序:严格按照进程到达就绪队列的时间顺序进行调度

  3. 简单公平:实现简单,对所有进程一视同仁

  4. 优点

    • 实现简单:逻辑清晰,易于理解和实现

    • 公平性:所有进程按到达顺序获得服务

    • 无饥饿现象:每个进程最终都会被执行

  5. 缺点

    • 护航效应:短进程可能被长进程阻塞

      护航效应

      护航效应(convoy effect)是指长进程会阻塞短进程的执行,导致短进程的响应时间变长。

      想象一下,如果有一个执行时间很长的进程排在队列前面,后面所有进程都要等待它完成。就像高速公路上的慢车,会拖慢整条车流的速度。

    • 平均等待时间长:特别是当短进程排在长进程后面时

    • 响应时间差:不适合交互式系统

Example

假设有三个进程:

  • 进程A:到达时间0,执行时间3

  • 进程B:到达时间1,执行时间1

  • 进程C:到达时间2,执行时间2

使用先来先服务调度算法,甘特图(Gantt Chart)如下:

时间轴: 0----1----2----3----4----5----6
进程A:  [==============]
进程B:       [----------====]
进程C:            [----------=========]

性能指标:

  • 进程A:等待时间 \(0\),周转时间 \(3\)

  • 进程B:等待时间 \(2\),周转时间 \(3\)

  • 进程C:等待时间 \(2\),周转时间 \(4\)

平均等待时间: \(\frac{0+2+2}{3} = 1.33\)

平均周转时间: \(\frac{3+3+4}{3} = 3.33\)

短作业优先

定义

顾名思义,短作业优先调度算法(Short Job First, SJF以作业(或进程)执行时间的长短来决定调度的优先级,时间越短,优先级越高。

SJF 既可以用于作业调度,也可以用于进程调度

  • 针对作业调度,SJF 从后备队列中选择若干估计运行时间最短的作业,将它们调入内存运行

  • 针对进程调度,SJF 关联到每个进程下次运行的CPU区间长度,调度最短的进程执行

    • 另外,针对进程调度,SJF 有两种模式:

      • 非抢占式SJF: 一旦进程开始执行,就会一直运行到完成

      • 抢占式SJF(Shortest Remaining Time First, SRTF): 进程执行一段时间后,如果新的进程进入就绪队列,且新进程的执行时间比当前进程的剩余执行时间短,则新进程会抢占当前进程,当前进程会重新进入就绪队列

// 非抢占式SJF调度算法
void nonPreemptiveSJF(vector<Process>& processes) {
    int n = processes.size();
    int currentTime = 0;
    vector<bool> completed(n, false);

    cout << "Non-preemptive SJF scheduling order:\n";

    for (int i = 0; i < n; i++) {
        // 找到当前时间已到达且未完成的最短作业
        int shortestIndex = -1;
        int shortestTime = INT_MAX;

        for (int j = 0; j < n; j++) {
            if (!completed[j] && 
                processes[j].arrivalTime <= currentTime && 
                processes[j].burstTime < shortestTime) {
                shortestTime = processes[j].burstTime;
                shortestIndex = j;
            }
        }

        if (shortestIndex == -1) {
            // 没有进程到达,等待下一个进程到达
            int nextArrival = INT_MAX;
            for (int j = 0; j < n; j++) {
                if (!completed[j]) {
                    nextArrival = min(nextArrival, processes[j].arrivalTime);
                }
            }
            currentTime = nextArrival;
            i--; // 重新尝试
            continue;
        }

        // 执行选中的进程
        Process& p = processes[shortestIndex];
        p.completionTime = currentTime + p.burstTime;
        p.turnaroundTime = p.completionTime - p.arrivalTime;
        p.waitingTime = p.turnaroundTime - p.burstTime;

        cout << "Process P" << p.id << " execution time: " << currentTime 
             << " - " << p.completionTime << endl;

        currentTime = p.completionTime;
        completed[shortestIndex] = true;
    }
}
// 抢占式SJF调度算法
void preemptiveSJF(vector<Process>& processes) {
    int n = processes.size();
    int currentTime = 0;
    int completed = 0;

    // 初始化剩余时间
    for (auto& p : processes) {
        p.remainingTime = p.burstTime;
    }

    cout << "Preemptive SJF scheduling order:\n";

    while (completed < n) {
        // 找到当前时间已到达且剩余时间最短的进程
        int shortestIndex = -1;
        int shortestTime = INT_MAX;

        for (int i = 0; i < n; i++) {
            if (processes[i].remainingTime > 0 && 
                processes[i].arrivalTime <= currentTime && 
                processes[i].remainingTime < shortestTime) {
                shortestTime = processes[i].remainingTime;
                shortestIndex = i;
            }
        }

        if (shortestIndex == -1) {
            // 没有进程可执行,跳到下一个到达时间
            int nextArrival = INT_MAX;
            for (int i = 0; i < n; i++) {
                if (processes[i].remainingTime > 0) {
                    nextArrival = min(nextArrival, processes[i].arrivalTime);
                }
            }
            currentTime = nextArrival;
            continue;
        }

        Process& p = processes[shortestIndex];

        // 检查是否有新进程到达并可能抢占
        bool preempted = false;
        for (int i = 0; i < n; i++) {
            if (processes[i].arrivalTime == currentTime + 1 && 
                processes[i].remainingTime < p.remainingTime) {
                // 新进程到达且更短,当前进程被抢占
                cout << "Process P" << p.id << " preempted, execution time: " 
                     << currentTime << " - " << currentTime + 1 << endl;
                p.remainingTime -= 1;
                currentTime++;
                preempted = true;
                break;
            }
        }

        if (!preempted) {
            // 执行到完成
            cout << "Process P" << p.id << " execution time: " << currentTime 
                 << " - " << currentTime + p.remainingTime << endl;

            currentTime += p.remainingTime;
            p.remainingTime = 0;
            p.completionTime = currentTime;
            p.turnaroundTime = p.completionTime - p.arrivalTime;
            p.waitingTime = p.turnaroundTime - p.burstTime;
            completed++;
        }
    }
}
特点与优缺点

对于平均等待时间而言,SJF是最优的(对一组指定的进程而言),它给出了最短的平均等待时间。

  • 优点

    • 平均等待时间最短:数学上可证明SJF能给出最优解

    • 系统吞吐量高:短作业快速完成,释放资源

    • 简单直观:逻辑清晰,易于理解

  • 缺点

    • 长作业饥饿:长作业可能永远得不到执行

    • 需要预知时间:实际中很难准确预测执行时间

    • 不公平:对长作业用户不友好

Example

假设有四个进程:

进程 到达时间 执行时间
A 0 5
B 1 4
C 2 2
D 3 3
  • 使用非抢占式SJF调度算法,甘特图如下:

    时间轴: 0----1----2----3----4----5----6----7----8----9----10----11----12----13----14
    进程A:  [========================]
    进程B:       [------------------------------====================]
    进程C:            [---------------=========]
    进程D:                 [-----------------------------------------==================]
    

    性能指标:

    • 进程A: 等待时间 \(0\),周转时间 \(7\)

    • 进程B: 等待时间 \(6\),周转时间 \(10\)

    • 进程C: 等待时间 \(3\),周转时间 \(5\)

    • 进程D: 等待时间 \(8\),周转时间 \(11\)

    平均等待时间: \(\frac{0+6+3+8}{4} = 4.25\)

    平均周转时间: \(\frac{7+10+5+11}{4} = 8.25\)

  • 若使用抢占式SJF调度算法,则甘特图如下:

    时间轴: 0----1----2----3----4----5----6----7----8----9----10----11----12----13----14
    进程A:  [=========----------===============]
    进程B:       [--------------------------------------------=========================]
    进程C:            [=========]
    进程D:                 [-------------------===============]
    

    性能指标:

    • 进程A: 等待时间 \(2\),周转时间 \(7\)

    • 进程B: 等待时间 \(9\),周转时间 \(13\)

    • 进程C: 等待时间 \(0\),周转时间 \(2\)

    • 进程D: 等待时间 \(4\),周转时间 \(7\)

    平均等待时间: \(\frac{2+9+0+4}{4} = 3.75\)

    平均周转时间: \(\frac{7+13+2+7}{4} = 7.25\)

优先级调度

定义

优先级调度算法(Priority Scheduling, PS是一种基于进程优先级进行调度的算法,系统为每个进程分配一个优先级值,调度器总是选择优先级最高的进程执行,既可用于作业调度,也可用于进程调度。

PS 是当前主流的OS调度算法,基于作业/进程的紧迫程度,由外部赋予作业相应的优先级,调度算法根据优先级进行调度

struct Process {
    int id;
    int priority;        // 优先级(数值越小优先级越高)
    int arrivalTime;
    int burstTime;
    int remainingTime;
    int completionTime;
    int turnaroundTime;
    int waitingTime;
};

// 优先级调度算法实现
void priorityScheduling(vector<Process>& processes) {
    int n = processes.size();
    int currentTime = 0;
    int completed = 0;

    // 初始化剩余时间
    for (auto& p : processes) {
        p.remainingTime = p.burstTime;
    }

    cout << "Priority scheduling order:\n";

    while (completed < n) {
        // 找到当前时间已到达且优先级最高的进程
        int highestPriorityIndex = -1;
        int highestPriority = INT_MAX;

        for (int i = 0; i < n; i++) {
            if (processes[i].remainingTime > 0 && 
                processes[i].arrivalTime <= currentTime && 
                processes[i].priority < highestPriority) {
                highestPriority = processes[i].priority;
                highestPriorityIndex = i;
            }
        }

        if (highestPriorityIndex == -1) {
            // 没有进程可执行,跳到下一个到达时间
            int nextArrival = INT_MAX;
            for (int i = 0; i < n; i++) {
                if (processes[i].remainingTime > 0) {
                    nextArrival = min(nextArrival, processes[i].arrivalTime);
                }
            }
            currentTime = nextArrival;
            continue;
        }

        // 执行选中的进程
        Process& p = processes[highestPriorityIndex];
        cout << "Process P" << p.id << " (Priority: " << p.priority 
             << ") execution time: " << currentTime 
             << " - " << currentTime + p.remainingTime << endl;

        currentTime += p.remainingTime;
        p.remainingTime = 0;
        p.completionTime = currentTime;
        p.turnaroundTime = p.completionTime - p.arrivalTime;
        p.waitingTime = p.turnaroundTime - p.burstTime;
        completed++;
    }
}
特点与优缺点
  • 优点

    • 灵活性高:可根据业务需求调整优先级

    • 实时性好:高优先级进程能快速响应

    • 资源优化:重要任务优先获得资源

  • 缺点

    • 饥饿问题:低优先级进程可能永远得不到执行

      老化

      为了避免饥饿问题,可以采用老化技术(Aging,即随着进程的等待时间增加,其优先级逐渐提高。

    • 优先级倒置:可能出现高优先级进程等待低优先级进程的情况

    • 复杂度高:需要合理设计优先级分配策略

应用方式
  • 按调度模式可分为抢占式非抢占式

  • 按调度优先级类型可分为静态优先级动态优先级

    • 静态优先级:在进程创建时分配,进程运行期间不变

    • 动态优先级:在进程运行期间根据某种规则进行动态调整

最高响应比优先

定义

最高响应比优先调度算法(Highest Response Ratio Next, HRRN是一种非抢占式调度算法,是优先级调度算法的一个特例,通过计算每个进程的响应比来决定调度顺序,通常用于作业调度

其中,这里的响应比与优先级分别通过如下公式计算:

\[ 响应比R_{p} = \frac{等待时间 + 要求服务时间}{要求服务时间} = \frac{响应时间}{要求服务时间} \]
\[ 优先级P_{p} = \frac{等待时间 + 要求服务时间}{要求服务时间} \]
// HRRN调度算法实现
struct Process {
    int id;
    int arrivalTime;
    int burstTime;
    int waitingTime;
    int completionTime;
    int turnaroundTime;
    double responseRatio;
};

void HRRN_Scheduling(vector<Process>& processes) {
    int n = processes.size();
    int currentTime = 0;
    vector<bool> completed(n, false);

    cout << "HRRN scheduling order:\n";

    for (int i = 0; i < n; i++) {
        // 计算所有已到达且未完成进程的响应比
        int bestIndex = -1;
        double highestRatio = -1;

        for (int j = 0; j < n; j++) {
            if (!completed[j] && processes[j].arrivalTime <= currentTime) {
                // 计算响应比
                int waitingTime = currentTime - processes[j].arrivalTime;
                processes[j].responseRatio = (double)(waitingTime + processes[j].burstTime) / processes[j].burstTime;

                if (processes[j].responseRatio > highestRatio) {
                    highestRatio = processes[j].responseRatio;
                    bestIndex = j;
                }
            }
        }

        if (bestIndex == -1) {
            // 没有进程到达,等待下一个进程到达
            int nextArrival = INT_MAX;
            for (int j = 0; j < n; j++) {
                if (!completed[j]) {
                    nextArrival = min(nextArrival, processes[j].arrivalTime);
                }
            }
            currentTime = nextArrival;
            i--; // 重新尝试
            continue;
        }

        // 执行选中的进程
        Process& p = processes[bestIndex];
        p.waitingTime = currentTime - p.arrivalTime;
        p.completionTime = currentTime + p.burstTime;
        p.turnaroundTime = p.completionTime - p.arrivalTime;

        cout << "Process P" << p.id << " (Response Ratio: " << p.responseRatio 
             << ") execution time: " << currentTime << " - " << p.completionTime << endl;

        currentTime = p.completionTime;
        completed[bestIndex] = true;
    }
}
特点与优缺点

HRRN 即考虑了作业的等待时间,有考虑了作业的运行时间:

  • 如等待时间相同,运行时间越短的响应比越高,类似SJF

  • 如运行时间相同,则取决于等待时间,类似FCFS

  • 长作业可随其等待时间的增加而提高响应比,从而获得调度机会,克服饥饿问题

  • 缺点:每次调度前都需要计算响应比,增加了计算开销

Example

假设有五个作业:

作业 到达时间 执行时间
A 0 5
B 1 4
C 2 2
D 3 3
E 4 1

使用HRRN调度算法:

  • 第一次调度:

    A 作业结束,其他作业的响应比 \(R_p\) 分别为:

    • \(R_{p_{B}} = \frac{5-1 + 4}{4} = 2\)

    • \(R_{p_{C}} = \frac{5-2 + 2}{2} = 2.5\)

    • \(R_{p_{D}} = \frac{5-3 + 3}{3} = 1.67\)

    • \(R_{p_{E}} = \frac{5-4 + 1}{1} = 2\)

    作业C响应比最高,调度

  • 第二次调度:

    C 作业结束,其他作业的响应比 \(R_p\) 分别为:

    • \(R_{p_{B}} = \frac{5+2-1 + 4}{4} = 1.5\)

    • \(R_{p_{D}} = \frac{5+2-3 + 3}{3} = 2.33\)

    • \(R_{p_{E}} = \frac{5+2-4 + 1}{1} = 4\)

    作业D响应比最高,调度

  • 第三次调度:

    D 作业结束,其他作业的响应比 \(R_p\) 分别为:

    • \(R_{p_{B}} = \frac{5+2+3-1 + 4}{4} = 3.25\)

    • \(R_{p_{E}} = \frac{5+2+3-4 + 1}{1} = 7\)

    作业E响应比最高,调度

最终的调度顺序为: A -> C -> D -> E -> B,甘特图如下:

时间轴: 0----1----2----3----4----5----6----7----8----9----10----11----12----13----14----15
作业A:  [========================]
作业B:       [----------------------------------------------------=======================]
作业C:            [---------------=========]
作业D:                 [--------------------===============]
作业E:                      [-------------------------------====]

性能指标:

  • 作业A: 等待时间 \(0\),周转时间 \(5\)

  • 作业B: 等待时间 \(10\),周转时间 \(4\)

  • 作业C: 等待时间 \(3\),周转时间 \(5\)

  • 作业D: 等待时间 \(4\),周转时间 \(7\)

  • 作业E: 等待时间 \(6\),周转时间 \(7\)

平均等待时间: \(\frac{0+10+3+4+6}{5} = 4.6\)

平均周转时间: \(\frac{5+4+5+7+7}{5} = 5.6\)

时间片轮转调度

时间片轮转调度算法(Round Robin, RR中,系统将按FCFS策略组织的就绪队列中的进程以时间片为单位进行轮转,每个进程在时间片内执行,时间片结束后,进程将被抢占并插入到就绪队列的末尾。

  • 时间片: 系统可设置每隔一定时间(通常为10~100毫秒)便产生一次中断,去激活进程调度程序进行调度,把CPU分配给队首进程,并令其执行一个时间片。
#include <vector>
#include <queue>
#include <algorithm>

struct Process {
    int id;
    int arrival_time;
    int burst_time;
    int remaining_time;
    int completion_time;

    Process(int id, int arrival, int burst) 
        : id(id), arrival_time(arrival), burst_time(burst), 
          remaining_time(burst), completion_time(0) {}
};

void round_robin_scheduling(std::vector<Process>& processes, int time_quantum) {
    std::queue<Process*> ready_queue;
    int current_time = 0;

    // 按到达时间排序
    std::sort(processes.begin(), processes.end(), 
              [](const Process& a, const Process& b) {
                  return a.arrival_time < b.arrival_time;
              });

    int process_index = 0;

    while (process_index < processes.size() || !ready_queue.empty()) {
        // 将到达的进程加入就绪队列
        while (process_index < processes.size() && 
               processes[process_index].arrival_time <= current_time) {
            ready_queue.push(&processes[process_index]);
            process_index++;
        }

        if (!ready_queue.empty()) {
            Process* current_process = ready_queue.front();
            ready_queue.pop();

            // 执行时间 = min(剩余时间, 时间片)
            int execution_time = std::min(current_process->remaining_time, time_quantum);

            // 更新进程状态
            current_process->remaining_time -= execution_time;
            current_time += execution_time;

            // 如果进程未完成,重新加入队列
            if (current_process->remaining_time > 0) {
                ready_queue.push(current_process);
            } else {
                // 进程完成,记录完成时间
                current_process->completion_time = current_time;
            }
        }
    }
}
进程切换时机
  • 若时间片尚未结束,但正在运行的进程便已经完成,则立即激活调度程序,从就绪队列删除该进程,并调度就绪队列中的队首进程

  • 若时间片结束时,正在运行的进程还未完成,则剥夺其CPU使用权,并插入就绪队列的末尾,然后调度就绪队列中的队首进程

时间片大小的确定

时间片的大小应选择适中,使得大多数进程能在一个时间片内完成,从而减少进程切换的次数。时间片的大小应根据进程的类型和数量来确定,通常为10~100毫秒

  • 时间片过大: 导致所有进程都到达就绪队列后,长作业会持续占用CPU,导致其他进程无法获得CPU资源

  • 时间片过小: 导致进程切换过于频繁,增加系统开销

  • 一般准则: 时间片/10 > 进程上下文切换时间

优点与缺点
  • 优点

    • 公平性:每个进程都能获得CPU时间

    • 响应性:交互式进程能及时响应

    • 简单性:算法简单,易于实现

    • 无饥饿:不会出现进程永远得不到CPU的情况

  • 缺点

    • 上下文切换开销:频繁切换影响性能

    • 时间片选择困难:需要权衡响应时间和系统开销

    • 不适合I/O密集型进程:可能浪费CPU时间片

多级反馈队列调度

定义

在 RR 中,我们假设所有进程的紧迫性是相同的,但显然事实并非如此。

多级反馈队列调度算法(Multi-Level Feedback Queue, MLFQ便是在 RR 的基础上,根据进程的紧迫性不同,设置多个优先级不同的就绪队列,每个优先级对应一个时间片,同时每个就绪队列都采用FCFS算法,从而实现多级反馈队列调度。

多级反馈队列调度
多级反馈队列调度