“计算机科学中没有什么超出人们生活智慧的知识:CS中的概念和算法,总能找到生活中的例子”
自己讲授操作系统近乎10年了,虽然很早就想将该课程也落实到"设计与实现",可调研之下,想要达成还是太过琐细,所以,主要专注于将操作系统(OS: Operating System)的专题成体系,并尽可能直观地让学生直观了解大致的设计与实现。
NB:
此处专讲 von Neumann 模型上的操作系统的概念,即在 von Neumann Computer model 标识的有限资源(1 CPU, limited Linearly addressed Space [Main Memory, Disk])上,如何支持多道程序的并发运行(Concurrent Execution)。
1. 导引
现代操作系统的目标(Goal) 是在 von Neumann Computer model 标识的有限资源(1 CPU, limited Linearly addressed Space [Main Memory, Disk])上,支持多道程序的并发运行(Concurrent Execution) - Support the concurrent execution of multi-processed programs
之所以能够达成此目标,自然得益于前人的智慧 - 建基于 von Neumann 的内涵和外延:
-
第一个层次 - 内涵,也就是von Neumann 中部件(Components)的功能:
- CPU是执行程序指令的部件(Machine Cycle - Fetch, Decode, Execute)而已。
- 即便是为了提升效率引入了 MMU (Memory Management Unit), L1/L2 Cache, Pipelined Instruction Execution 等,但,了解到前面的简单层次也就够用了
- 磁盘(或其他永久介质,如光盘,固态硬盘)以文件的形式来保存程序;
- 内存,作为CPU可直接访问的空间(Cache暂时不考虑,对概念的理解没有影响) - 也可以有不同的玩法:想要执行程序,程序(指令和数据)的全部或部分必须在内存里。"只要内存里保有程序的部分,程序也就可以执行",这一点为后面的多个复用留了后门
- 内存的使用,分为操作系统部分和用户部分
- 内存、磁盘空间的管理 - 二者实有共同之处(见后面的表述)
- CPU是执行程序指令的部件(Machine Cycle - Fetch, Decode, Execute)而已。
-
第二个层次 - 外延,就是如何复用(Multiplexing)那些资源以支持多道程序的并发执行:
- 内存中可以同时保存多个程序(的部分),它们的执行,其实也就是轮流使用CPU执行相应程序的指令而已 - 任意时刻,只有一个程序在使用CPU
- 也就是后面所谓的复用CPU, 复用内存;甚至复用程序
- 可也因为"多道程序的并发执行"造成诸多的挑战,共享资源的分配和访问要协调好(Synchronization),否则会造成数据不一致和死锁的风险(见后面的表述)
- 内存中可以同时保存多个程序(的部分),它们的执行,其实也就是轮流使用CPU执行相应程序的指令而已 - 任意时刻,只有一个程序在使用CPU
NB:
- 如何使用CPU等细节 - 那些寄存器,特殊指令等自己查查吧
所以,理解操作系统的两个基本问题就是:(1) 程序的保存和并发执行;(2) 分配和访问共享资源的协调
NB:
当然,操作系统发展到现在,还有很多其他的考虑:GUI,Networking, Security, 甚或是Distributed 的支持。我总结为 EMM + GSD
EMM是指 Execution - Mapping 1+2:其中,Mapping 1 是指将File 映射到内存空间;Mapping 2 是指将File 映射到磁盘空间。
GSD:GUI - Security - Distributed
2. 程序的保存和并发执行 (记住,只有1个CPU和有限的线性地址空间 - 内存和磁盘)
为管理在计算机中运行的程序,需要记录支持程序运行的一些信息,如程序在磁盘和内存中的位置,所有权,优先权,是否占用CPU等,这自然需要对应的数据结构,称之为进程 (Process)
有资料对进程的描述是"进程,是指运行中的程序",让人不明觉厉!
线程(Thread)的概念
1 个 CPU, 对于程序的执行,没有什么可说的。
也就是将要执行程序的信息配置给CPU (至少设定IR和PC寄存器),然后,CPU就按照取指令、解释(解码 - Decoding)、执行逐条执行程序而已。
其他的,也就是如何在多程序间进行CPU的分配(即CPU调度,对应CPU复用),有很多的算法 - 至少要知道 FIFO (First In First Out,或FCFS - First Come First Served)吧
2个空间 - 内存和磁盘(其他介质的永久存储是类似的,不赘述) - 对应内存复用:
- 现代计算机对内存和磁盘,在空间管理的概念上是类似的,即将空间分割成等大小的区域(Regions - 内存是Frame, 文件是 Page, 磁盘是Block。一般它们的大小是一样的 - 当然,也可以在格式化磁盘时选择不同的尺寸);
- 为保障安全性,内存空间分为操作系统空间(系统空间)和用户空间
- 虚拟空间的概念,即程序可访问的空间并不受物理内存的限制,而是取决于设定的地址长度,如32位操作系统,可访问的空间就是字节(B - Byte),即4GB
- 磁盘上有一块空间用于支持交换,这样,变相地增大了内存的空间
- 管理这些区域的数据结构也保存在内存和磁盘中:数据结构管理的信息包括已用区域,未用区域;文件的管理(所谓的目录数据结构);以及区域-用户的对应信息(一个文件保存在哪些区域中?那些区域如何连接起来?)等
- Bitmap (Bit Vector), Linked List (FAT, ...), Tree (I-node, ...)
- 事关全局的数据结构(操作系统那些系统调用,计算机资源的管理,进程的管理,内存和磁盘的部分数据结构等),都是要放在所谓的内核空间里;
- 这样说起来,操作系统支持多道用户程序执行的形式,更多地像是多线程的概念:按照"Linux 内存管理","深入浅出Linux内核内存管理基础"的描述,用户进程都可以访问所谓的内核空间。
- 基于这些数据结构,
- 某用户想要创建一个文件,看看是否有可用的磁盘区域(Block),有的话,将文件的信息写入目录结构,然后,将内存中的文件数据写入那些可用的磁盘区域 - Mapping 2
- 是的,如果在创建的过程中才发现磁盘空间不够了,那就会提醒用户做出选择 - 另选一个磁盘,或者放弃
- 某程序要执行,操作系统找到程序对应的磁盘文件的那些区域,然后,按需将部分区域的内容(程序指令+数据)拷贝到内存的可用区域中然后,操作系统调度CPU执行那些指令 - Mapping 1
- 某用户想要创建一个文件,看看是否有可用的磁盘区域(Block),有的话,将文件的信息写入目录结构,然后,将内存中的文件数据写入那些可用的磁盘区域 - Mapping 2
并发执行
并发执行,也就是前面说过的 - "内存中可以同时保存多个程序(的部分),它们的执行,其实也就是轮流使用CPU执行相应程序的指令而已 - 任意时刻,只有一个程序在使用CPU"
但是,要记住,操作系统也是程序的集合,所以,即便是用户程序间没有直接的关联(共享数据或前后连接等),也会因为有资源的竞争(毕竟资源有限)而体现共享资源协调的现象:
- 可用的内存区域:想运行某程序的时候,已经没有可用的 - 共享资源的分配协调
- 打印机的使用 - 共享资源的访问协调
- CPU 的调度 - 共享资源的分配协调
- 用户程序与操作系统间的数据共享 - 现代操作系统为保障安全,用户是不能直接使用系统资源的,而需要调用操作系统的系统调用[所谓的System Call],就需要将用户的信息拷贝至操作系统的System Call 函数 (从用户空间拷贝至操作系统空间;将System Call的结果从操作系统空间拷贝至用户空间) - 共享资源的访问协调
- 等等
协调不好的话,就有数据不一致性,以及死锁的问题:
- 数据不一致性 (Data Inconsistency)
- 死锁 (Deadlock)
3. 协调的挑战 - 共享资源(数据)的分配协调,和访问协调
共享资源(数据)的访问协调 - 数据不一致性
互斥的实现 - 四种方案实现锁机制(Lock Mechanism):软,硬,OS's Semaphore, HPL's Monitor
NB:
要注意,与DBMS中的机制是不同的,后者更为复杂
共享资源(数据)的分配协调 - 死锁(Deadlock)
四种策略:足够的资源(那个公式);鸵鸟(视而不见的策略 - 其实是误会人家鸵鸟了);"防患于未然"的预防(Prevention)策略 - 打破必要条件;"量力而行"的避免(Avoidance)策略 - 银行家算法;"兵来将挡水来土埯"策略 - Detection & Recovery
NB:
死锁有四个必要条件 - 互斥,非抢占,持有等待,等待环
4. 现在可以看看操作系统的历史和未来
CPU的使用没什么改变,所以,操作系统的演变体现为有限线性地址空间的使用,以及操作系统本身结构设计的演变上
内存空间的使用
[图片上传失败...(image-e3a459-1622598816519)].png)
早期的 用户独占用户空间,后来的Fixed- and Variable- partitioning, 再到后来的 Paging
进而有了VM(Virtual Memory)的概念 - On-demand Segmenting 和 On-demand Paging等
操作系统结构的演变
Monolithic, Layered, Microkernel, Hybrid
NB:
个人不认为VM (Virtual Machine)是一个操作系统结构,而是一个(虚拟化)技术而已
操作系统编写:早期是专门的机器码、汇编直接上手,后来大多基于C (C++等变种)编写,也有尝试其他语言的(如JOS - Java 操作系统)。
未来
IoT
从操作系统的发展历史来看,其商业价值总是与相应的硬件相对应的 - 成业硬件消也硬件:Linux 在服务器领域无可替代;Windows 在个人电脑/笔记本广受欢迎;Android 在移动手机难以撼动。华为的鸿蒙操作系统虽说一方面是不得不然,另一方面其选定的切入点也耐人寻味 - 想依赖万物互联而做大做强,自信能做成第三大软件生态。作为国人,拭目以待!
HPC - Distributed OS
虽说在HPC领域,Unix/Linux 系的操作系统的地位是绝对的主流(因为是开源,因为是可以定制),但是,也还是有很多其他的专门软件,如MPI (Message Passing Interface是一个标准,有很多的实现版本)、Globus,并且随着大数据生态软件开源的促进,国内也涌现出了很多的专门软件,如数据存储的Greenplum、GaussDB、Kylin和HAWQ, 消息中间件 RabitMQ,微服务的Dubbo等。
5. 疑问 (Q & A)
5.1 使用磁盘空间的演变?在梳理的过程中,想起来,都是介绍内存空间的使用演变([Fixed- or Variable-]Partitioning, Paging),磁盘空间早期是怎样管理的?
- I-node, FAT等是当下操作系统教材会提到的。它们之前的呢?似乎如下:
- 程序保存在卡片上的阶段,只是专注于驱动卡片的读写
- 在SBS (Simple Batch System)、FMS (Fortran Monitor System)中,文件的保存介质已转向磁带,那存储空间的数据结构也自然是磁带的结构 - 已经有所谓 (Header, Data, Tail) 了
5.2 Knuth 的 Buddy's System 还有操作系统使用吗?
- The buddy memory allocation technique is a memory allocation algorithm that divides memory into partitions to try to satisfy a memory request as suitably as possible. This system makes use of splitting memory into halves to try to give a best fit. According to Donald Knuth, the buddy system was invented in 1963 by Harry Markowitz, and was first described by Kenneth C. Knowlton (published 1965).[1] The Buddy memory allocation is relatively easy to implement. It supports limited but efficient splitting and coalescing of memory blocks. -- From Wikipedia
- "由于伙伴系统的内部碎片通常较为严重(对于任意的内存分配需求,其平均空间浪费率会达到25%),因此该算法基本已经成为历史,在实践中较少使用" - From "垃圾回收算法手册:自动内存管理的艺术 笔记"
5.3 那么,现在的操作系统 (以Linux 为例),操作系统内存是怎样的数据结构?
应该能够意识到,现在内核空间的管理也是On-demand Paging了
为了充分利用和管理系统内存资源,Linux采用虚拟内存管理技术,利用虚拟内存技术让每个进程都有4GB 互不干涉的虚拟地址空间。进程初始化分配和操作的都是基于这个「虚拟地址」,只有当进程需要实际访问内存资源的时候才会建立虚拟地址和物理地址的映射,调入物理内存页。不管是用户空间还是内核空间,使用的地址都是虚拟地址,当需进程要实际访问内存的时候,会由内核的「请求分页机制」产生「缺页异常」调入物理内存页。
也就是说,除了部分固定的内存空间,其他的内存是操作系统和用户程序按照On-demand Paging 的概念来共用的
在 x86 32 位系统里,Linux 内核地址空间是指虚拟地址从 0xC0000000 开始到 0xFFFFFFFF 为止的高端内存地址空间,总计 1G 的容量, 包括了内核镜像、物理页面表、驱动程序等运行在内核空间 。[去看看"Linux 内存管理","深入浅出Linux内核内存管理基础"]
可以看出,每个进程(1,2,3......,n)都有自己的私有用户空间(0-3GB),这个空间对系统中的其他进程是不可见的。最高的1GB内核空间则由则由所有进程以及内核共享。可见,内核最多寻址1G的虚拟地址空间。用户空间对应进程,所以每当进程切换,用户空间就会跟着变化;而内核空间是由内核负责映射,它并不会跟着进程变化,是固定的。内核空间地址有自己对应的页表,用户进程各自有不同的页表。******每个进程的用户空间都是完全独立、互不相干的**
直接映射区(最小896),即操作系统固定使用的部分 - 一般不变,保存 Zone_Normal 和 Zone_DMA等设备相关的信息;而"总计128M的"里面的数据结构,可帮助管理(4GB-896MB)的虚拟内存 - 仍然遵循 On-demand Paging 的思想。