提交 e37ff4dd 编写于 作者: deathwish5's avatar deathwish5

merge main

......@@ -16,7 +16,7 @@ Remzi H. Arpaci-Dusseau 和 Andrea C. Arpaci-Dusseau 的《Operating Systems: Th
为应对“缺少历史发展的脉络”的问题,我们重新设计操作系统实验和教学内容,按照操作系统的历史发展过程来建立多个相对独立的小实验,每个实验体现了操作系统的一个微缩的历史,并从中归纳总结出操作系统相关的概念与原理,并在教学中引导学生理解这些概念和原理是如何一步一步演进的。
为应对“忽视硬件细节或用复杂硬件”的问题,我们在硬件(x86, ARM, MIPS, RISC-V 等)和编程语言(C, C++, Go, Rust 等)选择方面进行了多年尝试。在 2017 年引入了 RISC-V 架构作为操作系统实验的硬件环境,在 2018 年引入 Rust 编程语言作为开发操作系统的编程语言,使得学生以相对较小的开发和调试代价能够用 Rust 语言编写运行在 RISC-V 上的操作系统。而且方便和简化了让操作系统的概念和原理形象化,可视化的过程。学生可以把操作系统的概念和原理直接对应到程序代码、硬件规范和操作系统的实际执行中,加强了学生对操作系统内涵的实际体验和感受。
为应对“忽视硬件细节或用复杂硬件”的问题,我们在硬件(x86, ARM, MIPS, RISC-V 等)和编程语言(C, C++, Go, Rust 等)选择方面进行了多年尝试。在 2017 年引入了 RISC-V 架构作为操作系统实验的硬件环境,在 2018 年引入 Rust 编程语言作为开发操作系统的编程语言,使得学生以相对较小的开发和调试代价能够用 Rust 语言编写运行在 RISC-V 上的操作系统。我们简化了形象化、可视化操作系统的概念和原理的过程,目的是让学生可以把操作系统的概念和原理直接对应到程序代码、硬件规范和操作系统的实际执行中,加强学生对操作系统内涵的实际体验和感受。
所以本书的目标是以简洁的 RISC-V 架构为底层硬件基础,根据上层应用从小到大的需求,按 OS 发展的历史脉络,逐步讲解如何设计并实现满足这些需求的“从小到大”的多个“小”操作系统,并在设计实现操作系统的过程中,逐步解析操作系统各种概念与原理的知识点,对应的做到有“理”可循和有“码”可查,最终让读者通过主动的操作系统设计与实现来深入地掌握操作系统的概念与原理。
......@@ -32,15 +32,15 @@ Remzi H. Arpaci-Dusseau 和 Andrea C. Arpaci-Dusseau 的《Operating Systems: Th
**目前常见的操作系统内核都是基于 C 语言的,为何要推荐 Rust 语言?**
- 没错, C 语言就是为写 UNIX 而诞生的。Dennis Ritchie 和 KenThompson 没有期望设计一种新语言能帮助高效简洁地开发复杂的应用业务逻辑,只是希望用一种简洁的方式抽象出计算机的行为,便于编写控制计算机硬件的操作系统,最终的结果就是 C 语言
- C 语言的指针既是天使又是魔鬼,且 C 语言缺少有效的并发支持,导致内存和并发漏洞成为当前操作系统的噩梦。
- 事实上, 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 硬件逻辑越来越复杂,且不够开放,不能改变,不是开源的,提高了操作系统开发者的学习难度。
- 目前为止最常见的架构是 x86 和 ARM ,他们已广泛应用在服务器,台式机,移动终端和很多嵌入式系统中。它们需要支持非常多的软件系统和应用需求,导致它们越来越复杂。
- x86 后向兼容的策略确保了它的江湖地位,但导致其丢不掉很多已经比较过时的硬件设计,让操作系统疲于适配这些硬件特征。
- x86 和 ARM 在商业上都很成功,其广泛使用使得其 CPU 硬件逻辑越来越复杂,且不够开放,不能改变,不是开源的,提高了操作系统开发者的学习难度。
- 从某种角度上看,新出现的 RISC-V 的核心目标是灵活适应未来的 AIoT 场景,保证基本功能,提供可配置的扩展功能。其开源特征使得学生都可以方便地设计一个 RISC-V CPU。
- 写面向 RISC-V 的 OS 的代价仅仅是你了解 RISC-V 的 Supevisor 特权模式,知道 OS 在 Supevisor 特权模式下的控制能力。
\ No newline at end of file
......@@ -18,9 +18,9 @@
:scale: 50 %
:name: computer-hw-sw
如果看看我们的身边, Android 应用运行在 ARM 处理器上 Android 操作系统的执行环境中,微软的 Office 应用运行在 x86-64 处理器上 Windows 操作系统的执行环境中,Web Server应用运行在 x86-64 处理器上 Linux 操作系统的执行环境中, Web app 应用运行在 x86-64 或 ARM 处理器上 Chrome OS 操作系统的执行环境中。而在一些嵌入式环境中,操作系统以运行时库的形式与应用程序紧密结合在一起,形成一个可在嵌入式硬件上的单一执行软件。所以,在不同的应用场景下,操作系统的边界也是不同的,我们可以把运行时库、图形界面支持库等这些可支持不同应用的系统软件 (System Software) 也看成是操作系统的一部分。
如果看看我们的身边, Android 应用运行在 ARM 处理器上 Android 操作系统的执行环境中,微软的 Office 应用运行在 x86-64 处理器上 Windows 操作系统的执行环境中,Web Server应用运行在 x86-64 处理器上 Linux 操作系统的执行环境中, Web app 应用运行在 x86-64 或 ARM 处理器上 Chrome OS 操作系统的执行环境中。而在一些嵌入式环境中,操作系统以运行时库的形式与应用程序紧密结合在一起,形成一个可在嵌入式硬件上单一执行的嵌入式应用。所以,在不同的应用场景下,操作系统的边界也是不同的,我们可以把运行时库、图形界面支持库等这些可支持不同应用的系统软件 (System Software) 也看成是操作系统的一部分。
站在应用程序的角度来看,我们可以发现常见的应用程序其实是运行在由硬件、操作系统、运行时库、图形界面支持库等所包起来的一个 :ref:`执行环境 (Execution Environment) <exec-env>` 中,应用程序只需根据与系统软件约定好的应用程序二进制接口 (ABI, Application Bianry Interface) 来请求执行环境提供的各种服务或功能,从而完成应用程序自己的功能。基于这样的观察,我们可以把操作系统再简单一点地定义为: **应用程序的软件执行环境** 。从这个角度出发,操作系统可以包括运行时库、图形界面支持库等系统软件,也能适应在操作系统发展的不同历史时期对操作系统的概括性描述和定义。
站在应用程序的角度来看,我们可以发现常见的应用程序其实是运行在由硬件、操作系统、运行时库、图形界面支持库等所包起来的一个 :ref:`执行环境 (Execution Environment) <exec-env>` 中,应用程序只需根据与系统软件约定好的应用程序二进制接口 (ABI, Application Binary Interface) 来请求执行环境提供的各种服务或功能,从而完成应用程序自己的功能。基于这样的观察,我们可以把操作系统再简单一点地定义为: **应用程序的软件执行环境** 。从这个角度出发,操作系统可以包括运行时库、图形界面支持库等系统软件,也能适应在操作系统发展的不同历史时期对操作系统的概括性描述和定义。
.. image:: EE.png
:align: center
......@@ -29,9 +29,9 @@
站在计算机发展的百年时间尺度看
----------------------------------
虽然电子计算机的出现距今才仅仅七十年左右,但计算机技术和操作系统已经发生了巨大的变化。从计算机发展的短暂的历史角度看,操作系统也是从无到有地逐步发展起来的。操作系统主要完成对硬件控制和对应用程序的服务所必需的功能,它的历史与计算机的发展史密不可分。操作系统的内涵和功能随着历史的发展也在一直变化、改进中。如今在二十一世纪初期的大众眼中,操作系统就是他们的手机/终端上的软件系统,包括各种应用程序集合,图形界面和网络浏览器是其中重要的组成部分。
虽然电子计算机的出现距今才仅仅七十年左右,但计算机技术和操作系统已经发生了巨大的变化。从计算机发展的短暂的历史角度看,操作系统也是从无到有地逐步发展起来的。操作系统主要完成控制硬件控制和为应用程序提供服务这些必不可少的功能,它的历史与计算机的发展史密不可分。操作系统的内涵和功能随着历史的发展也在一直变化、改进中。如今在二十一世纪初期的大众眼中,操作系统就是他们的手机/终端上的软件系统,包括各种应用程序集合,图形界面和网络浏览器是其中重要的组成部分。
其实,操作系统的内涵和外延随着历史的发展也一直在变化,并没有类似于“1+1=2”这样的明确定义。参考生物的进化史,我们也给操作系统的进化历史做一个简单的概述,从中可以看到操作系统在各个时间段上包含什么,具有什么样的特征。但无论操作系统的内在实现和具体目标如何变化,其管理计算机硬件,给应用提供服务的功能需求没有变化。
其实,操作系统的内涵和外延随着历史的发展也一直在变化,并没有类似于“1+1=2”这样的明确定义。参考生物的进化史,我们也给操作系统的进化历史做一个简单的概述,从中可以看到操作系统在各个时间段上包含什么,具有什么样的特征。但无论操作系统的内在实现和具体目标如何变化,其管理计算机硬件,给应用提供服务的核心定位没有变化。
寒武纪生物大爆发时代
~~~~~~~~~~~~~~~~~~~~~~
......@@ -47,12 +47,12 @@
泥盆纪鱼类时代和二叠纪两栖动物时代
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
在 20 世纪 50~60 年代,计算机发展到大型机阶段,而所对应的早期操作系统非常多样化、专用化,生产商生产出针对各自硬件的专用操作系统,大部分用汇编语言编写,这导致操作系统的进化比较缓慢,但进化在持续,从“手工操作”进化到了“批处理”阶段和“多道程序”阶段。在 1964 年, IBM 公司开发了面向 System/360 系列机器的统一可兼容的操作系统—— OS/360 ,它是一种批处理操作系统。为了能充分地利用计算机系统,应尽量使该系统连续运行,减少空闲时间,所以批处理操作系统把一批作业(古老的术语,可理解为现在的程序)以脱机方式输入到磁带上,并使这批作业能一个接一个地连续处理,流程如下:
在 20 世纪 50~60 年代,计算机发展到大型机阶段,而所对应的早期操作系统非常多样化、专用化,生产商生产出针对各自硬件的专用操作系统,大部分用汇编语言编写,这导致操作系统的进化比较缓慢,但进化在持续进行,从“手工操作”进化到了“批处理”阶段和“多道程序”阶段。在 1964 年, IBM 公司开发了面向 System/360 系列机器的统一可兼容的操作系统—— OS/360 ,它是一种批处理操作系统。为了能充分地利用计算机系统,应尽量使该系统连续运行,减少空闲时间,所以批处理操作系统把一批作业(古老的术语,可理解为现在的程序)以脱机方式输入到磁带上,并使这批作业能一个接一个地连续处理,流程如下:
1. 将磁带上的一个作业装入内存;
2. 把运行控制权交给该作业;
3. 当该作业处理完成后,把控制权交还给操作系统;
4. 重复1-3的步骤。
2. 操作系统把运行控制权交给该作业;
3. 当该作业处理完成后,控制权被交还给操作系统;
4. 重复1-3的步骤处理下一个作业直到所有作业处理完毕
批处理操作系统分为单道批处理系统和多道批处理系统。单道批处理操作系统只能管理内存中的一个(道)作业,无法充分利用计算机系统中的所有资源,致使系统整体性能较差。多道批处理操作系统能管理内存中的多个(道)作业,可比较充分地利用计算机系统中的所有资源,提升系统整体性能。
......@@ -71,7 +71,7 @@
.. note::
可以在 :ref:`本书第三章 <link-chapter3>` 的第四节可以看到分时操作系统的设计实现,以及操作系统可强制让应用程序被动放弃 CPU 以提高系统整体执行效率的过程。并且 UNIX 还有虚存、文件、进程等当前操作系统的关键特性,这些内容也在本书的第四章~第七章中有详细的设计描述。
可以在 :ref:`本书第三章 <link-chapter3>` 的第四节可以看到分时操作系统的设计实现,以及操作系统可强制让应用程序被动放弃 CPU 使得应用可以公平共享系统中的软硬件资源。并且 UNIX 还有虚存、文件、进程等当前操作系统的关键特性,这些内容也在本书的第四章~第七章中有详细的设计描述。
古近纪哺乳动物时代
~~~~~~~~~~~~~~~~~~~~~~~
......
......@@ -12,23 +12,23 @@
**API 与 ABI 的区别**
应用程序二进制接口 ABI 是不同二进制代码片段的连接纽带。ABI 定义了二进制机器代码级别的规则,主要包括基本数据类型,通用寄存器的使用,参数的传递规则,以及堆栈的使用等等。ABI 是用来约束链接器 (Linker) 和汇编器 (Assembler) 的。基于不同高级语言编写的应程序、库和操作系统,如果遵循同样的ABI定义,那么它们就能正确链接和执行。
应用程序二进制接口 ABI 是不同二进制代码片段的连接纽带。ABI 定义了二进制机器代码级别的规则,主要包括基本数据类型,通用寄存器的使用,参数的传递规则,以及堆栈的使用等等。ABI 是用来约束链接器 (Linker) 和汇编器 (Assembler) 的。基于不同高级语言编写的应用程序、库和操作系统,如果遵循同样的 ABI 定义,那么它们就能正确链接和执行。
应用程序编程接口 API 是不同源代码片段的连接纽带。API 定义了一个源码(如 C 语言)级函数的参数,参数的类型,函数的返回值等。因此 API 是用来约束编译器 (Compiler) 的:一个 API 是给编译器的一些指令,它规定了源代码可以做以及不可以做哪些事。API 与编程语言相关,如 LibC 是基于 C 语言编写的标准库,那么基于 C 的应用程序就可以通过编译器建立与 LibC 的联系,并能在运行中正确访问 LibC 中的函数。
应用程序编程接口 API 是不同源代码片段的连接纽带。API 定义了一个源码级(如 C 语言)函数的参数,参数的类型,函数的返回值等。因此 API 是用来约束编译器 (Compiler) 的:一个 API 是给编译器的一些指令,它规定了源代码可以做以及不可以做哪些事。API 与编程语言相关,如 LibC 是基于 C 语言编写的标准库,那么基于 C 的应用程序就可以通过编译器建立与 LibC 的联系,并能在运行中正确访问 LibC 中的函数。
对于实际操作系统而言,具有大量的服务接口,比如目前 Linux 有三百个系统调用接口。下面列出了一些相对比较重要的操作系统接口:
对于实际操作系统而言,具有大量的服务接口,比如目前 Linux 有三百个系统调用接口。下面列出了一些相对比较重要的操作系统接口或抽象
* 进程(即程序运行过程)管理:复制创建进程 ``fork`` 、退出进程 ``exit`` 、执行进程 ``exec`` ...
* 同步互斥的并发控制:信号量 ``semaphore`` 、管程 ``monitor`` 、条件变量 ``condition variable`` ...
* 进程间通信:管道 ``pipe`` 、信号 ``signal`` 、事件 ``event`` ...
* 虚存管理:内存空间映射 ``mmap`` 、改变数据段地址空间大小 ``sbrk`` 、共享内存 ``shm`` ...
* 文件I/O操作:读 ``read`` 、写 ``write`` 、打开 ``open`` 、关闭 ``close`` ...
* 外设I/O操作:外设包括键盘、显示器、串口、磁盘、时钟 ...,但接口是直接采用了文件I/O操作的系统调用接口
* 进程(即程序运行过程)管理:复制创建进程 fork 、退出进程 exit 、执行进程 exec 等。
* 同步互斥的并发控制:信号量 semaphore 、管程 monitor 、条件变量 condition variable 等。
* 进程间通信:管道 pipe 、信号 signal 、事件 event 等。
* 虚存管理:内存空间映射 mmap 、改变数据段地址空间大小 sbrk 、共享内存 shm 等。
* 文件I/O操作:读 read 、写 write 、打开 open 、关闭 close 等。
* 外设I/O操作:外设包括键盘、显示器、串口、磁盘、时钟 ...,但接口均采用了文件 I/O 操作的通用系统调用接口。
.. note::
上述表述在某种程度上说明了操作系统对计算机硬件重要组成的抽象和虚拟化,是的应用程序只需基于对简单的抽象概念的访问来到达对计算机系统资源的使用:
上述表述在某种程度上说明了操作系统对计算机硬件重要组成的抽象和虚拟化,使得应用程序只需基于对简单的抽象概念的访问来到达对计算机系统资源的使用:
* 文件 (File) 是外设的一种抽象和虚拟化。特别对于存储外设而言,文件是持久存储的抽象。
* 地址空间 (Address Space) 是对内存的抽象和虚拟化。
......
此差异已折叠。
......@@ -6,10 +6,7 @@
:maxdepth: 5
基于操作系统的四个抽象,我们可以看出,从总体上看,操作系统具有五个方面的特征:虚拟化 (Virtualization)、
并发性 (Concurrency)、异步性、共享性和持久性 (Persistency)。操作系统的虚拟化可以理解为它对内存、CPU 的抽象和处理;并发性和共享性可以理解为操作系统支持多个应用程序“同时”运行;
异步性可以从操作系统调度、中断处理对应用程序执行造成的影响等几个方面来理解;
持久性则可以从操作系统中的文件系统支持把数据方便地从磁盘等存储介质上存入和取出来理解。
基于操作系统的四个抽象,我们可以看出,从总体上看,操作系统具有五个方面的特征:虚拟化 (Virtualization)、并发性 (Concurrency)、异步性、共享性和持久性 (Persistency)。操作系统的虚拟化可以理解为它对内存、CPU 的抽象和处理;并发性和共享性可以理解为操作系统支持多个应用程序“同时”运行;异步性可以从操作系统调度、中断处理对应用程序执行造成的影响等几个方面来理解;持久性则可以从操作系统中的文件系统支持把数据方便地从磁盘等存储介质上存入和取出来理解。
虚拟性
----------------------------------
......@@ -18,28 +15,22 @@
内存虚拟化
~~~~~~~~~~~~~~
首先来看看内存的虚拟化。
程序员在写应用程序的时候,不用考虑其程序的起始内存地址要放到计算机内存的具体某个位置,
而是用字符串符号定义了各种变量和函数,直接在代码中便捷地使用这些符号就行了。
这是由于操作系统建立了一个 **地址固定** , **空间巨大** 的虚拟内存给应用程序来运行,这是 **空间虚拟化** 。
这里的每个符号在运行时是要对应到具体的内存地址的。这些内存地址的具体数值是什么?程序员不用关心。为什么?
因为编译器会自动帮我们把这些符号翻译成地址,形成可执行程序。程序使用的内存是否占得太大了?
在一般情况下,程序员也不用关心。
首先来看看内存的虚拟化。程序员在写应用程序的时候,不用考虑其程序的起始内存地址要放到计算机内存的具体某个位置,而是用字符串符号定义了各种变量和函数,直接在代码中便捷地使用这些符号就行了。这是由于操作系统建立了一个 *地址固定* , *空间巨大* 的虚拟内存给应用程序来运行,这是 **空间虚拟化** 。这里的每个符号在运行时是要对应到具体的内存地址的。这些内存地址的具体数值是什么?程序员不用关心。为什么?因为编译器会自动帮我们把这些符号翻译成地址,形成可执行程序。程序使用的内存是否占得太大了?在一般情况下,程序员也不用关心。
.. note::
还记得虚拟地址(逻辑地址)的描述吗?
但编译器 (Compiler,比如 gcc) 和链接器 (linker,比如 ld) 也不知道程序每个符号对应的地址应该放在未来程序运行时的哪个物理内存地址中。所以,编译器的一个简单处理办法就是,设定一个固定地址(比如 0x10000)作为起始地址开始存放代码,代码之后是数据,所有变量和函数的符号都在这个起始地址之后的某个固定偏移位置。假定程序每次运行都是位于一个不会变化的起始地址。
这里的变量指的是全局变量,其地址在编译链接后会确定不变。但局部变量是放在堆栈中的,会随着堆栈大小的动态变化而变化。
这里编译器产生的地址就是虚拟地址。
实际上,编译器 (Compiler,比如 gcc) 和链接器 (linker,比如 ld) 也不知道程序每个符号对应的地址应该放在未来程序运行时的哪个物理内存地址中。所以,编译器的一个简单处理办法就是,设定一个固定地址(比如 0x10000)作为起始地址开始存放代码,代码之后是数据,所有变量和函数的符号都在这个起始地址之后的某个固定偏移位置。假定程序每次运行都是位于一个不会变化的起始地址。这里的变量指的是全局变量,其地址在编译链接后会确定不变。但局部变量是放在堆栈中的,会随着堆栈大小的动态变化而变化。这里编译器产生的地址就是虚拟地址。
这里,编译器和链接器图省事,找了一个适合它们的解决办法。当程序要运行的时候,这个符号到机器物理内存的映射必须要解决了,这自然就推到了操作系统身上。操作系统会把编译器和链接器生成的执行代码和数据放到物理内存中的空闲区域中,并建立虚拟地址到物理地址的映射关系。由于物理内存中的空闲区域是动态变化的,这也导致虚拟地址到物理地址的映射关系是动态变化的,需要操作系统来维护好可变的映射关系,确保编译器“固定起始地址”的假设成立。只有操作系统维护好了这个映射关系,才能让程序员只需写一些易于人理解的字符串符号来代表一个内存空间地址,且编译器只需确定一个固定地址作为程序的起始地址就可以不用考虑将来这个程序要在哪里运行的问题,从而实现了 **空间虚拟化** 。
应用程序在运行时不用考虑当前物理内存是否够用。如果应用程序需要一定空间的内存,但由于在某些情况下,物理内存的空闲空间可能不多了,这时操作系统通过把物理内存中最近没使用的空间(不是空闲的,只是最近用得少)换出(就是“挪地”)到硬盘上暂时缓存起来,这样空闲空间就大了,就可以满足应用程序的运行时内存需求了,从而实现了 **空间大小虚拟化** 。
CPU虚拟化
CPU 虚拟化
~~~~~~~~~~~~~~
再来看CPU虚拟化。不同的应用程序可以在内存中并发运行,相同的应用程序也可有多个拷贝在内存中并发运行。而每个程序都“认为”自己完全独占了CPU在运行,这是”时间虚拟化“。这其实也是操作系统给了运行的应用程序一个虚拟幻象。其实是操作系统把时间分成小段,每个应用程序占用其中一小段时间片运行,用完这一时间片后,操作系统会切换到另外一个应用程序,让它运行。由于时间片很短,操作系统的切换开销也很小,人眼基本上是看不出的,反而感觉到多个程序各自在独立”并行“执行,从而实现了 **时间虚拟化** 。
再来看 CPU 虚拟化。不同的应用程序可以在内存中并发运行,相同的应用程序也可有多个拷贝在内存中并发运行。而每个程序都“认为”自己完全独占了 CPU 在运行,这是”时间虚拟化“。这其实也是操作系统给了运行的应用程序一个幻象。其实是操作系统把时间分成小段,每个应用程序占用其中一小段时间片运行,用完这一时间片后,操作系统会切换到另外一个应用程序,让它运行。由于时间片很短,操作系统的切换开销也很小,人眼基本上是看不出的,反而感觉到多个程序各自在独立”并行“执行,从而实现了 **时间虚拟化** 。
.. note::
并行 (Parallel) 是指两个或者多个事件在同一时刻发生;而并发 (Concurrent) 是指两个或多个事件在同一时间间隔内发生。
......@@ -49,7 +40,7 @@ CPU虚拟化
并发性
----------------------------------
操作系统为了能够让 CPU 充分地忙起来并充分利用各种资源,就需要给很多任务给它去完成。这些任务是分时完成的,由操作系统来完成各个应用在运行时的任务切换。并发性虽然能有效改善系统资源的利用率,但并发性也带来了对共享资源的争夺问题,即同步互斥问题;执行时间的不确定性问题,即并发程序在执行中是走走停停,断续推进的。并发性对操作系统的设计也带来了很多挑战,一不小心就会出现程序执行结果不确定,程序死锁等很难调试和重现的问题。
操作系统为了能够让 CPU 充分地忙起来并充分利用各种资源,就需要给很多任务给它去完成。这些任务是分时完成的,由操作系统来完成各个应用在运行时的任务切换。并发性虽然能有效改善系统资源的利用率,但也带来了对共享资源的争夺问题,即同步互斥问题;执行时间的不确定性问题,即并发程序在执行中是走走停停,断续推进的。并发性对操作系统的设计也带来了很多挑战,一不小心就会出现程序执行结果不确定,程序死锁等很难调试和重现的问题。
异步性
----------------------------------
......@@ -67,6 +58,7 @@ CPU虚拟化
操作系统提供了文件系统来从可持久保存的存储介质(磁盘, SSD 等,以后以硬盘来代表)中取数据和代码到内存中,并可以把内存中的数据写回到硬盘上。硬盘在这里是外设,具有持久性,以文件系统的形式呈现给应用程序。
.. note::
文件系统也可看成是操作系统对存储外设(如硬盘、SSD 等)的虚拟化。
这种持久性的特征进一步带来了共享属性,即在文件系统中的文件可以被多个运行的程序所访问,从而给应用程序之间实现数据共享提供了方便。即使掉电,存储外设上的数据还不会丢失,可以在下一次机器加电后提供给运行的程序使用。持久性对操作系统的执行效率提出了挑战,如何让数据在高速的内存和慢速的硬盘间高效流动是需要操作系统考虑的问题。
......
source/chapter0/interrupt.png

51.0 KB | W: | H:

source/chapter0/interrupt.png

249.9 KB | W: | H:

source/chapter0/interrupt.png
source/chapter0/interrupt.png
source/chapter0/interrupt.png
source/chapter0/interrupt.png
  • 2-up
  • Swipe
  • Onion skin
......@@ -236,7 +236,7 @@ Rust 的 core 库内建了以一系列帮助实现显示字符的基本 Trait
整体工作完成!当然,我们实现的很简陋,用户态执行环境和应用程序都放在一个文件里面,以后会通过我们学习的软件工程的知识,进行软件重构,让代码更清晰和模块化。
现在,我们编译执行一下。
现在,我们编译并执行一下,可以看到正确的字符串输出,且程序也能正确结束!
.. code-block:: console
......@@ -245,38 +245,53 @@ Rust 的 core 库内建了以一系列帮助实现显示字符的基本 Trait
Compiling os v0.1.0 (/media/chyyuu/ca8c7ba6-51b7-41fc-8430-e29e31e5328f/thecode/rust/os_kernel_lab/os)
Finished dev [unoptimized + debuginfo] target(s) in 0.61s
$ qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os
段错误 (核心已转储)
$ qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os; echo $?
Hello, world!
9
.. 下面出错的情况是会在采用 linker.ld,加入了 .cargo/config
.. 的内容后会出错:
.. .. [build]
.. .. target = "riscv64gc-unknown-none-elf"
.. .. [target.riscv64gc-unknown-none-elf]
.. .. rustflags = [
.. .. "-Clink-arg=-Tsrc/linker.ld", "-Cforce-frame-pointers=yes"
.. .. ]
系统崩溃了!借助以往的操作系统内核编程经验和与下一节调试kernel的成果经验,我们直接定位为是 **栈** (Stack) 没有设置的问题。我们需要添加建立栈的代码逻辑。
.. 重新定义了栈和地址空间布局后才会出错
.. code-block:: asm
.. 段错误 (核心已转储)
# entry.asm
.. 系统崩溃了!借助以往的操作系统内核编程经验和与下一节调试kernel的成果经验,我们直接定位为是 **栈** (Stack) 没有设置的问题。我们需要添加建立栈的代码逻辑。
.. .. code-block:: asm
.. # entry.asm
.section .text.entry
.globl _start
_start:
la sp, boot_stack_top
call rust_main
.. .section .text.entry
.. .globl _start
.. _start:
.. la sp, boot_stack_top
.. call rust_main
.section .bss.stack
.globl boot_stack
boot_stack:
.space 4096 * 16
.globl boot_stack_top
boot_stack_top:
.. .section .bss.stack
.. .globl boot_stack
.. boot_stack:
.. .space 4096 * 16
.. .globl boot_stack_top
.. boot_stack_top:
然后把汇编代码嵌入到 ``main.rs`` 中,并进行微调。
.. 然后把汇编代码嵌入到 ``main.rs`` 中,并进行微调。
.. code-block:: rust
.. .. code-block:: rust
#![feature(global_asm)]
.. #![feature(global_asm)]
global_asm!(include_str!("entry.asm"));
.. global_asm!(include_str!("entry.asm"));
#[no_mangle]
#[link_section=".text.entry"]
extern "C" fn rust_main() {
.. #[no_mangle]
.. #[link_section=".text.entry"]
.. extern "C" fn rust_main() {
再次编译执行,可以看到正确的字符串输出,且程序也能正确结束!
.. 再次编译执行,可以看到正确的字符串输出,且程序也能正确结束!
......@@ -280,7 +280,7 @@ Rust 中的 ``llvm_asm!`` 宏的完整格式如下:
.. code-block:: console
$ git clone -b v4-illegal-priv-code-csr-in-u-mode-app git@github.com:chyyuu/os_kernel_lab.git
$ git clone -b v4-illegal-priv-code-csr-in-u-mode-app https://github.com/chyyuu/os_kernel_lab.git
$ cd os_kernel_lab/user
$ make build
......
......@@ -35,6 +35,7 @@ challenge: 支持多核,实现多个核运行用户程序。
.. code-block::
├── os(内核实现)
│   ├── build.rs (在这里实现用户程序的打包)
│   ├── Cargo.toml(配置文件)
│   ├── Makefile (要求 make run 可以正确执行,尽量不输出调试信息)
│ ├── build.rs (在这里实现用户程序的打包)
......@@ -42,7 +43,7 @@ challenge: 支持多核,实现多个核运行用户程序。
│   ├── main.rs(内核主函数)
│   ├── ...
├── reports
│   ├── lab1.md/pdf
│   ├── lab2.md/pdf
│   └── ...
├── README.md(其他必要的说明)
├── ...
......
......@@ -38,6 +38,16 @@ tips: 可以使用优先级队列比较方便的实现 stride 算法。如使用
- 实现 stride 调度算法,实现 sys_gettime, sys_set_priority 两个系统调用并通过 `Rust测例 <https://github.com/DeathWish5/rCore_tutorial_tests>`_ 中 chapter3 对应的所有测例,测例详情见对应仓库,系统调用具体要求参考 `guide.md <https://github.com/DeathWish5/rCore_tutorial_tests/blob/master/guide.md>`_ 。
.. _gettime-semantic-diff:
.. note::
**sys_gettime 在测例和教程正文中语义的不同**
为了更加贴近 POSIX 标准系统调用接口,在测例中 ``sys_gettime`` 需要将当前时间保存在一个 ``TimeVal`` 中,但是在用户库 ``user_lib`` 中的 ``get_time`` 函数仍然是以毫秒为单位,它的实现方式是将 ``TimeVal`` 中的秒数 ``sec`` 和微秒数 ``usec`` 转化为合计的毫秒数。因此,如果基于实验框架来做的话, ``sys_gettime`` 在内核中的实现需要发生变化。
另外需要注意的是,在修改之后, ``sys_gettime`` 和 POSIX 标准接口也仅仅做到了格式相同。在 POSIX 标准接口中 ``sys_gettime`` 统计当前相对 1970-01-01 00:00:00 +0000 (UTC) 过去的时间,而我们并没有用到任何 RTC 外设,只能做到统计自开机之后过去的时间。
需要说明的是 lab3 有3类测例,``ch3_0_*`` 用来检查基本 syscall 的实现,``ch3_1_*`` 基于 yield 来检测基本的调度,``ch3_2_*`` 基于时钟中断来测试 stride 调度算法实现的正确性。测试时可以分别测试 3 组测例,使得输出更加可控、更加清晰。
特别的,我们有一个死循环测例 ``ch3t_deadloop`` 用来保证大家真的实现了时钟中断。这一章中我们人为限制一个程序执行的最大时间(必须很大),超过就杀死,这样,我们的程序更不容易被恶意程序伤害。这一规定可以在实验4开始删除,仅仅为通过 lab3 测例设置。
......
......@@ -81,7 +81,7 @@
增加硬件加速虚实地址转换
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
开始回顾一下 ** 计算机组成原理** 课。如上图所示,当应用取指或者执行
开始回顾一下 **计算机组成原理** 课。如上图所示,当应用取指或者执行
一条访存指令的时候,它都是在以虚拟地址为索引读写自己的地址空间。此时,CPU 中的 **内存管理单元**
(MMU, Memory Management Unit) 自动将这个虚拟地址进行 **地址转换** (Address Translation) 变为一个物理地址,
也就是物理内存上这个应用的数据真实被存放的位置。也就是说,在 MMU 的帮助下,应用对自己地址空间的读写才能被实际转化为
......
......@@ -30,7 +30,7 @@ mmap 系统调用新定义:
- 错误:
- [addr, addr + len) 存在已经被映射的页。
- 物理内存不足。
- port & ~0x7 != 0 (port 其余位必须为0)。
- port & !0x7 != 0 (port 其余位必须为0)。
- port & 0x7 = 0 (这样的内存无意义)。
munmap 系统调用新定义:
......
......@@ -13,15 +13,27 @@
- 第三章《多道程序与分时多任务》中,出于一些对于总体性能或者交互性的要求,从 CPU 的角度看,它在执行一个应用一段时间之后,会暂停这个应用并切换出去,等到之后切换回来的时候再继续执行。其核心机制就是 **任务切换** 。对于每个应用来说,它会认为自己始终独占一个 CPU ,不过这只是内核对 CPU 资源的恰当抽象给它带来的一种幻象。
- 第四章《地址空间》中,我们利用一种经典的抽象—— **地址空间** 来代替先前对于物理内存的直接访问。这样做使得每个应用独占一个访存空间并与其他应用隔离起来,并由内核和硬件机制保证不同应用的数据被实际存放在物理内存上的位置也不相交。于是开发者在开发应用的时候无需顾及其他应用,整个系统的安全性也得到了一定保证。
事实上,由于我们还没有充分发掘这些抽象的能力,应用的开发和使用仍然比较受限这尤其体现在交互能力上。目前为止,所有的应用都是在内核初始化阶段被一并加载到内存中的,之后也无法对应用进行动态增删,从用户的角度来看这和第二章的批处理系统似乎并没有什么不同。
事实上,由于我们还没有充分发掘这些抽象的能力,应用的开发和使用仍然比较受限,且用户在应用运行过程中的灵活性和交互性不够强,这尤其体现在交互能力上。目前为止,所有的应用都是在内核初始化阶段被一并加载到内存中的,之后也无法对应用进行动态增删,从用户的角度来看这和第二章的批处理系统似乎并没有什么不同。
.. _term-terminal:
.. _term-command-line:
于是,本章我们会开发一个用户 **终端** (Terminal) 或称 **命令行** (Command Line) 作为用户界面,它就和我们今天常用的 OS 中的没有什么不同:只需在其中输入命令即可启动或杀死应用,或者监控系统的运行状况。这自然是现代 OS 中不可缺少的一部分,大大增加了系统的可交互性
于是,本章我们会开发一个用户 **终端** (Terminal) 或称 **命令行** 应用(Command Line Application, 俗称 **Shell** ) ,形成用户与操作系统进行交互的命令行界面(Command Line Interface),它就和我们今天常用的 OS 中的命令行应用(如 Linux中的bash,Windows中的CMD等)没有什么不同:只需在其中输入命令即可启动或杀死应用,或者监控系统的运行状况。这自然是现代 OS 中不可缺少的一部分,并大大增加了系统的 **可交互性** ,使得用户可以更加灵活地控制系统
为了方便开发,我们需要在已有抽象的基础上引入一个新的抽象:进程,还需要实现若干基于进程的功能强大的系统调用。
.. note::
**任务和进程的关系与区别**
第三章提到的 **任务** 和这里提到的 **进程** 有何关系和区别? 这需要从二者对资源的占用和执行的过程这两个方面来进行分析。
任务和进程都是一个程序的执行过程,或表示了一个运行的程序;都是能够被操作系统打断并通过切换来分时占用CPU资源;都需要 **地址空间** 来放置代码和数据;都有从开始运行到结束运行这样的生命周期。
第三章提到的 **任务** 是这里提到的 **进程** 的初级阶段,还没进化到拥有更强大的动态变化的功能:进程可以在运行的过程中,创建 **子进程** 、 用新的 **程序** 内容覆盖已有的 **程序** 内容、可管理更多的 物理或虚拟的 **资源** 。
实践体验
-------------------------------------------
......
......@@ -50,7 +50,6 @@ challenge: 支持多核。
(2) 其实使用了题(1)的策略之后,fork + exec 所带来的无效资源的问题已经基本被解决了,但是今年来 fork 还是在被不断的批判,那么到底是什么正在"杀死"fork?可以参考 `论文 <https://www.microsoft.com/en-us/research/uploads/prod/2019/04/fork-hotos19.pdf>`_ ,**注意**:回答无明显错误就给满分,出这题只是想引发大家的思考,完全不要求看论文,球球了,别卷了。
(3) fork 当年被设计并称道肯定是有其好处的。请使用 **带初始参数** 的 spawn 重写如下 fork 程序,然后描述 fork 有那些好处。注意:使用"伪代码"传达意思即可,spawn 接口可以自定义。可以写多个文件。
.. code-block:: rust
fn main() {
......@@ -64,7 +63,7 @@ challenge: 支持多核。
0
}
4. 描述进程执行的几种状态,以及 fork/exec/wait/exit 对状态的影响。
4. 描述进程执行的几种状态,以及 fork/exec/wait/exit 对状态的影响。
报告要求
------------------------------------------------------------
......
......@@ -76,7 +76,7 @@ challenge: 支持多核。
(2) 假设我们的邮箱现在有了更加强大的功能,容量大幅增加而且记录邮件来源,可以实现“回信”。考虑一个多核场景,有 m 个核为消费者,n 个为生产者,消费者通过邮箱向生产者提出订单,生产者通过邮箱回信给出产品。
- 假设你的邮箱实现没有使用锁等机制进行保护,在多核情景下可能会发生些问题?单核一定不会发生问题吗?为什么?
- 假设你的邮箱实现没有使用锁等机制进行保护,在多核情景下可能会发生些问题?单核一定不会发生问题吗?为什么?
- 请结合你在课堂上学到的内容,描述读者写者问题的经典解决方案,必要时提供伪代码。
- 由于读写是基于报文的,不是随机读写,你有什么点子来优化邮箱的实现吗?
......
......@@ -129,7 +129,7 @@
│   │   │   └── virtio_blk.rs(Qemu 平台的 virtio-blk 块设备)
│   │   └── mod.rs
│   ├── entry.asm
│   ├── fs(修改:在文件系统中新增普通文件的支持)
│   ├── fs(修改:在文件系统中新增标准文件的支持)
│   │   ├── inode.rs(新增:将 easy-fs 提供的 Inode 抽象封装为内核看到的 OSInode
│   │   │ 并实现 fs 子模块的 File Trait)
│   │   ├── mod.rs
......
......@@ -113,6 +113,8 @@ Blocks 给出 ``os`` 目录也占用 8 个块进行存储。实际上目录也
在一个复杂的计算机系统中,可以同时包含多个持久存储设备,它们上面的数据可能是以不同文件系统格式存储的。为了能够对它们进行统一管理,在内核中有一层 **虚拟文件系统** (VFS, Virtual File System) ,它规定了逻辑上目录树结构的通用格式及相关操作的抽象接口,只要不同的底层文件系统均实现虚拟文件系统要求的那些抽象接口,再加上 **挂载** (Mount) 等方式,这些持久存储设备上的不同文件系统便可以用一个统一的逻辑目录树结构一并进行管理。
.. _fs-simplification:
简易文件与目录抽象
-------------------------------------------------
......@@ -137,7 +139,7 @@ Blocks 给出 ``os`` 目录也占用 8 个块进行存储。实际上目录也
/// 功能:打开一个标准文件,并返回可以访问它的文件描述符。
/// 参数:path 描述要打开的文件的文件名(简单起见,文件系统不需要支持目录,所有的文件都放在根目录 / 下),
/// flags 描述打开文件的标志,具体含义下面给出。
/// 返回值:如果出现了错误则返回 -1,打开标准文件的文件描述符。可能的错误原因是:文件不存在。
/// 返回值:如果出现了错误则返回 -1,否则返回打开标准文件的文件描述符。可能的错误原因是:文件不存在。
/// syscall ID:56
pub fn sys_open(path: *const u8, flags: u32) -> isize;
......
......@@ -9,7 +9,7 @@ lab7 实验要求
硬链接
++++++++++++++++++++++++++++++++++++++++++++++++++
你的电脑桌面是咋样的?是放满了图标吗?反正我的 windows 是这样的。显然很少人会真的可执行文件放到桌面上,桌面图标其实都是一些快捷方式。或者用 unix 的术语来说:软链接。为了减少工作量,我们今天来实现软链接的兄弟:[硬链接](https://en.wikipedia.org/wiki/Hard_link)。
你的电脑桌面是咋样的?是放满了图标吗?反正我的 windows 是这样的。显然很少人会真的可执行文件放到桌面上,桌面图标其实都是一些快捷方式。或者用 unix 的术语来说:软链接。为了减少工作量,我们今天来实现软链接的兄弟:[硬链接](https://en.wikipedia.org/wiki/Hard_link)。
硬链接要求两个不同的目录项指向同一个文件,在我们的文件系统中也就是两个不同名称目录项指向同一个磁盘块。本节要求实现三个系统调用 sys_linkat、sys_unlinkat、sys_stat。
......
第七章:数据持久化存储(施工)
第七章:数据持久化存储
==============================================
.. toctree::
......
......@@ -53,10 +53,9 @@ rCore-Tutorial-Book 第三版
:doc:`/log`
项目/文档于 2021-03-03 最后一次更新,情况如下:
项目/文档于 2021-03-09 最后一次更新,情况如下:
- 更新了第四章练习题。
- 为方便调试,提供了 riscv64 gcc 工具链的下载链接。
- 将所有分支的 RustSBI 版本更新为 [81d53d8] 的 0.2.0-alpha.1 ,主要是在 Qemu 平台上支持非法指令的转发,目前可以正确处理带有非法指令的应用程序了。参考 ch2 分支上的测例 ``00hello_world.rs`` 。
项目简介
......@@ -94,4 +93,5 @@ rCore-Tutorial-Book 第三版
- 2021-01-18:加入第零章。
- 2021-01-30:第四章完成。
- 2021-02-16:第五章完成。
- 2021-02-20:第六章完成。
\ No newline at end of file
- 2021-02-20:第六章完成。
- 2021-03-06:第七章完成。到这里为止第一版初稿就已经完成了。
\ No newline at end of file
更新日志
===============================
2021-03-09
-------------------------------
- 将所有分支的 RustSBI 版本更新为 [81d53d8] 的 0.2.0-alpha.1 ,主要是在 Qemu 平台上支持非法指令的转发,目前可以正确处理带有非法指令的应用程序了。参考 ch2 分支上的测例 ``00hello_world.rs`` 。
2021-03-07
-------------------------------
- 在各章分支的链接脚本中加入了 ``.srodata/.sbss/.sdata`` 。
2021-03-06
-------------------------------
- 文档第一版初稿(全七章)完成!
- 修复了框架中基于 Qemu 平台运行却仍需要下载 kflash.py 工具的问题。
2021-03-05
-------------------------------
- 第三章练习中增加了对于 ``sys_gettime`` 语义在教程和测例中差异的相关说明, :ref:`详情 <gettime-semantic-diff>` 。
- 修正了第四章练习中 mmap 系统调用语义中的一处错误。
2021-03-03
-------------------------------
- 更新了第四章练习题。
- 为方便调试,提供了 riscv64 gcc 工具链的下载链接。
- 将文档渲染改为宽屏模式。
2021-02-28
-------------------------------
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册