引言#

本章导读#

本章主要解释了在已经有一系列优秀的操作系统教材的情况下,为何要写本书。所以本章一开始就是分析学生目前学习操作系统碰到的困难和问题,并介绍如何参考操作系统历史,结合操作系统的完整实验来设计本书的各个章节来编写本书。接下来将从非常高层次的角度和计算机以及操作系统的发展史来进一步描述了什么是操作系统、操作系统的访问接口、操作系统的抽象、操作系统特征,让同学能够对操作系统有一个大致的整体把握。最后介绍了本书关联的操作系统实验环境(包括在线实验和本地实验等)的搭建过程,为后续开展各个操作系统实验打好基础。

为何要写这本操作系统书#

在目前的操作系统教学中,已有一系列优秀的操作系统教材,例如 William Stallings 的《Operating Systems Internals and Design Principles》,Avi Silberschatz 、 Peter Baer Galvin 和 Greg Gagne 的《Operating System Concepts》, Remzi H. Arpaci-Dusseau 和 Andrea C. Arpaci-Dusseau 的《Operating Systems: Three Easy Pieces》等。

有待思考的问题#

然而,从我们自 2000 年以来的教学实践来看,某些经典教材对操作系统的概念和原理很重视,但还有如下一些问题有待进一步思考:

  • 原理与实践脱节:与操作系统的具体实现而言,操作系统的原理与概念相对过于抽象。目前的一些教材缺乏在“操作系统的原理与概念”和“操作系统的设计与实现”之间建立关联关系的桥梁,使得二者之间存在较大的鸿沟。这导致学生即使知道了操作系统的概念,还只能停留在“纸上谈兵”的阶段,依然不知如何实现一个操作系统。另外,学生在完成设计与实现操作系统的实验过程中,容易“一叶障目,不见泰山”,陷入到硬件规范、汇编代码、数据结构、编程优化等细节中,不知这些细节与操作系统概念的关系,缺少全局观和系统思维,难以与课堂上老师讲解的操作系统基本概念对应起来。

  • 缺少历史发展的脉络:以史为鉴,可以知兴替。操作系统的概念和原理是从实际操作系统设计与实现的历史发展过程中,随着计算机硬件和应用需求的变化,从无到有逐步演进而产生的,有其发展的历史渊源和规律。但目前的大部分教材只提及当前主流操作系统的概念和原理,有“凭空出现”的感觉,学生并不知道这些内容出现的前因后果,只知道 “How” ,而不知道 “Why” 。而且操作系统发展史上的很多设计思路和实践方法起起伏伏,不断演进,它们并没有过时,而是以新的形态出现。如操作系统远古阶段的 LibOS 设计思路在当前云计算时代重新焕发青春,成为学术机构和各大互联网企业探索的新热点。

  • 忽视硬件细节或用复杂硬件:很多教材忽视或抽象硬件细节,使得操作系统概念难以落地,学生了解不到软硬件是如何具体协同运行的。部分教材把复杂的 x86 处理器作为操作系统实验的硬件参考平台,缺乏对当前快速发展的 RISC-V 等精简体系结构的实验支持,使得学生在操作系统实验中可能需要花较大代价了解相对繁杂的 x86 硬件细节,编程容易产生缺陷(bug),影响操作系统实验的效果,以及对操作系统核心概念的掌握。

解决问题的思路#

这些现存问题增加了学生学习和掌握操作系统的难度。我们尝试通过如下方法来解决上面三个问题,达到缓解学生的学习压力,提升学习兴趣,能在一个学期内比较好地掌握操作系统的目标。

具体而言,为应对“原理与实践脱节”的问题,我们强调 实践先行,实践引领原理 的教学理念。MIT 教授 Frans Kaashoek 等师生设计实现了基于 UNIX v6 的 xv6 教学操作系统用于每年的本科操作系统课的实验中,并在课程讲解中把原理和实验结合起来,在国际上得到了广泛的认可,也给了我们很好的启发。经过十多年的操作系统教学工作,我们认为:对一位计算机专业的本科生而言,设计实现一个操作系统(包括CPU)有挑战但可行,前提是这样的操作系统要简洁小巧,能体现操作系统中最基本的核心思想,并能把操作系统各主要部分的原理与概念关联起来,形成一个整体。而且还需要丰富的配套资源,比如对操作系统的整体框架、核心算法、关键组件之间的联系等的分析文档、配套的图示和视频讲解、能够自动测试操作系统功能的测试用例和测试环境、能展现操作系统逐步编写过程的在线源代码版本管理环境,以及逐步递进的综合性在线实验环境等,这样就能够让学生很方便地通过实践来加深对操作系统原理和概念的理解,并能让操作系统原理和概念落地。

为应对“缺少历史发展的脉络”的问题,我们重新设计操作系统实验和教学内容,按照操作系统的历史发展过程来设立每一章的内容,每一章会围绕操作系统支持应用的某个核心目标来展开,形成相应的软硬件基本知识点和具体实践内容。同时建立与每章配套的多个逐步递进且相对独立的小实验,每个实验会形成一个独立的操作系统,体现了操作系统的一个微缩的发展历史,并可从中归纳总结出操作系统相关的概念与原理。这样可以在教学中引导学生理解操作系统的这些概念和原理是如何一步一步演进的。表面上看,这样会要求同学了解多个不同的操作系统,增加了同学的学习负担。但其实每个实验中的操作系统都是在前一个实验的操作系统上的渐进式扩展,同学只需理解差异的部分即可。而且学生通过分析不同操作系统对应用支持能力和对应实现上的差异,可以更加深入地理解相关操作系统概念与原理出现的前因后果。也许有同学认为讲解历史上的操作系统太过时了。但我们认为:技术可以过时,思想值得传承。

为应对“忽视硬件细节或用复杂硬件”的问题,我们在硬件(x86, ARM, MIPS, RISC-V 等)和编程语言(C, C++, Go, Rust 等)选择方面进行了多年尝试。在 2017 年把 复杂的 x86 架构换为 简洁的 RISC-V 架构,作为操作系统实验的硬件环境,降低了学生学习硬件细节的负担。在 2018 年引入 Rust 编程语言作为操作系统实验的可选编程语言之一,通过Rust语言特有的编译时内存/并发安全检查、强类型静态分析、高级数据结构支持、抽象封装支持、内嵌汇编和丰富的第三方库,来提高编程效率、降低调试成本,从而减少了用C语言编程和对硬件操作出现较多运行时缺陷的情况。使得学生以相对较小的开发和调试代价进行操作系统实验。同时,我们把操作系统的概念和原理直接对应到程序代码、硬件规范和操作系统的实际执行中,加强学生对操作系统内涵的实际体验和感受。

如何基于本书学习操作系统#

前期准备#

学习操作系统需要有一些前期准备,主要包括计算机科学基础知识,比如计算机组成原理、数据结构与算法、编程语言、软件开发环境等。具体而言,需要了解计算机的基本原理,特别是RISC-V处理器的指令集和部分特权操作;还有就是需要掌握基本的数据结构和算法,毕竟操作系统也是一种软件,需要通过多种数据结构和算法解决问题;在了解操作系统的设计并进行操作系统实验的过程中,需要掌握系统级的高级编程语言和汇编语言,比如C或Rust编程语言,RISC-V汇编语言,这样才能深入理解操作系统的实现细节和设计思想;最后还需掌握操作系统的开发与实验环境,本书的主要涉及的开发与实验环境是Linux,所以同学们需要能够通过Linux的命令行界面使用各种开发工具和辅助工具,而掌握基于图形界面或字符界面的IDE集成开发环境,如VSCode、Vim、Emacs等,可以提高分析操作系统源码,简化操作系统的开发与调试过程。

目标与步骤#

所以本书的目标是以简洁的 RISC-V 基本架构为底层硬件基础,根据上层应用从小到大的需求,按 OS 发展的历史脉络,逐步讲解如何设计实现能满足“从简单到复杂”应用需求的多个“小”操作系统。并且在设计实现操作系统的过程中,逐步解析操作系统各种概念与原理的知识点,做到有“理”可循和有“码”可查,最终让同学通过操作系统设计与实现来深入地掌握操作系统的概念与原理。

在本书中,第零章是对操作系统的一个概述,让同学对操作系统的历史、定义、特征等概念上有一个大致的了解。后面的每个章节体现了操作系统的一个微缩的历史发展过程,即从对应用由简到繁的支持角度出发,每章会讲解如何设计一个可运行应用的操作系统,满足应用的阶段性需求。从而同学可以通过配套的操作系统设计实验,了解如何从一个微不足道的“小”操作系统,根据应用需求,添加或增强操作系统功能,逐步形成一个类似 UNIX 的相对完善的“小”操作系统。每一步都小到足以让人感觉到易于掌控。而在每一步结束时,你都能运行一个支持不同应用执行的“小”操作系统。

本书提供了哪些“小”操作系统?

我们按照操作系统的发展历史,设计了如下一些逐步进化的“小”操作系统

  • LibOS: 让APP与HW隔离,简化应用访问硬件的难度和复杂性

  • BatchOS: 让APP与OS隔离,加强系统安全,提高执行效率

  • Multiprog & Timesharing OS: 让APP共享CPU资源

  • Address Space OS: 隔离APP访问的内存地址空间,限制APP之间的互相干涉,提高安全性

  • Process OS: 支持APP动态创建新进程,增强进程管理和资源管理能力

  • Filesystem OS:支持APP对数据的持久保存

  • IPC OS:支持多个APP进程间数据交互与事件通知

  • Thread & Coroutine OS:支持线程和协程APP,简化切换与数据共享

  • SyncMutex OS:在多线程APP中支持对共享资源的同步互斥访问

  • Device OS:提高APP的I/O效率和人机交互能力,支持基于外设中断的串口/块设备/键盘/鼠标/显示设备

另外,通过足够详尽的测试程序和自动测试框架,可以随时验证同学实现的操作系统在每次更新后是否正常工作。由于实验的代码规模和实现复杂度在一个逐步递增的可控范围内,同学可以结合对应操作系统实验的原理/概念分析,来建立操作系统概念原理和实际实现的对应关系,从而能够通过操作系统实验的实践过程来加强对理论概念的理解,并通过理论概念来进一步指导操作系统实验的实现与改进。

如何学习操作系统?

这取决于你想学习操作系统的目标,这里主要分为两类:

  • 掌握基本原理为主,了解具体实现为辅(一般学习)

    • 理解式学习方式:逐章阅读与实践,阅读分析应用,并通过分析应用与OS的动态执行过程,掌握OS原理。

  • 掌握操作系统实现和原理为主(深入学习)

    • 构造式学习:在理解式学习方式基础上,进一步分析源码,逐步深入了解每个OS的内部增量实现,并且参考并基于这些小OS,扩展部分OS功能,通过测试用例,从而同时掌握操作系统实现和原理。

编程语言与硬件环境#

在你开始阅读与实践本书讲解的内容之前,你需要决定用什么编程语言来完成操作系统实验。你可以选择你喜欢的编程语言和在你喜欢的CPU上来实现操作系统。我们推荐的编程语言和架构分别是 Rust 和 RISC-V。

编程语言与指令集选择

目前常见的操作系统内核都是基于 C 语言的,为何要推荐 Rust 语言?

  • 事实上, C 语言就是为写 UNIX 而诞生的。Dennis Ritchie 和 KenThompson 没有期望设计一种新语言能帮助高效地开发复杂与并发的操作系统逻辑(面向未来),而是希望用一种简洁的方式来代替难以使用的汇编语言抽象出计算机的行为,便于编写控制计算机硬件的操作系统(符合当时实际情况)。

  • C 语言的指针既是天使又是魔鬼。它灵活且易于使用,但语言本身几乎不保证安全性,且缺少有效的并发支持。这导致内存和并发漏洞成为当前基于 C 语言的主流操作系统的噩梦。

  • Rust 语言具有与 C 一样的硬件控制能力,且大大强化了安全编程和抽象编程能力。从某种角度上看,新出现的 Rust 语言的核心目标是解决 C 的短板,取代 C 。所以用 Rust 写 OS 具有很好的开发和运行体验。

  • 用 Rust 写 OS 的代价仅仅是学会用 Rust 编程。

目前常见的指令集架构是 x86 和 ARM ,为何要推荐 RISC-V ?

  • 目前为止最常见的指令集架构是 x86 和 ARM ,它们已广泛应用在服务器、台式机、移动终端和很多嵌入式系统中。由于它们的通用性和向后兼容性需求,需要支持非常多(包括几十年前实现)的软件系统和应用需求,导致这些指令集架构越来越复杂。

  • x86 后向兼容的策略确保了它在桌面和服务器领域的江湖地位,但导致其丢不掉很多已经比较过时的硬件设计,让操作系统通过冗余的代码来适配各种新老硬件特征。

  • x86 和 ARM 在商业上都很成功,其广泛使用使得其 CPU 硬件逻辑越来越复杂,且不够开放,不能改变,不是开源的,难以让感兴趣探索硬件的学生了解硬件细节,在某种程度上让CPU成为了一个黑盒子,并使得操作系统与硬件的交互变得不那么透明,增加了学习操作系统的负担。

  • 从某种角度上看,新出现的 RISC-V 的核心目标是灵活适应未来的 AIoT (人工智能物联网, AI + IoT)场景,保证基本功能,提供可配置的扩展功能。其开源特征使得学生都可以深入CPU的运行细节,甚至可以方便地设计一个 RISC-V CPU。从而可帮助学生深入了解操作系统与硬件的协同执行过程。

  • 编写面向 RISC-V 的 OS 的硬件学习代价仅仅是你了解 RISC-V 的 Supervisor 特权模式,知道 OS 在 Supervisor 特权模式下的控制能力。

本书章节导引#

本书由0~9共10章组成,其中第0章是本书的总览,介绍了为何写本书,概述了操作系统的简要发展历史,操作系统的定义,系统调用接口,操作系统的抽象表示和特征等,以及如何基于本书来学习操作系统。

第1章主要讲解了如何通过操作系统来解决应用和硬件隔离达到简化应用编程的问题。并详细讲述了如何设计和实现建立在裸机上的执行环境,如何编写可在裸机执行环境上运行的显示“Hello World”的应用程序。最终形成可运行在裸机上的寒武纪“三叶虫”操作系统 – LibOS。这样学生能对应用程序和它所依赖的执行环境的抽象概念与具体实现有一个全面和深入的理解。

第2章主要讲解了如何通过操作系统来保障系统安全和多应用支持这两个核心问题。并详细讲述了应该如何设计应用程序,如何通过批处理方式支持多个程序的自动加载和运行,如何实现应用程序与操作系统在执行特权上的隔离。最终形成可运行多个应用程序的泥盆纪“邓式鱼”操作系统 – BatchOS。这样学生可以看到系统调用、特权级、批处理等概念在操作系统上的具体实现,并了解如何通过批处理方式提高系统的整体性能,如何通过特许权隔离来保护操作系统,如何实现跨特权级的系统调用等操作系统核心技术。

第3章主要讲解了如何在提高多程序运行的整体性能并保证多个程序运行的公平性这两个核心问题。并详细讲述了如何通过提前加载应用程序到内存来减少应用程序切换开销,如何通过应用程序之间的协作机制来支持程序主动放弃处理器并提高系统整体性能,如何通过基于硬件中断的抢占机制支持程序被动放弃处理器来保证不同程序对处理器资源使用的公平性,也进一步提高了应用对 I/O 事件的响应效率。最终形成了支持多道程序的二叠纪“锯齿螈” 操作系统 – MultiprogOS,支持协作机制的三叠纪“始初龙” 操作系统 – CoopOS,支持分时多任务的三叠纪“腔骨龙” 操作系统 – TimesharingOS。这样学生可以通过分析这些操作系统的设计与实现,提炼出任务、任务切换等操作系统的核心概念,对计算机硬件的中断处理机制、操作系统的分时共享等机制有更深入的理解。

第4章主要讲解了内存的安全隔离问题和高效使用问题。有限的物理内存是操作系统需要管理的一个重要资源,如何让运行在一台计算机上的多个应用程序得到无限大的内存空间,如何能够隔离运行应用能访问的内存空间并保证不同应用之间的内存安全是本章要重点解决的问题。为此需要了解计算机硬件中的页表和TLB机制,并通过操作系统在内存中构建面向自身和不同应用的页表,形成应用与应用之间、应用与操作系统之间的内存隔离,从而解决内存安全隔离问题。通过缺页异常和动态修改页表等技术,让当前运行的应用正在或即将访问的数据位于内存中,不常用的数据缓存放到存储设备(如硬盘等),形成分时复用内存的操作系统能力,即“虚存”能力。最终形成支持内存隔离的侏罗纪“头甲龙”操作系统 – Address Space OS。学生通过分析操作系统的设计与实现,可以把地址空间这样的抽象概念和页表的具体设计建立起联系,掌握如何通过页表机制来实现地址空间。对任务切换中增加的地址空间切换机制也会有更深入的了解。能够理解虚存机制中的各种页面置换策略能否有效实现,以及如何具体实现。

第5章主要讲解了如何提高应用程序动态执行的灵活性和交互性的问题,即让开发者能够及时控制程序的创建、运行和退出的管理问题。在第5章之前,在操作系统整个执行过程中,应用程序是被动地被操作系统加载运行,开发者与操作系统之间没有交互,开发者与应用程序之间没有交互,应用程序不能控制其它应用的执行。这使得用户不能灵活地选择执行某个程序。这需要给用户提供一个灵活的应用程序(俗称 shell ),形成用户与操作系统进行交互的命令行界面(Command Line Interface)。用户可以在这个 shell 程序中输入命令即可启动或杀死应用,或者监控系统的运行状况,使得开发者可以更加灵活地控制系统。这种新的用户需求需要重构操作系统的功能,让操作系统提供支持应用程序动态创建/销毁/等待/暂停等服务。这就在已有的 任务 抽象的基础上进一步新抽象: 进程 ,用于表示和管理应用程序的整个执行过程。这样最终形成具备灵活强大的进程管理功能的白垩纪“伤齿龙”操作系统 – Process OS。学生通过分析操作系统的设计与实现,可以把进程、进程调度、进程切换、进程状态、进程生命周期这样的抽象概念与操作系统实现中的进程控制块数据结构、进程相关系统调用功能、进程调度与进程切换函数的具体设计建立其联系,能够更加深入掌握进程这一操作系统的核心概念。

第6章主要讲解了如何让程序方便地访问存储设备上的数据的问题。由于放在内存中的数据在计算机关机或掉电后就会消失,所以应用程序要把内存中需要长久保存的数据放到存储设备上存起来,并在需要的时候能读到内存中进行处理。文件和文件系统的出现极大地简化了应用程序访问存储设备上数据的操作。第6章将设计并实现操作系统和核心模块,即一个简单的文件系统 – easyfs,向上给应用程序提供了常规文件和目录文件两种抽象,并提供 openclosereadwrite 四个系统调用来读写文件中的数据,向下通过存储设备驱动程序对存储设备这种 I/O 外设物理资源进行管理。这样就形成了支持文件访问的 “霸王龙” 操作系统 – Filesystem OS。学生通过分析操作系统的设计与实现,可以看到文件、文件系统这样的操作系统抽象如何通过一个具体的文件系统 – easyfs 来体现的。并可以看到并理解文件系统与进程管理、内存管理之间的紧密联系,从而支持应用程序便捷地对存储设备上的数据进行访问。

第7章主要讲解如何让不同的应用进行数据共享与合作的问题。在第7章之前,进程之间被操作系统彻底隔离了,导致进程之间无法方便地分享数据,不能一起协作。如果能让不同进程实现数据共享与交互,就能把不同程序的功能组合在一起,实现更加强大和灵活的复杂功能。第7章的核心目标就是让不同应用通过进程间通信的方式组合在一起运行。为此,将引入新的操作系统概念 – 管道(pipe),以支持进程间的I/O重定向功能,即让一个进程的输出成为另外一个进程的输入,从而让进程间能够有效地合作起来。这样管道其实也可以看成是一种特殊的内存文件,并可基于文件的操作来实现进程间的内存数据共享。除了数据共享机制,进程间也需要快捷的通知机制,这就引出了信号(Signal) 事件通知机制,让进程能够及时的获得并处理来自其他进程或操作系统发的紧急通知。这样最终形成支持多个APP进程间数据交互与事件通知功能的白垩纪“迅猛龙”操作系统 – IPC OS。学生通过分析操作系统的设计与实现,可以看到进程间的隔离和共享是可以同时做到的,并可进一步了解在进程的基础上如何通过管道机制来打破进程间建立的地址空间隔离,实现数据共享,以及如何通过信号机制打断进程的正常执行来及时响应相对紧急的事件,从而掌握多应用共享协同的操作系统机制。

第8章主要讲解如何提高多个应用并发执行的效率和如何保证能多个应用正确访问共享资源的问题。进程的地址空间隔离会带来管理上的运行时开销,比如TLB刷新、页表切换等。如果把一个进程内的多个可并行执行的任务通过一种更细粒度的方式让操作系统进行调度,那么就可以在进程内实现并发执行,且由于这些任务在进程内的地址空间中,不会带来页表切换等运行时开销。这里的任务就是线程(Thread)。线程间共享地址空间,使得它们访问共享资源更加方便,但如果处理不当,就可能出现资源访问冲突和竞争的问题。这就需要通过同步机制来协调进程或线程的执行顺序,并通过互斥机制来保证在同一时刻只有一个进程或线程可以访问共享资源,从而避免了资源冲突和竞争的问题。第8章在进程管理的基础上进行重构,设计实现了线程管理机制,形成了支持多线程app的达科塔盗龙OS – ThreadOS;并进一步设计了支持线程同步互斥访问共享资源的锁机制、信号量机制和条件变量机制,最终形成了支持多线程APP同步互斥访问共享资源的白垩纪“慈母龙”操作系统 – SyncMutex OS。学生通过分析操作系统的设计与实现,可以理解线程和进程的关系与区别,理解同步互斥机制的不同特征和运行机理,从而能够深入理解支持并发访问共享资源的同步互斥机制的原理和实现。

第9章主要讲解如何让应用便捷访问I/O设备并让应用有更多感知与交互能力的问题。计算机中的外设特征各异,如显卡、触摸屏、键盘、鼠标、网卡、声卡等。在第9章之前,同学们已经接触到了串口、时钟、和磁盘设备,使得应用程序能通过操作系统输入输出字符、访问时间、读写在磁盘上的数据,并通过时钟中断让操作系统具有了抢占式分时多任务调度的能力,但这仅仅覆盖了很小的一部分外设,而且在实践上对操作系统与外设的交互细节也涉及不多。操作系统需要对外设有更多的深入理解,才能有效地管理和访问外设,给应用提供丰富的感知与交互能力。在原理与概念方面,第9章简要分析了外设的发展历程,外设的数据传输方式。并进一步阐述操作系统如何对外设建立不同层次的抽象和不同I/O执行模型,以便于操作系统对外设的内部管理,应用程序对外设的高效便捷访问。在实践上,第9章分析了操作系统如何通过设备树(Device Tree)来解析出计算机中的外设信息,并重新设计了基于中断方式的串口驱动程序,涉及串口设备初始化和串口数据输入输出,以及改进进程/线程的调度机制,让等待串口输入或输出完成的进程/线程进入阻塞状态,从而提高系统整体执行效率。在第9章还进一步介绍了QEMU模拟的virtio设备架构,以及virtio设备驱动程序的主要功能;并对virtio-blk设备及其驱动程序,virtio-gpu设备及其驱动程序进行了比较深入的分析。这样最终形成支持图形游戏APP并具备高效外设中断响应的侏罗纪侏罗猎龙操作系统 – Device 学生通过分析操作系统的设计与实现,可以深入了解不同外设的特征,外设的I/O传输方式,不同层次的外设抽象概念和I/O执行模型,从而对操作系统如何有效管理不同类型的外设有一个相关完整的理解。

百闻不如一见,如果同学们通过读书和阅读代码能逐步地明确每一章要解决的应用需求和问题,渐进地了解每章操作系统中内核模块的组成,并掌握内核模块的功能,以及不同内核模块之间的关系,能归纳总结出操作系统的设计思路、策略与机制、原理与概念,就能达到了解操作系统的层次。百见不如一干,仅仅看还是不够的,本书的重要目标是希望能推动同学们能够通过编程来掌握操作系统。如果同学们还能通过课后习题和编程实验来完成操作系统的新功能,发现编程中的bug并修复bug,通过测试用例,实现你自己编写的操作系统,那将达到掌握操作系统的更高层次。希望同学们能够完整走完整个操作系统的学习和练习的过程,当你完成整个过程后,再回首看,能够发现原来操作系统还可以这样有趣和有用。