tags:
- OS
操作系统是计算机系统不可或缺的一部分,它是连接用户和硬件的桥梁,负责协调资源并优化计算机的使用体验。无论是个人电脑、手机还是现代的车机系统,操作系统都扮演着核心的角色。每当我们使用这些设备时,操作系统正在背后默默地完成资源调度和任务管理工作。
那什么是操作系统?好像一时间很难回答出来,好像它们天然的存在在我们的手机/电脑上。如果你感到困惑,不用担心,因为这个问题就是我们本阶段要回答的问题。因为操作系统是计算机系统的一部分,所以,咱们先来看看什么是计算机系统。
计算机系统是一个宏观的概念,由计算机硬件和计算机软件共同构成。硬件为计算机系统提供了基础设施 (infrastructure) ;软件赋予计算机系统功能,使其能够完成各种任务并满足用户需求。
当你的电脑或手机处于关机状态时,所能看到的就是计算机硬件啦。现代计算机主要遵循 John von Neumann 架构(指令和数据共享同一内存)或其变种 Harvard 架构(指令和数据分开存储)。这些架构定义了计算机由一组特定组件通过特定方式连接而成。这些内容在计算机组成原理中对此有详细介绍,我们这里简单地回顾一下。
在计算机组成原理中,我们学到,von Neumann 计算机由五大核心部件构成。分别是:运算器、控制器、存储器/内存、输入部件和输出部件构成。即要想组成一台计算机,我们就需要有:
如下,我们展示了计算机硬件。我们把没有安装任何软件的计算机称为裸机,想一想,假设这台计算机没有安装任何软件,我们该如何使用裸机完成一些任务?
在使用裸机前,我们先来看看计算机的工作流。von Neumann 架构将计算机分为五大部件,其中,CU 负责协调和控制其他四个部件完成各自的任务。所以计算机的工作流程实际上是由控制器指挥其他部件按顺序执行指令。
由于 von Neumann 架构的计算机是一种基于存储程序 (stored-program) 的设计思想的架构,即指令和数据存储在同一个存储器中。指令的处理流程通常包括以下四个步骤:
CPU 需要处理数据,输入设备就负责将外部信息输入到计算机系统中。常见的输入设备包括键盘、鼠标、扫描仪等。此外,现代计算机还包括触摸屏、语音识别系统和摄像头等高级输入设备,用于更加多样化和直观的用户交互。
当 CPU 要处理某些数据时,它会从 cache 或内存中读取并处理这些数据。如果 cache 和内存没有相关的数据或指令,系统还可能会在硬盘中寻找相关的数据。在并行化计算时代,除了 CPU,系统在一些如图形处理、机器学习等并行计算的应用场景中还会依赖 GPU (Graphics Processing Unit)。
当数据处理完成之后,我们想得到处理后的结果。这时,系统就会让输出设备将计算机处理后的信息输出给用户。常见的输出设备包括显示器、打印机、扬声器等。
我们现在知道了计算机工作流程,我们再来深入一下,看看 CPU 如何执行机器语言指令。如果你要做菜,第一步你需要从冰箱里面先拿原材料,然后处理原材料,烹制菜肴,最后装盘上桌。CPU 执行指令和做菜一样,你要先从内存里面取指令和数据,经过解码,CPU 执行指令,最后写回结果。
这整个过程称为指令执行周期,也简称为指令周期。一般由取指令、指令解码、执行、结果写回四部分组成(有的还包括访存周期)。在指令周期中,你可能经常见到以下几个寄存器:PC 寄存器 (Program Counter, AKA Instruction Pointer)、MAR 寄存器 (Memory Address Register)、MDR 寄存器 (Memory Data Register, AKA Memory Buffer Register)、IR 寄存器 (Instruction Register)。
我们以一个 X+Y 的加法指令做例子,其指令执行周期如下:
CU 负责从内存中获取下一条要执行的指令。它通过 PC 得到当前指令的地址,从内存中读取指令。MDR 的大小就决定了CPU 一次性能够 fetch 多少指令。之后,指令会被加载到 IR 中去处理(解码+执行)。完成这一系列操作后,CU 就会更新当前 PC 中的值(进行 PC+1 的操作),以指向下一条要取 fetch 的指令地址。
取指令阶段顺序:PC (In charge of CU) -> MAR -> (MEMORY) -> MDR -> IR -> (Instruction executes)
在指令被加载到 IR 后,控制单元就会解码指令,根据其操作码来确定它是一条什么类型的指令(本例中说加法指令,即需要两个操作数)。后识别出操作数位于寄存器 A 和寄存器 B。解码阶段确定了需要执行的具体操作及涉及的数据。
指令解码后,根据 CU 的控制信号,ALU 执行实际加法操作。它从寄存器读取值,执行加法操作并将结果暂存。
在操作完成后,CU 一般会指示将 ALU 的计算结果写回寄存器。
至此,我们已经大致了解了一条计算机指令是如何执行的了,尽管这个过程涵盖了取指、解码、执行和写回等多个步骤,但在我们的视角下,这一切几乎透明不可见。所有的逻辑都被封装在一片微小的芯片中了,呈现在我们眼前的,仅仅是排列整齐的针脚们。
我们将这种把复杂机制封装于内部,只暴露简洁接口的设计思想称为——抽象 (abstruction),这是计算机科学中最核心的理念之一。
作为计算机硬件的核心,CPU 通过众多封装的管脚与外界进行交互。假如我们想使用裸机(没有操作系统的计算机)进行一个加法运算,所需要的步骤看似简单:我们只要把被加数放到一个寄存器中,把加数放在另一个寄存器中,然后执行 CPU 的加法指令,等待一瞬间,我们可以在加数寄存器上观察到输出结果了。
我们说过,这一看似轻而易举的过程的背后实际上是万亿计逻辑电路的协作。既然不用关心 CPU 内部到底发生了什么,我们来正式操作一台裸机。
如果你想在裸机环境中编程,你就必须使用机器语言,而且每次都要设置寄存器的值。机器语言是基于硬件的最低级别编程语言,是 CPU 唯一能够直接理解并执行的语言。
作为使用裸机的用户,我们并不关心门电路怎么实现加法指令,我们只需要使用 CPU 提供的加法指令就好了。但我们确实需要关心一件事情:我们希望机器码在厂商后续的升级型号中也能继续使用,不然每次得到新的 CPU 我们都需要编写新的机器代码。为此,芯片厂商就会通过指令集体系结构 (ISA) 对底层晶体管电路进行规约抽象。
例如,8086 及后续的芯片型号使用的架构就是 x86 ,由于 ISA 总是向下兼容的,因而你可以在后续任何版本的 x86 架构芯片上运行相同的机器语言代码。也就是说,ISA 封装了底层的硬件,为裸机程序员提供了一个统一的抽象接口。我们又看到了,封装和抽象的思想。
光说不做假把式,我们来使用一下那个年代的裸机。假想我们有一颗诞生于 1978 年的 8086 微处理器芯片,得益于芯片内部对底层逻辑电路的封装,要完成一个加法运算(1+2),我们只需要想办法让芯片的 IR 寄存器先后呈现这样的电平状态:
10111000 00000001 00000000
10111011 00000010 00000000
00000001 11011000
这些指令代表的含义如下:
我们前面提到过,因为 von Neumann 架构的计算机都是存储程序方式工作的计算机。所以我们需要将这三条指令连续的放到内存中,然后想办法一步步地加载这三条指令。因为 PC 寄存器会自动地进行指令 + 1,所以我们只需要想办法让 CPU 取到第一条指令。虽然 CPU 对逻辑的封装固然方便,但是这样子还是好麻烦。
为了让机器自动地帮我们做事,软件应运而生。计算机软件是运行在计算机硬件上的一系列程序和数据的集合。软件告诉计算机硬件需要先这样,在那样......,从而实现自动化操作。
我们前面说,硬件提供了计算机系统的基础设施,但硬件裸机的使用体验很差,没有人会死板到直接与硬件打交道。通过编写软件,让 CPU 自动地执行软件中的一条条指令,并在适当的时刻与外部设备交互,如接收用户输入、控制输出设备等。有了这层抽象,计算机好像没那么难用了。根据其作用,计算机软件又可分为系统软件和应用软件。
操作系统就是一层软件,帮助我们管理抽象计算机的硬件资源并提供友好可靠的 API 和交互界面。作为用户,我们只要使用系统为我们提供的接口就好了。所以,我们可以说操作系统是封装硬件的软件,作用是为应用软件提供服务。操作系统提供了对硬件资源的抽象和管理,使用户不再需要直接与计算机硬件对接,替代了人工与裸机硬件的交互。
由于屏蔽了硬件资源和硬件的复杂性,操作系统必须为应用程序提供特殊的 API 接口,以申请和管理硬件资源。我们把这些 API 叫做系统调用,使应用程序可以通过操作系统提供的标准化接口访问硬件资源。简化了应用程序开发,还通过权限管理和硬件抽象层提高了系统的安全性和稳定性。
除了操作系统,应用程序也是计算机软件一大重要的组成部分。即使经过了系统的封装,计算机依然不算好用,但人们对于软件开发的热忱一点不减。各式各样的计算机软件层出不穷,我们的生活中充斥着让人感到新奇和让人兴奋的应用软件。
我们利用软件来请求计算机为我们解决一些问题或获得一些需求,不同的软件定义着不同的解决用户问题或获取需求的方式。根据软件功能,我们还能够进一步地划分(办公、娱乐......)。
在上一节中,我们从一个整体视角简单地描述了计算机系统的构成与操作系统的地位。那么,操作系统究竟是什么?它具体扮演着怎样的角色?为了更清晰地回答这些问题,我们将从用户视角和系统视角两个角度进行探讨。
前面,我们尝试模拟使用二进制机器语言直接和计算机硬件进行交互,即便只是完成加法运算,也可见这种方式的复杂性和低效性。用户希望计算机的使用变得更加简单、直观,而有了操作系统对硬件功能的封装,机器的易用性和使用体验得以大幅提升。
从计算机的发展史可以看出,人机交互(用户和操作系统的接口)的方式随着科技的进步而不断演化。从最早的打孔纸带,到 CLI 命令行交互界面,再到 GUI 图形化的交互界面,直至如今的触屏交互方式,人机交互变得越来越便捷。未来,语音交互甚至脑机接口会成为新兴的交互方式么?
在个人电脑 PC 的时代,一个人独享一台电脑。而在 1970s,计算机仍是稀缺货,同时被多个用户使用。所以计算机还需要为每个用户提供一种自己独占整个计算机的错觉。
除了易用性,计算机的性能也是使用体验中很重要的一个部分。而裸机的 CPU 和硬件资源又改变不了,所以,操作系统还应通过优化资源调度策略来最大化 CPU 和硬件资源的利用率。为了提升用户的使用体验,现代操作系统中的调度算法一般会优先保障用户交互相关的任务,如图形界面的平滑操作,而将其他后台任务的优先级适当降低。
需要注意的是,并非所有计算机系统都以用户体验为核心。比如说嵌入式计算机系统(如车机系统)可能更注重实时性需求,甚至会以牺牲一定的用户体验为代价,确保关键任务的按时完成。
从系统的角度看,操作系统就是一组专门设计用于与硬件交互的软件,负责管理所有硬件资源。从而,任何的应用程序都需要通过操作系统提供的系统调用 API 来请求并使用硬件资源。通过这种抽象,操作系统屏蔽了硬件的复杂性,简化了应用程序的开发,同时提高系统的稳定性和安全性。
从这种角度看,操作系统实际上就是计算机系统的资源管理器 (resource allocator),协调系统内的各种资源,例如占用的CPU时间、内存空间、存储空间和I/O设备的分配等。操作系统需要最大化硬件资源的利用率,还要通过权限管理与访问控制,确保资源的安全共享并防止资源冲突。
这种资源调度能力使操作系统成为计算机系统运行的核心,不仅仅需要协调控制 I/O 设备,还需要对用户程序的执行进行控制和管理,以防止用户程序出现错误或者对计算机资源的越界访问(防止应用程序直接操作硬件导致系统崩溃或数据损坏)。当应用程序真正想要访问系统资源时,应用程序就需要通过系统调用 API 想操作系统提出相关的请求,让操作系统代为完成。
所以我们如何去定义什么是操作系统?如何准确地定义它的角色和功能?上面我们已经看到,在不同的视角中,操作系统的定义也是不同的。在用户眼里,操作系统就是让机器易于使用的软件(通过 CLI 和 GUI);而从系统的层面来看,操作系统就是封装硬件,通过系统调用为上层应用提供服务的抽象层。
在许多教科书中,你会看到这样的定义:“操作系统是管理软硬件资源,为应用提供服务的系统软件”。这实际上也是从系统的层面上来定义的。而操作系统的核心目标是构建一个“用户友好”的计算机系统,使计算机能够高效地解决用户的问题和需求。
虽然纯计算机硬件也能直接执行计算任务,但我们也看到硬件远远无法满足用户对易用性和便捷性的要求。因此,我们想要通过软件自动地来帮助实现这些目标。而你可以发现许多软件程序都有一些共性的操作,例如对内存的操作、I/O 的操作等。为了统一管理和实现这些功能,我们将它们集成到一个软件中,即内核 (Kernel) 。内核的出现把计算机软件划分成了系统软件和应用软件。
有了操作系统提供的对下层硬件的抽象,不仅能够满足用户对易用性和便捷性的需求,而且通过在计算机系统中加入一层 indirection ,应用程序想要使用硬件资源必须经过过操作系统内核的管理。提高了整个系统的安全性。
通过上面的学习,我们应该能够明白操作系统的职责之所在。OS最基本的职责就是为我们提供一些抽象,让系统方便使用(user perspective)。除此之外,操作系统还应该管理资源,让系统的性能得到最大的利用(system perspective)。
当我们想要创建自己的软件时,我们就需要将想法告诉计算机。而计算机无法理解人类的语言,计算机能够理解的只有特定架构下的二进制 01 指令。所以我们就需要通过一些方式将我们的想法变成计算机能够理解的机器语言。这节课,我们来学习这一切如何实现。
在之前的学习中,我们了解到,与硬件交互需要使用机器语言。机器语言以二进制(01)表示,是计算机中直接与高低电平对应的语言。虽然它可以直接控制 CPU 和硬件资源,但有些太过于反人类,每次使用都要查阅相关的手册,而且还十分容易出错。
尽管能用二进制代码直接操纵包含数以亿计的晶体管已经可以称之为奇迹了(虽然 8086 只有大约 29000 个晶体管),但我们可能见到这种毫无章法的 01 二进制就烦。为了解决这一问题,人们发明了更易读、容易记忆的汇编语言。汇编使用助记符来表示不同的操作、寄存器等。比如说,你可以用 ADD
来指代加法操作。
汇编如何和机器语言对应上的呢?咱们回到之前的加法运算程序中:
10111000 00000001 00000000
10111011 00000010 00000000
00000001 11011000
机器语言的指令是具有一定的结构的,一般由操作码 (Opcode) 和操作数 (Operands) 构成。指示 CPU 应该执行的操作类型,例如“数据移动”或是“加法”等。操作数用于指定指令操作需要的数据或存储位置,如寄存器或内存地址。
第一条指令是将一个立即数”移动到“特定的寄存器中,我们把称为 AX
,这是一个通用寄存器。其中,前五位表示移动指令,后三位用于指定移动到哪个寄存器。之后的 00000001 00000000
是用小端方式表示立即数 1
。也就是说,这一条指令的作用是把立即数 1
移动到 AX
寄存器中。
操作码 | 十六进制 | 目标寄存器 |
---|---|---|
10111 000 |
B8 |
AX |
10111 001 |
B9 |
CX |
10111 010 |
BA |
DX |
10111 011 |
BB |
BX |
10111 100 |
BC |
SP |
10111 101 |
BD |
BP |
10111 110 |
BE |
SI |
10111 111 |
BF |
DI |
第二条指令和第一条指令差不多。
第三条指令是一个加法指令,其中操作码是 00000001
,表示一个加法操作。后面的一个字节表示将两个寄存器中的数进行相加。即 11 011 000
。11
表示的就是操作数是两个寄存器。
我们将上面的机器语言符号化,用 MOV AX
表示原来的 10111 000
,用 ADD
来指代之前的加法指令操作码 00000001
。不同的寄存器(000
- 111
)也用字母,如 AX
BX
等来表示。这样,一一对应的,我们就得到了下面的汇编语言指令:
MOV AX, 1 ; AX = 1
MOV BX, 2 ; BX = 2
ADD AX, BX ; AX = AX + BX → AX = 3
这样,是不是容易理解多了?
从机器语言到汇编的转变是计算机语言发展中的重大节点。通过符号化的助记符,程序员可以更好更直观地编写代码了。但仍然,汇编还是不够好。因为计算机的架构不同,使用的机器语言和汇编语言仍然是不同的(汇编一一对应机器码)。这是由 ISA 所决定的(如 x86, ARM, RISC-V, MIPS等)。
高级语言是汇编的封装和抽象。高级语言增加了代码在不同架构平台上的可移植性,屏蔽了底层细节,使得同一段代码可以在不同架构的机器下运行。这是通过高级语言编译器实现的,编译器会将高级语言程序编译成特定平台的汇编语言,再由汇编器将汇编代码转换成机器码供计算机读取。
最初的 Unix 系统就是用汇编语言编写的,然而汇编语言是 machine-specific 的,不支持不同平台的移植。虽然第一个高级语言 FORTRAN 的出现代表着编程语言有了更高层次的抽象,但最开始仍未解决可移植性问题。即你可能需要在不同的平台上写不同的程序。
C 语言的出现改变了这一局面。C 语言设计的初衷之一就是为了实现代码的可移植性。它通过提供一个接近底层硬件的抽象层,使得程序员可以编写在不同硬件平台上运行的代码,而不需要对每个平台进行大量的修改。
在前两节课中,我们初步介绍了抽象的概念——抽象是计算机科学发展的基石。CPU 通过暴露管脚,将晶体管的硬件封装为一个抽象的计算单元;操作系统则进一步对这些封装的硬件(如 CPU、内存、I/O 等)进行抽象,为应用程序提供接口。这种设计让开发者能够忽略硬件的复杂性,而将精力放在实现业务逻辑和用户交互上。
随后,我们学习了汇编语言和高级语言。汇编语言对机器语言进行抽象,为开发者提供了更友好的编程接口。而高级语言则进一步抽象底层细节,大幅提升了代码在不同平台上的可移植性,让开发者能够更轻松地与硬件交互。
通过抽象,复杂的底层细节得以简化,使得计算机系统能够更高效地工作,同时也让开发者能够专注于解决实际问题。在理解了抽象的概念后,本课将探讨操作系统需要管理的资源及其管理方式。简单来说,操作系统必须管理以下几大核心资源:
如果一段程序不能被 CPU 所执行,那即使这段程序功能在强大,它依旧没有任何意义。在操作系统中,进程指的是正在执行的程序。比如,你现在正在浏览的网页、运行的微信应用,以及正在播放的 QQ 音乐,都属于独立的进程。它们的共同点是:这些程序实例都在一台计算机上运行(即由 CPU 执行)。
我们把可以在机器上运行的程序称为可执行程序,这些程序通常经过操作系统的封装(Windows 下的 .exe
格式,Linux 下的 .elf
格式)。在磁盘上存储的可执行程序需要被载入内存后,才能转化为进程并开始执行。
简单来说,进程可以被理解为程序在 CPU 上运行的一个实例。静态存储在磁盘上的程序是“被动的静态实体”,而进程则是“主动的动态实体”,因为它代表了程序正在运行的状态。所以同样的程序载入两次内存,创建的是两个不同的进程。
为了完成任务,运行中的程序——即进程需要操作系统分配必要的资源,例如:
当我们启动一个程序时,操作系统会将程序载入内存并为其分配所需的资源。在程序的运行过程中,进程也可能会动态向操作系统申请额外资源,确保任务能够持续完成。而当进程终止时,操作系统会需要回收该进程占用的所有资源,并重新分配给其他任务。
在操作系统中,为了管理和调度进程的运行,操作系统需要支持以下功能:
这些功能我们将会在 PROCESS MANAGEMENT 的部分进行介绍。
CPU 只能从内存中加载指令执行,所以任何需要运行的程序就需要首先加载到内存中。在大多数情况下,计算机都会从一块可读可写的内存中读取并运行程序,我们称之为主存,也叫随机存储器 (Random Access Memory, RAM)。主存一般采用动态随机存储器 (DRAM) 的半导体技术实现。
RAM 是一种易失性 (volatile) 存储器,即一旦断电,RAM 中存储的数据就会消失。所以除了主存,计算机系统中还需要其他类型的存储器来保存关键数据,即使在断电时也不丢失。比如,计算机加电后运行的第一个程序,我们称之为启动程序 (bootstrap program),一般存储在一块非易失性存储器中,如闪存 (Flash) 和电可擦写可编程只读存储器 (EEPROM) 。
内存由一系列的字节构成,每个字节都拥有其唯一的地址。不同指令集架构下的 CPU 可能通过不同的汇编指令和内存进行交互。比如在 x86 汇编中,常常通过 MOV
指令来实现加载和存储操作。如:
MOV AX, 10
MOV BX, [0x1000]
ADD AX, BX
MOV [0x1000], AX
这段代码将一个立即数 10
赋予 AX
,然后将内存地址 0x1000
的数据加载到寄存器 BX
中。相加两个寄存器并将结果存储在 AX
中,最后将结果写回 0x1000
中。
而在 ARM 架构下的汇编中,CPU 通过 LDR
和 STR
指令和内存进行交互。LDR
就是将主存中的数据加载到 CPU 中的某个寄存器里面,STR
就是将寄存器中的内存存储到主存中。我们在上面看到,x86 是通过 MOV
指令来实现这两种操作的。ARM 汇编中,我们可以用下面的代码实现同样的功能:
LDR R1, [0x1000]
ADD R1, R1, #10
STR R1, [0x1000]
其效果和 x86 汇编是一样的。
之前,我们接触到了 CPU 指令周期,在 von Neumann 架构的机器下,CPU执行的每条指令都会经历这样的指令周期,包括:取指令周期,解码周期,执行周期和写回周期(可能没有)。在指令的取指令周期中,CPU就会从内存中加载指令。
从上面的描述中,我们窥得在主存计算机系统中的核心地位,但是 DRAM 半导体的性质赋予了主存一些难言之隐。在大多数情况下,我们都希望自己的劳动成果能够永久地保存下来。但主存是易失性的存储器,只要断电,内容就会永久丢失掉。而且主存一般情况下都很小,不能够存储我们想要的所有程序和数据。
为了解决主存的难言之隐,大部分的计算机系统都会提供一个多级的存储器结构来优化存储性能并扩展主存功能。比如,我们现在有二级存储 (secondary storage) 来作为主存的扩展。我们可以将需要永久保存的程序和数据放到二级存储器中进行永久保存。
二级存储器通常由 hard-disk drives (HDDs) 或 solid-state drives (SSDs) 这类非易失性的 (nonvolatile) 存储介质组成。大多数程序都会存储在二级存储器中等待加载进内存中执行。虽然二级存储器容量很大,而且具有非易失性,但二级存储器通常都很慢。
非易失性的存储器除了二级存储器之外还有三级存储器。三级存储器结构通常由光盘和磁带构成,你可以想象它们有多慢,所以三级存储器一般只用作程序或数据的备份。
无论是计算机系统还是存储层次结构中,主存始终处于核心位置。主存就相当于一个共享的仓库,为 CPU 和 I/O 设备提供快速的访问。在取指令周期中,CPU 从主存中读取指令。我们先前提到程序和数据都是存储在二级存储器中的,但主存是 CPU 唯一能够直接访问的大容量存储器,如果 CPU 要执行某个程序,就必须首先被加载到主存中。
在早期的计算机中,主存容量有限,这意味着内存可能只能确保一个程序的执行。为了让程序能够运行,操作系统会采用将程序加载到固定物理地址(绝对地址)。程序直接使用物理地址来访问内存。CPU 也通过物理地址取指令和数据。在程序执行结束后,操作系统会释放其占用的内存空间,供其他程序使用。
之后,随着内存的扩大和 CPU 性能的提升,为了充分利用资源并支持多程序系统,降低计算机对用户的响应,使得计算机需要将好多个进程加载进内存中。也就诞生了最早的对内存管理的需求。为实现有效的内存管理,诞生了许多不同的内存管理模式,并且大多都并依赖特定硬件的支持。
为了支持多程序并发,后续系统引入了静态重定位 (static relocation) ,通过依赖一些特殊的寄存器(如基址寄存器和界限寄存器)来实现地址的转换。这个时期的系统开始使用逻辑地址(Logical Address) 的概念。这时的内存管理仍然非常简单,操作系统只需要:
为了进一步优化内存的使用效率,同时保障系统的稳定性和安全性。现代操作系统引入了更复杂的内存管理机制,例如,现代计算机系统都引入了内存管理单元 (MMU) 来实现如虚拟内存 (Virtual Memory) 和内存保护 (Memory Protection) 等更复杂的内存管理机制。此处略过。
我们后续将在 Memory Management 部分详细介绍操作系统是如何进行内存管理的。
除了非易失性,主存还有一个问题,即相比较于 CPU 而言的速度太慢了。在 70-80 年代的早期计算机中,主存的速度和 CPU 较为匹配。但随着半导体技术的突破,主存和 CPU 性能开始扩大,逐渐落后于 CPU 的吞吐要求。
(比如 1980 年 8086 CPU 主频 5MHz(200ns),DRAM 访问延迟约 200ns,基本匹配;而 2020 年的 i9-10900K 主频 5.3GHz(0.19ns),DDR4 内存延迟约 50ns,差距达 263 倍。)
这种速度差异就意味着 CPU 在等待主存响应时会浪费大量资源。为了弥补这一性能差距,我们需要一种速度能匹配 CPU 的存储介质。要这么多层的存储介质的好处就是让每次 CPU 在取指令/数据时,都优先从更快的存储介质中寻找,这就是缓存 (Caching) 所做的事情。
之前我们了解到了二级存储器的概念,在一定程度上解决了内存易失性的问题。我们会将程序和数据存放在二级存储器上,如果 CPU 在内存中找不到相关的信息,系统就会从二级存储器中寻找并将需要的信息全部或部分的加载到内存中。所以,你可以将内存理解为二级存储器的“缓存”。
为了匹配 CPU 的速度,我们需要引入一种更快的存储介质,这样在 CPU 执行指令时,率先从这种更快的介质中寻找需要的数据或指令。如果未找到,则从内存中加载数据,并将数据存入这种告诉存储介质中方便后续使用。我们把这种介质称为缓存 (Cache) 。
缓存是用一种叫 SRAM (Static Random Access Memory) 的技术实现的,它有以下特点:
现代计算机系统中,缓存通常集成在 CPU 芯片上,并且采用分层设计,以进一步优化性能。
缓存有 CPU 和内置的 MMU 单元进行管理,虽然这一层次对于操作系统而言不可见,但是系统程序员必须了解这一存储层次。比如,操作系统的调度策略就需要考虑到缓存这一层次的存储结构。在 CPU SCHEDULING 阶段和 MEMORY MANAGEMENT 阶段,我们还会接触到这一存储层次。
在操作系统中,I/O 管理是核心模块。在某种意义上,I/O 系统的运行效率和稳定性直接地决定了整个系统的可用性,因为 I/O 的管理涉及到于用户交互设备的工作(如键盘、鼠标、显示器等)。为了确保系统的可靠性和性能,操作系统需要对 IO 进行管理。
此外,操作系统的一大职责就是对底层硬件的封装。这意味着操作系统需要封装并隐藏 I/O 设备的细节,为上层应用提供统一的接口,从而简化开发与使用。这些 I/O 设备的复杂细节由操作系统的 I/O 子系统负责管理。I/O 子系统主要包含以下部分:
中断我们会在 INTERRUPTS AND SYSTEM CALLS 进行介绍,操作系统 I/O 子系统会和大存储管理、文件系统管理在 I/O SUBSYSTEM & MASS STORAGE & FILE-SYSTEMS 这三个阶段进行详细地介绍。
在学习内存管理的时候,我们穿插了一些层次化存储结构的内容。我们看到,在现代计算机中,计算机系统必须提供二级存储层次作为主存的后备。二级存储器是一类特殊的 I/O 设备,通常采用 HDDs 和 SSDs 这样的存储介质。它们的速度较主存慢得多,且数据的交互方式类似于 I/O 操作。
为了高效管理二级存储,操作系统需要提供如下的管理功能:
除了二级存储器,计算机系统中可能还存在三级存储结构(如磁带驱动器、光盘驱动器等),这一层次对于性能而言并不关键。但如果系统还包含这一层次的存储,操作系统也需要提供类似的管理功能,如:
文件系统操作系统在大存储系统管理的基础上实现的核心模块。借助二级/三级存储器(也叫块设备)非易失性的特性,文件系统实现了对程序和数据的持久化保存 (Persistence)。文件系统一般会封装对二级存储器的大存储管理,为用户提供了更高级的接口,方便操作和管理。
操作系统会将物理存储设备上存放的数据抽象为逻辑上的存储单元,也就是我们常见的文件 (File),文件是一个包含相联信息的集合。通常而言,文件用于表示程序或数据(可能是数字、字母或二进制类型)。文件有很多类型,一般根据后缀名来区分不同类型格式的文件。
为了更好地管理二级存储器上的文件,文件系统通常会用目录 (Directories) 来管理文件。此外,文件系统还会将逻辑上的文件与存储文件的底层的物理存储介质(如块设备)进行映射,管理其逻辑与物理结构。
为了高效管理二级存储器上的文件,文件系统提供了以下核心功能:
之前的学习中,我们明确指出了操作系统的核心作用,即封装底层硬件、为上层应用软件提供服务的系统程序。通过引入间接层,操作系统使得用户能够更方便地使用计算机,同时不需要关心复杂的硬件细节。此外,系统的安全性也得到了保障,因为每个程序必须通过系统调用来申请资源。
在本节课中,我们将学习操作系统如何通过抽象与虚拟化为应用程序提供服务,并优化资源使用。同时,我们还会回答一个关键问题:操作系统为应用程序提供的资源在应用的视角下是怎么样的?
抽象是操作系统设计中一个重要且核心的概念。前两课中,我们已经隐约地感受到抽象在不同层次的应用。在第一节课中,我们了解到 CPU 是对晶体管硬件的封装抽象、操作系统是对 CPU 内存及 I/O 等计算机硬件资源进行封装抽象。在第二节,我们又从编程语言的角度理解抽象。
抽象的意义在于:隐藏底层的复杂性,只向上层暴露最关键的信息。抽象可以简化上层的开发过程,并为系统架构的扩展性提供了支持。抽象的结果就是接口,它可以连接不同层次的系统组件,支持更高级的应用。根据功能与连接对象的不同,接口可以划分为以下几类:
我们在这里关注的是应用-操作系统-硬件之间的抽象,所以我们下面先简单介绍一下经过操作系统硬件抽象层抽象后的硬件资源在操作系统眼中是怎么样的。
操作系统硬件抽象层是内核的一部分,主要提供针对底层硬件的标准化接口。操作系统的作用之一就是封装底层硬件,内核的 HAL 就是系统与硬件进行直接交互的部分。使操作系统能够通过硬件驱动程序与具体的 CPU、内存和外设交互,而无需关心硬件实现的差异。
经过 HAL 的封装,操作系统的其他子模块(如调度器、内存管理、IO 子系统)可以使用统一的接口与下层的硬件进行交互,而不用担心平台架构的不同(ARM 还是 x86)。由此,HAL 的存在赋予了操作系统在不同架构平台上的可移植性。在不同的平台下,只需要更换相应的 HAL 模块就好了。
在 HAL 屏蔽硬件的细节的基础上,操作系统的其他资源管理模块就可以无视硬件细节,将 HAL 封装的系统硬件资源进行二次抽象来为应用程序提供更高级的 API 接口。二次抽象的作用往往就是对资源进行统一管理并提升系统的安全性。
以内存管理为例,内存管理模块会通过分页 (Paging) 机制将物理内存虚拟化成虚拟内存,为应用程序提供连续的虚拟地址空间。从而应用开发者就不需要关心实际物理内存的布局了。
由于虚拟内存需要通过操作系统内存管理模块将虚拟内存转化成物理内存,之后由 HAL 进行实际的分配和管理。所以间接的确保了内存资源的安全性。此外,资源管理模块通常还会实现更为高级的功能,如写时复制、内存隔离等高级功能。
在进程的视角下,操作系统通过虚拟化技术将物理资源转换为一致的虚拟资源供进程使用。尽管进程在运行时可以获得 CPU 资源、内存资源、I/O 资源,但经过资源管理模块的二次抽象,这些资源都将是虚拟的,什么意思呢?
上面我们提到了虚拟内存,简单来说,进程的视角下,它自认为得到的是一个连续、完整的内存空间。而在实际的物理内存中,这些部分可能是分散存储的,甚至与其他进程共享。
从进程的视角来看,每个进程也都认为自己独占 CPU。然而,这实际上是通过操作系统调度器实现的虚拟化。操作系统通过轮转调度 (Round-Robin) 等调度算法,将物理 CPU 的使用权在多个进程之间快速切换。
这种调度策略称为分时策略 (time-sharing),操作系统将 CPU 的运行时间划分为多个时间片,每个进程在一个时间片内运行,时间片耗尽后切换到下一个进程。通过这种方式,操作系统为每个进程提供了虚拟 CPU 的假象,使其感觉自己持续运行。
此外,操作系统还会通过设备驱动程序和 HAL,将复杂的设备交互封装为标准化的系统调用接口。在进程的视角下,I/O 操作简单而直接(read()
, write
系统调用),根本不需要了解 I/O 是怎么运作的。
经过上面的学习,我们了解到应用程序使用的资源只是操作系统通过虚拟化为应用程序提供的一种“幻象”。这层“幻象”是操作系统将底层物理资源(如 CPU、内存和 I/O 设备)通过抽象与虚拟化技术封装后呈现的。
作为使用计算机的用户,我们对机器的接触也停留在这一层“幻象”上。由于操作系统的封装和抽象,我们是看不到硬件的细节和物理资源是如何分配的。操作系统会帮我们将这一层“幻象”转换成机器上的物理资源。所以,我们可以说操作系统为应用程序和用户提供了一台“虚拟的机器”。
在上小节,我们讨论了资源虚拟化的概念,通过操作系统各个资源管理模块对物理硬件进行抽象和封装,使得应用程序能够简化对底层硬件的使用。而除了资源虚拟化,操作系统一般还提供一种更高级的虚拟化技术 (Virtualization),即提供在一个物理主机上运行多个操作系统的能力。
这种虚拟化一般通过一层虚拟化软件来实现,一般称为虚拟机管理器 (Hypervisor)。Hypervisor 会抽象底层的硬件资源,为每个操作系统实例(虚拟机)提供独立的运行环境。这种虚拟化技术和资源虚拟化很类似,我们将在 ADVANCED TOPICS 中进行详细地介绍这种虚拟化技术。
在考研 408 科目的参考书之一——《计算机操作系统》(汤小丹)中,将操作系统的特性归纳为“四大特征”(即并发、共享、虚拟和异步)。我们介绍过了虚拟(资源虚拟化),本节课我们就来介绍其他的三个特征。
在操作系统中,共享是资源管理的核心目标之一。通过共享,我们期望多个应用程序可以同时使用有限的系统资源,以提升资源利用率。共享特性往往基于资源虚拟化技术。操作系统通过将物理硬件资源(如 CPU、内存和 I/O)虚拟化,为每个进程提供一个独占所有资源的“幻象”。然而,实际情况是,这些资源被多个应用程序所共享。
内存是程序运行的基础资源。在操作系统中,主存通过虚拟内存技术被抽象为一个独立、连续的地址空间,供每个进程使用。实际上,进程看到的虚拟内存可能与其他应用共享相同的物理内存页。在实际应用中,共享内存可以用于应用程序之间的通信,还有通过复用相同的代码段来实现内存共享的动态链接库。
I/O 设备也是典型的共享资源。例如,多个任务可能需要使用同一个打印机,或者多个用户同时访问文件系统。操作系统就需要通过合理的 I/O 调度和资源管理,确保 I/O 的有序访问。
通过调度器的对物理 CPU 的虚拟化,每个进程会认为自己得到了独立的 CPU,但实际上得到的是一段 CPU 的时间片。操作系统的任务调度器通过快速切换任务,使得每个任务都能在极短的短时间内获得 CPU 的使用权。从而,在用户看来,多个任务似乎在同时进行。
通过分时,一个物理的 CPU 核心可以被多个进程在逻辑上“同时”使用,实现一种在宏观上同时运行,但在微观上时间片轮流交替的效果,这就是并发 (Concurrency)。
异步操作指在执行某个操作时,不需要等待该操作完成,而是可以继续执行其他任务。操作完成后,会通过某种机制通知执行者。这种机制在操作系统中尤其重要,特别是用于管理慢速 I/O 操作。异步机制使得 CPU 不需要和慢速 I/O 一直打交道,提高了 CPU 的运行效率。
操作系统通过中断和信号机制来实现异步操作。当某个 I/O 操作完成时,硬件会发送一个中断信号给 CPU,CPU 会暂停当前的任务,转而处理这个中断信号。处理完中断信号后,CPU会恢复之前的任务。这样,进程就不需要一直等待 I/O 操作的完成,而是可以在 I/O 操作完成时被通知。
异步操作不仅限于 I/O 操作,还可以应用于其他需要等待的操作,比如网络通信、定时任务等。通过异步操作,操作系统能够更高效地利用资源,提高系统的响应速度和吞吐量。
多个特权级别用于区分程序的运行权限;
通过用户帐户和权限管理隔离不同用户;
使用虚拟地址空间隔离不同的应用程序;
...