【C/C++ 小项目练习 模拟系统调度器】深入理解操作系统调度算法:使用C++构建模拟器

目录标题


1. 简介

1.1 什么是操作系统调度(Operating System Scheduling)

操作系统调度是操作系统内核的核心部分,它决定了哪个进程(Process)或线程(Thread)应该获得CPU时间。这是一种资源分配策略,旨在最大化利用CPU,同时满足各种性能指标,如响应时间(Response Time)、吞吐量(Throughput)和CPU利用率(CPU Utilization)。

“Premature optimization is the root of all evil.” 这句话出自Donald Knuth的名著《计算机程序设计艺术》(The Art of Computer Programming)。在这里,我们不是优化一个特定的应用程序,而是通过模拟不同的调度算法来找到最优的资源分配策略。

1.2 为什么我们需要模拟器(Why We Need a Simulator)

实现和测试操作系统级别的调度算法通常需要对底层系统有深入的理解,并且可能需要修改操作系统内核。这不仅复杂,而且风险较高。模拟器提供了一个安全的环境,可以在不影响实际系统的情况下测试和验证各种调度算法。

回想一下,当你第一次学习编程时,"Hello, World!"程序给你带来的满足感。为什么?因为它立即给了你反馈。同样,在学习和实验复杂的调度算法时,模拟器可以立即给你可见的、可量化的结果。

1.2.1 用于教育和研究

模拟器通常用于教育和研究目的。对于初学者来说,它是一个很好的工具,可以帮助他们理解调度算法的基础概念和性能影响。对于研究人员来说,模拟器提供了一个平台,可以用于开发和测试新的或改进的调度算法。

用途 优点 缺点
教育 容易上手,快速理解算法 可能不会涉及所有的细节和边界情况
研究 可以深入探讨特定算法或场景 需要更多的前置知识和编程经验

1.2.2 用于业界

尽管模拟器主要用于教育和研究,但它们也在业界有一定的应用。例如,在云计算和大数据环境中,资源调度是一个关键问题。通过模拟不同的调度策略,企业可以预先了解哪种策略最适合他们的需求,从而节省时间和资源。

2. 环境准备

2.1 选择编程语言和工具

在开始探究操作系统调度算法之前,首先我们需要准备一个合适的开发环境。虽然很多编程语言都可以用于这个任务,但C++因其高效性和灵活性,尤其适用于模拟低级操作。

2.1.1 C++标准库

C++有一个非常丰富的标准库,其中包括用于数据结构(如 std::vectorstd::queue)、算法(如 std::sort)和多线程(如 <thread> 库)等的类和函数。例如,在Bjarne Stroustrup的《The C++ Programming Language》一书中,有大量的章节专门讲解了这些库如何提高编程效率。

2.1.2 开发工具

对于C++开发,推荐使用IDE(Integrated Development Environment,集成开发环境)如Visual Studio或者CLion,或者文本编辑器如Visual Studio Code,再搭配CMake或其他构建工具。

2.2 前置知识

编程不仅是一门技术,更是一门艺术。当你面对一个复杂问题时,先别急于动手编码。停下来思考一下,拿出纸和笔,先规划一下你的代码结构。

2.2.1 算法基础

你需要了解基础的数据结构和算法,包括但不限于队列(Queue)、优先队列(Priority Queue)、排序算法(Sorting Algorithms)等。这些是构建任何有效模拟器的基石。

2.2.2 操作系统基础

理解进程(Process)、线程(Thread)和调度(Scheduling)等基础概念是非常必要的。这样,你才能更准确地模拟现实世界中的操作系统行为。

2.2.3 C++语法和特性

你需要熟悉基础的C++语法和一些高级特性,如类(Class)、模板(Templates)和STL(Standard Template Library)。这些知识点在Scott Meyers的《Effective C++》和《More Effective C++》中有很好的讲解。

2.3 设置你的编程环境

有了适当的前置知识和工具后,接下来就是设置你的编程环境。

2.3.1 安装编译器和IDE

选择一个你喜欢的IDE并安装相应的C++编译器。对于Windows用户,Visual Studio是一个很好的选择;对于Linux和Mac用户,GCC或Clang是更常见的选择。

2.3.2 配置项目

创建一个新的C++项目,并确保你能成功地编译和运行一个“Hello, World!”程序。这是确认你的开发环境已经准备就绪的好方法。

2.4 代码风格和规范

编程就像写作,每个人都有自己的风格。但与写作不同,代码是团队合作的结果。因此,遵循一致的代码风格和命名规范是非常重要的。

2.4.1 变量命名

选择有意义的变量名,避免使用简单的单字母名称(除非它们在数学公式或短循环中有明确的含义)。

2.4.2 注释和文档

“代码即文档” 是一个理想,但现实通常并非如此。因此,适当的注释和文档是非常必要的。

3. 进程控制块(PCB)的设计

在操作系统的世界里,一切都开始于进程。进程是系统资源分配和调度的基本单位。如果进程是舞台上的演员,那么进程控制块(Process Control Block, PCB)就是他们的剧本和身份证。

3.1 数据结构的选择

在C++中,类(Class)是组织数据和函数的一种极好的方式。它不仅提供了封装(Encapsulation)— 即将数据和操作数据的方法捆绑在一起 — 还允许数据隐藏和继承。这是非常符合我们人性化的思维方式的,我们总是喜欢将相关的事物放在一起,并设置某种访问级别。就像你不会将你的社交安全号码贴在微博上一样,某些数据应该是私有的。

3.1.1 选择 struct 还是 class?

在C++中,你可以选择 structclass 来定义PCB。这两者在C++中几乎是等价的,但有一个关键的区别:struct 的成员默认是 public,而 class 的成员默认是 private。通常,如果你要实现更复杂的逻辑和封装,class 是更好的选择。

struct class
默认访问修饰符 public private
适用场景 简单的数据结构,无需方法 需要封装和方法的复杂数据结构

3.2 PCB 成员变量的解释

定义了数据结构之后,我们需要深入了解每个成员变量的角色。

3.2.1 id(唯一标识符)

每个进程都需要一个唯一标识符(Unique Identifier)来区分。这是每个人都有名字的原因:没有名字,生活会一团糟。同样,操作系统需要一种方式来迅速识别和管理每个进程。

osp2023::id_type id;

3.2.2 total_time(总运行时间)和 time_used(已用时间)

这两个变量是进程的生命线。total_time 定义了进程需要多长时间来完成其任务,而 time_used 则跟踪到目前为止进程使用了多少时间。这两者的关系就像是人们设定的目标和实际完成的工作。你可能计划一周内完成一个项目(total_time),但实际上可能需要更长时间,或者可能提前完成(time_used)。

osp2023::time_type total_time;
osp2023::time_type time_used;

为了更好地管理时间和任务,优秀的人总是知道他们的目标(total_time)和他们已经投入了多少努力(time_used)。这样,他们可以更有效地调整策略。

3.2.3 total_wait_time(总等待时间)

这个变量是非常人性化的。等待是生活中不可避免的一部分,无论是等待咖啡,还是等待一个重要的电子邮件。在操作系统中,进程也需要等待 —— 通常是等待CPU时间。

osp2023::time_type total_wait_time;

total_wait_time 量化了这一等待,让我们可以衡量系统的效率和公平性。没有人喜欢等待,但如果必须等待,那么最好是等得值得。这正是为什么我们需要调度算法来最小化这一指标的原因。

3.3 C++代码示例

在这一节中,我们将看到一个

简单的PCB类的C++实现。

// PCB.h
#pragma once

namespace osp2023 {
    
    
    using id_type = long;
    using time_type = long;
}

class pcb {
    
    
public:
    osp2023::id_type id;
    osp2023::time_type total_time;
    osp2023::time_type time_used;
    osp2023::time_type total_wait_time;

    pcb(osp2023::id_type id, osp2023::time_type total_time) : 
        id(id), total_time(total_time), time_used(0), total_wait_time(0) {
    
    }
};

在这个例子中,我们定义了一个简单的 pcb 类,其中包括了我们上面讨论的所有成员变量。我们还添加了一个构造函数,以便在创建一个新的 pcb 对象时初始化这些变量。

请注意,虽然这个例子很简单,但它具有极大的扩展性。你可以添加更多的成员函数来实现更复杂的逻辑和计算。

4. 模拟器架构

在涉足操作系统的模拟世界之前,我们需要有一张地图,也就是一个稳固的架构。正如 Bjarne Stroustrup 在《The C++ Programming Language》中所强调的,良好的架构是解决复杂问题的关键。

4.1 类和对象设计

在C++中,封装(Encapsulation,封装)是一种将数据和操作数据的函数捆绑在一起的机制。这不仅仅是代码组织的问题,更是一种思维方式。当你有一个合理的类和对象结构,问题解决就像是拼图游戏,每个类都是拼图的一个片段。

4.1.1 Process Control Block (PCB, 进程控制块)

首先,我们需要一个表示进程的 pcb 类。这个类包含几个关键字段:

  • id(唯一标识符)
  • total_time(总执行时间)
  • time_used(已用执行时间)
  • total_wait_time(总等待时间)

这些字段有助于模拟进程在操作系统中的行为。

4.1.2 Simulator (模拟器)

Simulator 类是这个拼图的核心片段。它管理着一个就绪队列(Ready Queue),可能还有一个等待队列(Waiting Queue)和一个完成队列(Completed Queue)。

方法 功能 适用场景
addProcess() 将进程添加到就绪队列 初始化和进程从等待状态变为就绪状态时
schedule() 根据所选调度算法进行进程调度 每次CPU空闲时
execute() 执行选定的进程 选定进程后
terminate() 移动进程到完成队列 进程完成其 total_time

4.2 如何使用队列管理进程

队列(Queue)是一种先进先出(FIFO, First-In-First-Out)的数据结构,非常适合用于管理等待执行的进程。在C++中,你可以使用 <queue> 头文件提供的 std::queue 或者使用 <vector><algorithm> 手动管理一个队列。

4.2.1 使用 std::queue

使用 std::queue 的好处是它提供了一个清晰、简单的API,让你可以不必关心底层的实现细节。

#include <queue>

std::queue<pcb> readyQueue;

4.2.2 使用 std::vector 和手动管理

使用 std::vector<algorithm> 的好处是更大的灵活性。例如,如果你需要实现一个基于优先级的队列,std::vector 就是一个更好的选择。

#include <vector>
#include <algorithm>

std::vector<pcb> readyQueue;

在这种情况下,你可能需要手动排序或使用 std::priority_queue

4.3 代码示例

让我们来看一个简单的代码示例,该示例展示了如何使用 std::queue 实现一个基于时间片轮转(Round-Robin)的调度算法。

#include <queue>

std::queue<pcb> readyQueue;

void Simulator::schedule() {
    
    
    if (!readyQueue.empty()) {
    
    
        pcb& currentProcess = readyQueue.front();
        execute(currentProcess);
        readyQueue.pop();
        if (currentProcess.time_used < currentProcess.total_time) {
    
    
            readyQueue.push(currentProcess);
        } else {
    
    
            terminate(currentProcess);
        }
    }
}

void Simulator::execute(pcb& process) {
    
    
    process.time_used += TIME_SLICE;
    // ...
}

void Simulator::terminate(pcb& process) {
    
    
    completedQueue.push(process);
}

以上就是模拟器架构的关键组成部分和实现细节。理解这些不仅能让你构建出一个强大的调度算法模拟器,还能加深你对操作系统和C++编程的理解。如同 Robert C. Martin 在《Clean Code》一书中所说:“代码应该像散文一样容易阅读。”通过良好的架构和设计,我们更接近于实现这一目标。

5. 调度算法简介

5.1 FIFO(先进先出, First-In-First-Out)

5.1.1 工作原理

FIFO 是操作系统中最简单、最直接的一种调度算法。其核心思想是“先来先服务”。进程按照到达就绪队列的顺序进行调度。一旦一个进程开始执行,它会一直占用 CPU,直到其完成。

在实际操作系统中,FIFO 被视为一种非抢占式(Non-Preemptive)调度算法,这意味着一旦 CPU 被一个进程占用,其他进程必须等待,直到当前进程释放 CPU。

5.1.2 优缺点

优点 缺点
简单、易于实现 响应时间可能很长
公平 容易导致“饥饿”现象

当你面临一个问题并且你感觉很愚蠢的时候,记住:你不是第一个也不是最后一个面临这个问题的人。这种“先来先服务”的观念在生活中也很常见,但在技术实现中,它有时会产生不可预测的结果。举个例子,如果一个很“贪心”的进程(需要大量的 CPU 时间)恰好排在队列的前面,其他“谦让”的进程(需要较少的 CPU 时间)就会不得不等待很长时间,这种现象通常被称为“饥饿”。

// C++ FIFO 调度算法简单示例
#include <queue>
#include "pcb.h"

std::queue<pcb> readyQueue;

void schedule_FIFO() {
    
    
    while (!readyQueue.empty()) {
    
    
        pcb& current_process = readyQueue.front();
        readyQueue.pop();
        
        // 模拟进程执行
        executeProcess(current_process);
    }
}

5.2 SJF(最短作业优先, Shortest Job First)

5.2.1 工作原理

SJF 是基于进程执行时间(total_time)来进行调度的。具体来说,就是总是先执行预计运行时间最短的进程。在 Scott Meyers 的经典著作《Effective C++》中,他强调了“让接口易于使用,不易于误用”的重要性。SJF 正是这样一个算法:它很容易理解,也很直观。

5.2.2 优缺点

优点 缺点
平均等待时间较短 可能导致饥饿
理论上更“公平” 需要预知执行时间

你可能觉得选择最短的作业执行是一种“贪心”的行为,但实际上,这种策略通常会最小化总体的等待时间,从而提供更好的系统响应。这和你在超市排队选择看似最短的队伍有异曲同工之妙。

// C++ SJF 调度算法简单示例
#include <queue>
#include "pcb.h"

std::priority_queue<pcb> readyQueue;

void schedule_SJF() {
    
    
    while (!readyQueue.empty()) {
    
    
        pcb& current_process = readyQueue.top();
        readyQueue.pop();
        
        // 模拟进程执行
        executeProcess(current_process);
    }
}

这里只是介绍了调度算法的概念和基础实现,具体的应用场景和优化措施还需要根据实际需求来进行。同时,虽然代码示例是用 C++ 写的,但这些原理在其他编程语言和环境中也是通用的。希望这能给你带来一些启示,帮助你更好地理解和应用这些调度算法。

6. FIFO调度算法的实现

在操作系统中,进程调度是一个复杂但令人兴奋的主题。它就像一个精心策划的舞会,每个进程(或者说“舞者”)都等着轮到他们展示自己。而 FIFO(First-In-First-Out,先进先出)就是这场舞会中最简单、最直接的舞步。

6.1 工作原理

FIFO 是一种非抢占式(Non-Preemptive)调度算法。这意味着一旦一个进程开始执行,它会一直持有 CPU 直到任务完成。想象一下,你在一家餐厅排队等候,而服务员会一次服务一个客人,直到该客人的所有需求都得到满足。Sounds fair, right?

6.1.1 非抢占式的优点与缺点

使用非抢占式调度算法的一个明显优点是它的简单性。由于进程一旦开始就会运行到完成,因此我们不需要担心如何保存和恢复进程的状态,这大大简化了调度逻辑。

然而,这种简单性有时也会成为缺点。考虑一个需要大量计算时间的进程排在一个只需要很短时间的进程前面的情况。后一个进程会不必要地等待很长时间——这就是所谓的“饥饿”现象。

优点 缺点
简单易实现 可能导致饥饿
公平 响应时间可能较长

6.2 C++代码示例

让我们通过代码更深入地了解这一切。下面是一个使用 C++ 实现的 FIFO 调度算法的简单例子。

#include <iostream>
#include <queue>

struct pcb {
    
    
    int id;
    int total_time;
    // 构造函数和其他成员...
};

int main() {
    
    
    std::queue<pcb> readyQueue;  // 就绪队列

    // 假设我们已经有了一些进程在就绪队列中
    readyQueue.push(pcb{
    
    1, 10});
    readyQueue.push(pcb{
    
    2, 20});
    readyQueue.push(pcb{
    
    3, 5});

    while (!readyQueue.empty()) {
    
    
        pcb current_process = readyQueue.front();
        readyQueue.pop();

        // 模拟进程执行
        std::cout << "Executing process " << current_process.id << " for " << current_process.total_time << " ms" << std::endl;
        
        // 进程执行完成,不再放回就绪队列
    }

    return 0;
}

在这个例子中,我们使用了 C++ 的 std::queue 标准库来实现就绪队列。这个库为队列提供了一组简单而有效的操作,如 push(添加元素到队尾)、pop(移除队首元素)和 front(访问队首元素)。

6.3 优缺点

FIFO 的主要优点是其简单性和公平性。它很容易实现,而且在某种程度上是“公平”的,因为它按照进程到达的顺序来执行它们。

然而,这种“公平性”有时也是有代价的。如果一个非常耗时的进程恰好排在队列的前面,那么后面的进程就必须等待很长时间。这种情况在现实生活中也常见,比如你可能听说过“Amdahl’s Law”(阿姆达尔定律),它告诉我们在一个系统中,一个慢的部分可以严重影响整体性能。

总的来说,FIFO 是一个相当直接和简单的调度算法,但它并不总是最有效或最公平的选择,特别是在需要快速响应时间或高吞吐量的系统中。


希望这个章节能帮助你理解 FIFO 调度算法的内部工作原理,以及如何使用 C++ 来实现它。当你理解了这些基础知识后,更复杂的调度算法就会变得更容易掌握。毕竟,正如 Knuth(计算机科学之父之一)所说:“程序优化的第一条规则是不要优化;第二条规则(仅限专家)是还不要优化”。这也是为什么理解基础如此重要的原因。

7. SJF调度算法的实现

在本章中,我们将深入探讨最短作业优先(Shortest Job First, SJF)调度算法。我们将从它的工作原理开始,然后讨论如何在C++中有效地实现它。我会展示一些代码示例,并通过对比其他方法来突出其优缺点。

7.1 工作原理

最短作业优先(SJF)是一种非抢占式调度算法。这意味着一旦一个进程开始执行,它将持续运行,直到其完成(或用尽其 total_time)。

假设你是一个图书管理员,你的任务是将一堆不同厚度的书放回书架上。如果你选择从最薄的书开始,这样你可以更快地完成任务,因为你不需要花费大量时间来搬运厚重的书。同理,SJF调度算法也是通过优先调度总体运行时间最短的进程来尽量减少等待时间。

这种算法在计算平均等待时间时非常有效,但也有一个主要的缺点:它可能导致长作业被“饿死”(Starvation)。长作业可能会被不断地推迟,因为总是有更短的作业排在它前面。

7.2 如何有效地从数据结构中选择最短作业

7.2.1 使用优先队列

优先队列(Priority Queue)是C++ STL库中的一个非常强大的数据结构。它允许你在 (O(\log n)) 时间复杂度内插入和删除元素,这比线性搜索要快得多。

#include <queue>

std::priority_queue<pcb, std::vector<pcb>, [](const pcb& a, const pcb& b) {
    
    
    return a.total_time > b.total_time;
}> readyQueue;

使用优先队列,你可以确保队列的顶部始终是 total_time 最小的进程,这样你就可以立即访问并执行它。

7.2.2 使用线性搜索和向量

虽然这种方法的时间复杂度是 (O(n)),但如果你的就绪队列非常小,这可能是一个可行的选择。

int minIndex = 0;
for (int i = 1; i < readyQueue.size(); ++i) {
    
    
    if (readyQueue[i].total_time < readyQueue[minIndex].total_time) {
    
    
        minIndex = i;
    }
}

这里,我们通过简单地遍历就绪队列来找到具有最短 total_time 的进程。当找到这样的进程后,我们可以执行它,并从队列中移除。

方法 时间复杂度 适用场景
优先队列 (O(\log n)) 需要频繁插入和删除
线性搜索 (O(n)) 小规模队列,不需要频繁操作

7.3 C++代码示例

下面是一个简单的C++代码示例,演示了如何使用优先队列实现SJF算法。

#include <queue>

// 定义优先队列
std::priority_queue<pcb, std::vector<pcb>, [](const pcb& a, const pcb& b) {
    
    
    return a.total_time > b.total_time;
}> readyQueue;

while (!readyQueue.empty()) {
    
    
    pcb& shortestJob = readyQueue.top();  // 获取具有最短 total_time 的进程
    readyQueue.pop();  // 从队列中移除

    // 模拟进程执行
    shortestJob.time_used = shortestJob.total_time;
}

这样,我们就能确保总是执行具有最短 total_time 的进程,从而最小化平均等待时间。

这种方法在许多现实世界的场景中都非常有用。例如,当你在咖啡店排队时,如果每个人都只买一

种咖啡,而制作某一种咖啡所需的时间是已知的,那么用这种方法来确定顾客的服务顺序就可以最大限度地减少等待时间。

7.4 优缺点

7.4.1 优点

  • 高效率:SJF算法通常能提供较低的平均等待时间。

7.4.2 缺点

  • 可能导致饥饿:这一点尤其重要,因为在一个系统中,长作业和短作业往往需要平衡地得到处理。

在这里,我想引用Donald Knuth在《计算机程序设计艺术》(The Art of Computer Programming)中的一句话:“早期优化是万恶之源”。尽管SJF算法在降低平均等待时间方面表现出色,但过度优化可能会导致系统的不稳定,因为长作业可能永远得不到执行。

这就是SJF调度算法的全貌。希望这个深入的探讨能帮助你更好地理解这个广泛应用的调度算法。在下一章中,我们将探讨另一种非常流行的调度算法——时间片轮转(Round-Robin)。

8. 时间片轮转(Round-Robin)的实现

8.1 工作原理

时间片轮转(Round-Robin, RR)是一种非常受欢迎的进程调度算法,特别是在需要公平性和时间共享的环境中。这种方法的核心思想是每个进程都有平等的机会使用CPU,但每次只能使用一个很小的时间片(Time Slice)。

基础流程:

  1. CPU从就绪队列(Ready Queue)中选择一个进程并开始执行。
  2. 设置一个硬件定时器以中断CPU执行,持续时间即为预定义的时间片。
  3. 当时间片用尽或进程完成时,CPU停止执行该进程。
  4. 如果进程还未完成,它将被放回就绪队列末尾。
  5. CPU选择下一个进程,并重复步骤1-4。

这种调度方式的美妙之处在于它既简单又有效。你可以把它看作是一个排队买咖啡的场景。每个人都有限制的时间来下单,以确保每个人都能得到服务,而不会因某个人的大量订单而被拖延。

8.2 如何模拟时间片

在现实的操作系统中,时间片是通过硬件定时器来实现的。但在模拟环境中,我们可以用逻辑时间来代替。设想一个变量 current_time,每当一个进程执行一个时间片,这个变量就增加相应的数值。

8.2.1 逻辑时间 vs 真实时间

逻辑时间是一种模拟时间流逝的方式,与真实时间无关。它可以让你更灵活地控制模拟速度。这与看一部悬疑电影的情感经历类似:尽管电影可能只有两小时,但紧张的剧情可能让你感觉时间过得更长或更短。

8.3 C++代码示例

#include <queue>
#include <iostream>

struct pcb {
    
    
    int id;
    int total_time;
    int time_used = 0;
};

int main() {
    
    
    std::queue<pcb> readyQueue;
    int time_slice = 5; // 时间片长度
    int current_time = 0; // 当前逻辑时间

    // 假设我们有三个进程
    readyQueue.push({
    
    0, 15, 0});
    readyQueue.push({
    
    1, 10, 0});
    readyQueue.push({
    
    2, 20, 0});

    while (!readyQueue.empty()) {
    
    
        pcb current_process = readyQueue.front();
        readyQueue.pop();

        // 执行时间片
        current_process.time_used += time_slice;
        current_time += time_slice;

        // 检查进程是否完成
        if (current_process.time_used >= current_process.total_time) {
    
    
            std::cout << "Process " << current_process.id << " completed at time " << current_time << "\n";
        } else {
    
    
            readyQueue.push(current_process);
        }
    }
}

这个简单的C++程序展示了如何使用标准库中的 std::queue 来模拟时间片轮转调度。注意,这里我们使用了结构体 pcb 来模拟进程控制块。

8.3.1 代码解析

  • 我们定义了一个 pcb 结构体,其中包括 id(进程ID)、total_time(总执行时间)和 time_used(已用时间)。
  • 主函数中有一个就绪队列 readyQueue,用于存放等待执行的进程。
  • time_slice 是预定义的时间片长度,这里设为5。
  • current_time 是用于模拟逻辑时间的变量。

在这个模拟中,每个进程在每个时间片结束时都会检查是否已经完成。如果完成了,它就会从就绪队列中移除;否则,它会被放回队列的末尾以等待下一个时间片。

8.4 优缺点

时间片轮转算法有其优缺点,这也是为什么在某些场景下它非常有效,而在其他情况下可能不是最佳选择。

优点 缺点
公平 响应时间可能变长
简单 时间片长度选择困难
适应性强 可能引发上下文切换开销

这些优缺点让时间片轮转成为一个在许多场景下都相当可行的解决方案,但它也不是万能的。选择合适的时间片长度是一门艺术,需要综合考虑系统负载、进程特性和其他多种因素。

9. 性能指标

9.1 平均等待时间(Average Waiting Time, AWT)

一种衡量调度算法效能的关键指标是平均等待时间(AWT)。这是一个衡量进程在就绪队列中等待被调度的总时间的平均值。人们通常认为,一个好的调度算法应该最小化平均等待时间。

9.1.1 计算方法

平均等待时间是所有进程等待时间的平均值。在C++中,这可以通过以下代码来计算:

long long totalWaitTime = 0;
int numProcesses = completedQueue.size();

while (!completedQueue.empty()) {
    
    
    pcb& process = completedQueue.front();
    totalWaitTime += process.total_wait_time;
    completedQueue.pop();
}

double averageWaitTime = static_cast<double>(totalWaitTime) / numProcesses;

9.1.2 等待时间与人的耐心

等待时间过长会导致用户体验下降,这一点不仅适用于现实生活(如等待咖啡或电梯),也适用于操作系统和软件。因此,降低平均等待时间通常是提高系统性能和用户体验的有效途径。

9.2 平均周转时间(Average Turnaround Time, ATT)

平均周转时间是从进程进入就绪队列到进程完成执行所需的总时间。这包括了进程的等待时间和实际执行时间。

9.2.1 计算方法

平均周转时间可以通过下面的公式计算:

[
\text{Average Turnaround Time} = \frac{ {\text{Total Turnaround Time}}}{ {\text{Number of Processes}}}
]

在C++中,这可以通过以下代码来计算:

long long totalTurnaroundTime = 0;
int numProcesses = completedQueue.size();

while (!completedQueue.empty()) {
    
    
    pcb& process = completedQueue.front();
    totalTurnaroundTime += (process.total_time + process.total_wait_time);
    completedQueue.pop();
}

double averageTurnaroundTime = static_cast<double>(totalTurnaroundTime) / numProcesses;

9.2.2 周转时间与效率

高的周转时间可能意味着资源没有被有效利用,而低的周转时间则通常表示系统能快速地处理进程。

9.3 性能指标对比

性能指标 优点 缺点 应用场景
平均等待时间 直观,容易理解和计算 不考虑执行时间 实时系统,交互式系统
平均周转时间 考虑了等待和执行时间 较复杂 批处理系统

10. 高级话题:多队列和优先级调度

在我们之前的讨论中,我们主要集中在基础的调度算法和单一队列的实现上。但现实世界的操作系统通常面临更多复杂的场景和需求。这就引出了本章的核心话题:多队列和优先级调度。

10.1 为什么需要多个队列

正如Bjarne Stroustrup在《The C++ Programming Language》中所说,抽象是程序员最有力的工具。单一队列是一种非常简单的抽象,但当我们面对更多变的需求时,它可能不再足够。

10.1.1 处理多种状态的进程

在一个典型的操作系统中,进程可能处于多种状态:新创建(New)、就绪(Ready)、运行(Running)、等待(Waiting)和完成(Terminated)。单一队列很难有效地管理这些不同状态的进程。

10.1.2 优化性能和响应时间

通过使用多个队列,你可以更灵活地调度进程,从而改善系统性能。例如,高优先级的进程可以被快速地调度,而低优先级的进程则可以等待。

10.2 如何实现多队列和优先级调度

实现多队列并不是一件特别困难的事情,但它确实需要一些精细的设计。

10.2.1 数据结构选择

在C++中,我们有多种数据结构可以用于实现队列,包括std::queuestd::dequestd::priority_queue

数据结构 插入复杂度 删除复杂度 查找复杂度 适用场景
std::queue O(1) O(1) O(n) 简单的FIFO队列
std::deque O(1) O(1) O(n) 双端队列,可以从两端插入和删除
std::priority_queue O(log n) O(log n) O(1) 优先队列,可以根据优先级快速找到最小(或最大)元素

选择哪种数据结构取决于你的具体需求。如果你需要快速地按优先级调度进程,std::priority_queue可能是一个不错的选择。

10.2.2 优先级调度的实现

优先级调度通常使用一个或多个优先级队列来实现。每个队列对应一个不同的优先级。当CPU变为空闲状态时,操作系统会从具有最高优先级的队列中选择一个进程来执行。

std::priority_queue<pcb> high_priority_queue;
std::priority_queue<pcb> medium_priority_queue;
std::priority_queue<pcb> low_priority_queue;

在这种设置中,操作系统首先会检查high_priority_queue,如果它不为空,则从中选择一个进程进行调度。如果high_priority_queue为空,操作系统会继续检查medium_priority_queue,以此类推。

通过使用多个队列和优先级调度,你可以更灵活地管理不同类型和优先级的进程,从而更有效地利用系统资源。这也符合人类对于紧急任务应该优先处理的直观认识,使得整个系统更加“自然”和高效。

11. 总结和未来展望

在我们深入探索了操作系统调度算法的世界之后,你可能会觉得自己就像一位指挥家,掌握了如何优雅地调度各种资源和进程。这种掌控感非常迷人,但也带来了责任——优化算法意味着影响到每一个使用这些系统的人。

11.1 主要收获

11.1.1 对调度算法的理解

我们从三个主要的调度算法开始:FIFO(First-In-First-Out, 先进先出)、SJF(Shortest Job First, 最短作业优先)和RR(Round-Robin, 时间片轮转)。每个算法都有其适用场景,优点和缺点。

调度算法 适用场景 优点 缺点
FIFO 简单任务队列,无优先级 简单,易于实现 可能导致“饥饿”
SJF 需要最小化等待时间的场景 最小化等待时间 需要预知作业长度,可能导致“饥饿”
RR 需要时间共享的多任务环境 公平,响应时间可预测 需要合适的时间片长度,上下文切换开销

如 Donald Knuth 在《计算机程序设计艺术》(The Art of Computer Programming)中所说:“早期优化是一切罪恶的根源。” 在你开始优化之前,先确保你了解了每个算法的基本原理和适用场景。

11.1.2 C++编程技巧

通过本文,你不仅学习了操作系统的基本概念,而且锻炼了你的C++编程技巧。你可能已经熟悉了如何使用 std::vectorstd::queuestd::priority_queue 进行高效的数据操作。

11.2 如何扩展模拟器以支持更多算法或更复杂的场景

11.2.1 添加新算法

当你开始熟悉这些基础算法后,你会发现有无数种方式可以改进和创新。比如,你可以尝试添加一些更复杂的算法,如多级反馈队列(Multilevel Feedback Queue)。

11.2.2 模拟硬件中断和IO操作

目前为止,我们只关注了CPU调度。但在一个真实的系统中,进程经常需要等待I/O操作或响应硬件中断。这些都可以作为模拟器的扩展功能。

11.2.3 分布式和多核调度

如果你觉得单核CPU调度太简单了,那么为什么不尝试一下多核或分布式环境下的调度呢?这将是一个全新的挑战,但也提供了更多的优化机会。

11.3 那些被忽视的细节

编程就像是一场冒险,总有一些隐藏的“宝藏”等待你去发现。这些宝藏可能是一个优雅的算法,一个强大的库,或者仅仅是解决问题的新方法。

当我们沉浸在代码中时,可能会忽视一些人性的方面。就像 Carl Jung 曾经说过:“人们会做他们想做的事,而不是他们应该做的事。” 在编程中也是如此,我们可能会偏爱使用那些看似简单但实际上不够高效的方法。这就是为

什么深入理解各种调度算法的内部工作原理是如此重要的原因。

11.3.1 源码级别的理解

想要真正精通一个领域,你需要深入到源码级别。比如,在理解C++的 std::sort 函数时,你可以查看其内部实现,通常是一种快速排序(Quick Sort)或者归并排序(Merge Sort)的变体。

12. 理解Linux进程调度:实时与非实时

当我们谈论操作系统进程调度时,最常见的例子可能就是Linux了。Linux的调度模型与我们之前讨论的FIFO、SJF和RR等算法有一些不同,但核心目标是相同的:高效、公平地使用系统资源。Linux调度器有两大类:实时(Real-Time)和非实时(Non-Real-Time),它们分别适用于不同类型的应用场景。

12.1 实时调度(Real-Time Scheduling)

12.1.1 SCHED_FIFO和SCHED_RR

Linux提供了两种实时调度策略:SCHED_FIFO(First-In-First-Out)和SCHED_RR(Round-Robin)。这两种策略主要用于需要快速响应和高可靠性的系统,例如航空、医疗或工业自动化系统。

  • SCHED_FIFO: 与我们之前讨论的FIFO算法类似,但具有实时特性。一旦一个实时进程开始执行,它将继续执行,直到完成或被更高优先级的实时进程抢占。

  • SCHED_RR: 这是一种实时版本的时间片轮转算法。与SCHED_FIFO不同,SCHED_RR会在每个时间片后将当前进程移回队列尾部,以便其他同等优先级的进程可以执行。

12.1.2 优先级和抢占

实时调度算法通常涉及优先级(Priority)。在Linux中,实时进程具有从0(最高)到99(最低)的优先级范围。这是因为在实时系统中,某些任务是非常关键的,不能被延迟。

12.2 非实时调度(Non-Real-Time Scheduling)

12.2.1 CFS(Completely Fair Scheduler)

Linux的主要非实时调度算法是CFS。这是一个非常复杂的算法,旨在为所有进程提供公平和均衡的CPU时间分配。

CFS使用红黑树(Red-Black Tree)这一高级数据结构来管理进程,确保在(O(\log n))的时间内完成插入和删除操作。

12.2.2 时间和优先级

与实时调度不同,非实时调度使用的是动态优先级。Linux中的每个进程都有一个“静态优先级”和一个“动态优先级”。动态优先级会根据进程的行为和历史信息进行调整。

12.3 为什么Linux选择这样的模型?

你可能会问,为什么Linux不使用我们之前讨论的SJF或其他算法呢?答案很简单:Linux是为多种用途和多种类型的硬件设计的。它需要能够处理各种各样的工作负载和优先级,这就需要更为复杂和灵活的调度算法。

12.4 从源码角度看Linux调度器

如果你是那种喜欢深入底层的人,Linux内核源码是一个绝佳的学习资源。通过阅读源码,你可以深入了解到CFS是如何实现的,以及实时调度器是如何工作的。

在Linux中,这两种实时调度策略(SCHED_RRSCHED_FIFO)都支持优先级抢占,但它们在时间片方面有所不同。

SCHED_RR(Round-Robin)

  • SCHED_RR 中,每个进程被分配一个固定的时间片。
  • 当一个进程的时间片用完时,它会被放到具有相同优先级的就绪队列的末尾。
  • 在同一优先级的多个进程中,调度器将轮流给它们分配CPU时间。
  • 抢占:一个运行中的 SCHED_RR 进程只会在其时间片用完或者遇到更高优先级的进程时被抢占。

SCHED_FIFO(First-In-First-Out)

  • SCHED_FIFO 中,没有时间片的概念。
  • 一旦 SCHED_FIFO 进程开始执行,它将继续执行直到完成,除非有更高优先级的进程出现。
  • 抢占SCHED_FIFO 的进程可以被更高优先级的进程抢占,但在没有更高优先级进程的情况下,它会一直执行到完成。

所以,你可以这样理解:

  • SCHED_RR 是基于时间片的,并且在时间片到期时可能会转向其他同优先级的任务。
  • SCHED_FIFO 是基于优先级的,只有当有更高优先级的任务出现时才会被抢占。

两者都支持基于优先级的抢占,但它们在时间分配上有明显不同的策略。

结语

在我们的编程学习之旅中,理解是我们迈向更高层次的重要一步。然而,掌握新技能、新理念,始终需要时间和坚持。从心理学的角度看,学习往往伴随着不断的试错和调整,这就像是我们的大脑在逐渐优化其解决问题的“算法”。

这就是为什么当我们遇到错误,我们应该将其视为学习和进步的机会,而不仅仅是困扰。通过理解和解决这些问题,我们不仅可以修复当前的代码,更可以提升我们的编程能力,防止在未来的项目中犯相同的错误。

我鼓励大家积极参与进来,不断提升自己的编程技术。无论你是初学者还是有经验的开发者,我希望我的博客能对你的学习之路有所帮助。如果你觉得这篇文章有用,不妨点击收藏,或者留下你的评论分享你的见解和经验,也欢迎你对我博客的内容提出建议和问题。每一次的点赞、评论、分享和关注都是对我的最大支持,也是对我持续分享和创作的动力。


阅读我的CSDN主页,解锁更多精彩内容:泡沫的CSDN主页
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_21438461/article/details/132690828
今日推荐