Loading... # 执行环境 **执行环境** (Execution Environment) 是一个内涵很丰富且有一定变化的术语,它主要负责给在其上执行的软件提供相应的功能与资源,并可在计算机系统中形成多层次的执行环境。 对于现在直接运行在裸机硬件 (Bare-Metal) 上的操作系统,其执行环境是 *计算机的硬件* 。 在早期的计算机系统中,还没有操作系统,所以对于直接运行在裸机硬件上的应用程序而言,其执行环境也是 *计算机的硬件 *。 后来随着计算机技术的发展,应用程序下面形成了一层比较通用的函数库,这使得应用程序不需要直接访问硬件了,它所需要的功能(比如显示字符串)和资源(比如一块内存)都可以通过函数库的函数来帮助完成。在第二个阶段,应用程序的执行环境就变成了 函数库 -> 计算机硬件 ,而这时函数库的执行环境就是计算机的硬件。 ![][1] 再进一步,操作系统取代了函数库来访问硬件,函数库通过访问操作系统的系统调用服务来进一步给应用程序 提供丰富的功能和资源。在第三个阶段,应用程序的执行环境就变成了 函数库 -> 操作系统内核 -> 计算机硬件 。 在后面又出现了基于 Java 语言的应用程序,在函数库和操作系统之间,多了一层 Java 虚拟机,此时 Java 应用 程序的执行环境就变成了 函数库 -> Java 虚拟机 -> 操作系统内核 -> 计算机硬件 。 在云计算时代,在传统操作系统与 计算机硬件之间多了一层 Hypervisor/VMM ,此时应用程序的执行环境变成了 函数库 -> Java 虚拟机 -> 操作系统内核 -> Hypervisor/VMM -> 计算机硬件 。这里可以看到,随着软件需求的多样化和复杂化, **执行环境的层次** 也越来越多。 另外,CPU 在执行过程中,可以在不同层次的执行环境之间切换,这称为 **执行环境切换** 。执行环境切换主要是通过特定的 API 或 ABI 来完成的,这样不同执行环境的软件就能实现数据交换与互操作,而且还保证了彼此之间有清晰的隔离。 <div class="tip inlineBlock info"> 执行环境切换,比如说docker容器内的应用与物理机之间的交互,虚拟机与物理机之间的交互 </div> ![][2] 对于应用程序的执行环境而言,应用程序只能看到执行环境直接提供给它的接口(API 或 ABI),这使得应用程序所能得到的服务取决于执行环境提供给它的访问接口。所以,操作系统可以看成是应用程序执行环境,其形态可以是一个库,也可以是一个虚拟机等,或者它们的某种组合形式。 比如, 手机上的 Android操作系统 是Android应用程序的执行环境,它包括了库形态的Framework层,执行Java程序的虚拟机层,与操作系统交互的系统类C库和Linux kernel。 ## 执行环境基本定义 根据上面的介绍,可以得出对于应用程序的执行环境的一个基本的定义:执行环境是应用程序正确运行所需的服务与管理环境,用来完成应用程序在运行时的数据与资源管理、应用程序的生存期等方面的处理,它定义了应用程序有权访问的其他数据或资源,并决定了应用程序的行为限制范围。 --- # 普通控制流 各种应用程序在执行环境中执行其功能,而具体如何执行,取决于 **程序的控制流** 。程序的控制流 (Flow of Control or Control Flow) 是指以一个程序的指令、语句或基本块为单位的执行序列。而处理器的控制流是指处理器中程序计数器的控制转移序列。最简单的一种控制流(没有异常或中断产生的前提下)是一个“平滑的”序列,其中每个要执行的指令地址在内存中都是相邻的。如果站在程序员的角度来看控制流,会发现控制流是程序员编写的程序的执行序列,这些序列是程序员预设好的。程序运行时能以多种简单的控制流(顺序、分支、循环结构和多层嵌套函数调用)组合的方式,来一行一行的执行源代码(以编程语言级的视角),也是一条一条的执行汇编指令(以汇编语言级的视角)。对于上述的不同描述,我们可以统称其为 普通控制流 (CCF,Common Control Flow,简称 控制流) 。在应用程序视角下,它只能接触到它所在的执行环境,不会跳到其他执行环境,所以应用程序执行基本上是以普通控制流的形式完成整个运行的过程。 --- # 异常控制流 应用程序在执行过程中,如果发出系统调用请求,或出现外设中断、CPU 异常等情况,处理器执行的前一条指令和后一条指令将会位于两个完全不同的位置,即不同的执行环境 。比如,前一条指令还在应用程序的代码段中,后一条指令就跑到操作系统的代码段中去了,这就是一种控制流的“突变”,即控制流脱离了其所在的执行环境,并产生 执行环境的切换。 我们把这种“突变”的控制流称为 异常控制流 (ECF, Exceptional Control Flow) 。 应用程序 感知 不到这种异常的控制流情况,这主要是由于操作系统把这种情况 **透明** 地进行了执行环境的切换和对各种异常情况的处理,让应用程序从始至终地 **认为** 没有这些异常控制流的产生。 简单地说, 异常控制流 是处理器在执行过程中的突变,其主要作用是通过硬件和操作系统的协同工作来响应处理器状态中的特殊变化。比如当应用程序正在执行时,产生了时钟外设中断,导致操作系统打断当前应用程序的执行,转而进入 操作系统 执行环境去处理时钟外设中断。处理完毕后,再回到应用程序中被打断的地方继续执行。 <div class="tip inlineBlock info"> 对于中断,其实还有很多例子,比如说键盘输入,鼠标移动点击等中断操作 </div> <div class="tip inlineBlock share"> 在《深入理解计算机系统(CSAPP)》 中,对异常控制流也给出了相关定义: 系统必须能对系统状态的变化做出反应,这些系统状态不是被内部程序变量捕获,也不一定和程序的执行相关。现代系统通过使控制流发生突变对这些情况做出反应。我们称这种突变为异常控制流(Exceptional Control Flow, ECF) 这里的异常控制流不涉及编程语言级别的exception机制,所以不能混淆 </div> # 控制流上下文 站在硬件的角度来看普通控制流或异常控制流的具体执行过程,我们会发现从控制流起始的某条指令执行开始,指令可访问的所有物理资源的内容,包括自带的所有通用寄存器、特权级相关特殊寄存器、以及指令访问的内存等,会随着指令的执行而逐渐发生变化。 这里我们可以把控制流上下文理解成控制流在执行完某指令时的物理资源内容,即确保下一时刻能继续 正确 执行控制流指令的物理资源内容。或者可以把上下文理解成是一种状态,控制流上下文指的便是控制流所在执行环境的状态。 这里说的控制流的上下文是指仅会影响控制流正确执行的有限的物理/虚拟资源内容。我们需要理解程序中控制流的上下文对程序 正确 执行的影响。 如果在某时刻,由于某种有意或无意的原因,控制流的上下文发生了变化(比如某个寄存器的值变了),但并不是由于程序的控制流本身的指令导致的,这就会使得接下来的程序指令执行出现偏差,并最终导致执行过程或执行结果不符合预期,这种情形称为 **程序执行错误** 。 而操作系统有责任来保护应用程序中控制流的上下文,以让应用程序得以正确执行。 <div class="tip inlineBlock info"> - 物理资源:即计算机硬件资源,如CPU的寄存器、可访问的物理内存等。 - 虚拟资源:即操作系统提供的资源,如文件,网络端口号,网络地址,信号等。 </div> 如果一个控制流属于某个函数,那么这个控制流的上下文简称为函数调用上下文。 如果一个控制流属于某个应用程序,那么这个控制流的上下文简称为应用程序上下文。 如果把某 进程 看做是运行的应用程序,那么这个属于某个应用程序的控制流可简称为某进程上下文,这里要注意 **某个** 应用程序,并不是特定的应用程序,该控制流是进程级别的。 如果一个控制流属于操作系统,那么这个控制流的上下文简称为操作系统上下文。 如果一个控制流是属于操作系统中处理中断/异常/陷入的那段代码,那么这个控制流的上下文简称为中断/异常/陷入的上下文。 随着CPU的执行,各种前缀的上下文(执行环境的状态)会在不断的变化。如果出现了处理器在执行过程中的突变(即异常控制流)或转移(如多层函数调用),需要由维持执行环境的软硬件协同起来,保存发生突变或转移前的控制流上下文,即当前执行环境的状态(比如突变或函数调用前一刻的指令寄存器,栈寄存器和其他一些通用寄存器等内容),并在完成突变处理或被调用函数执行完毕后,恢复突变或转移前的控制流上下文。这是由于完成与突变相关的执行会破坏突变前的控制流上下文(比如上述各种寄存器的内容),导致如果不保存之前的控制流上下文,就无法恢复到突变前的正确的执行环境,继续正常的普通控制流的执行。 对于异常控制流的上下文保存与恢复,主要是通过 CPU 和操作系统(手动编写在栈上保存与恢复寄存器的指令)来协同完成;对于函数转移控制流的上下文保存与恢复,主要是通过编译器(自动生成在栈上保存与恢复寄存器的指令)来帮助完成的。 在操作系统中,需要处理三类异常控制流:外设中断 (Device Interrupt) 、陷入 (Trap) 和异常 (Exception,也称Fault Interrupt)。 # 异常控制流:中断 外设 中断 (Interrupt) 是由外部设备引起的外部 I/O 事件如时钟中断、控制台中断等。外设中断是异步产生的,与处理器的执行无关。产生中断后,操作系统需要进行中断处理来响应中断请求,这会破坏被打断前应用程序的控制流上下文,所以操作系统要保存与恢复被打断前应用程序的控制流上下文。 ![][3] # 异常处理流:异常 异常 (Exception) 是在处理器执行指令期间检测到不正常的或非法的内部事件(如除零错、地址访问越界)。产生异常后,操作系统需要进行异常处理,这会破坏被打断前应用程序的控制流上下文,所以操作系统要保存与恢复被打断前应用程序的控制流上下文。 <div class="tip inlineBlock info"> 如果是应用程序产生的不可恢复的致命错误导致的异常,操作系统有权直接终止该应用程序的执行。 </div> ![][4] # 异常控制流:陷入 陷入 (Trap) 是在程序中使用请求操作系统服务的系统调用而引发的有意事件。产生陷入后,操作系统需要执行系统调用服务来响应系统调用请求,这会破坏陷入前应用程序的控制流上下文,所以操作系统要保存与恢复陷入前应用程序的控制流上下文。 ![][5] <div class="tip inlineBlock share"> 通过查阅资料可以发现这几种异常控制流的定义在不同的资料有些许差别,有的书籍把中断、陷入和异常都统一为一种中断,表示程序的当前控制流被打断了,要去执行不属于这个控制流的另外一个没有程序逻辑先后关系的控制流;也有书籍把这三者统一为一种异常,表示相对于程序的正常控制流而言,出现了的一种没有程序逻辑先后关系的异常控制流。甚至也有书籍把这三者统一为一种陷入,表示相对于程序的正常控制流而言,CPU会陷入到操作系统内核中去执行。 在 RISC-V 的特权级规范文档中,异常指的是由于 CPU 当前指令执行而产生的异常控制流,中断指的是与 CPU 当前指令执行无关的异常控制流,中断和异常统称为陷入。当中断或异常触发时,我们首先进行统一的陷入处理流程,随即根据 `mcause/scause` 等寄存器的内容判定目前触发的是中断还是异常,再对应进行处理。在操作系统意义上的陷入,在 RISC-V 的语境下属于异常的一部分。另外,在 x86 架构下的“软件中断”(也即指令 `int 0x80` )可以理解为操作系统意义上的陷入,但在 RISC-V 语境下软件中断表示一种特殊的处理核间中断。 这些都是从不同的视角来阐释中断、陷入和异常,并没有一个唯一精确的解释。 </div> # 进程 站在应用程序自身的角度来看,进程 (Process) 的一个经典定义是一个正在运行的程序实例。在计算机系统中,我们可以“同时”运行多个程序,这个“同时”,其实是操作系统给用户造成的一个“幻觉”。在操作系统上运行一个程序时,应用程序会得到一个“幻觉”,就好像我们执行的一个程序是整个计算机系统中当前运行的唯一的程序,能够独占使用处理器、内存和外设,而且程序中的代码和数据好像是系统内存中唯一的对象。 ![][6] 站在计算机系统和操作系统的角度来看,就可以把“幻觉”去掉。其实,进程是应用程序的一次执行过程。并且在这个执行过程中,由“操作系统”执行环境来管理程序执行过程中的 进程上下文 – 一种控制流上下文。这里的进程上下文是指程序在运行中的各种物理/虚拟资源(寄存器、可访问的内存区域、打开的文件、信号等)的内容,特别是与程序执行相关的具体内容:内存中的代码和数据,栈、堆、当前执行的指令位置(程序计数器的内容)、当前执行时刻的各个通用寄存器中的值等。进程上下文如下图所示: ![][7] 处理器是计算机系统中的硬件资源。为了提高处理器的利用率,操作系统需要让处理器足够忙,即让不同的程序轮流占用处理器来运行。如果一个程序因某个事件而不能运行下去时,就通过进程上下文切换把处理器占用权转交给另一个可运行程序。进程上下文切换如下图所示: ![][8] <div class="tip inlineBlock success"> 因为处理器在不断地进行着进程上下文的切换,又或者说,处理器的占用权不断地在不同的程序之间交换,从而让用户产生了多个程序同时运行的“幻觉”。当然,多核处理器使得物理上同时运行多个程序成立,但是目前的处理器核数也无法支撑几百个进程在物理条件下执行的成立,且这样做也会造成处理器性能极大的浪费,所以实际上,还是依靠于进程上下文的轮换。 </div> 基于上文,我们可以给进程一个更加准确的定义:一个进程是一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程。操作系统中的进程管理需要采用某种调度策略将处理器资源分配给程序并在适当的时候回收,并且要尽可能充分利用处理器的硬件资源。 --- # 地址空间 **地址空间** (Address Space) 是对物理内存的虚拟化和抽象,也称虚存 (Virtual Memory)。它就是操作系统通过处理器中的内存管理单元 (MMU, Memory Management Unit) 硬件的支持而给应用程序和用户提供一个大的(可能超过计算机中的物理内存容量)、连续的(连续的地址空间编址)、私有的(其他应用程序无法破坏)的存储空间。这需要操作系统将内存和外存(即持久存储,硬盘是一种典型的外存)结合起来管理,为用户提供一个容量比实际内存大得多的虚拟存储器,并且需要操作系统为应用程序分配内存空间,使用户存放在内存中的程序和数据彼此隔离、互不侵扰。操作系统中的虚存管理与处理器的 MMU 密切相关,在启动虚存机制后,软件通过 CPU 访问的每个虚拟地址都需要通过 CPU 中的 MMU 转换为一个物理地址来进行访问。下面是虚拟的地址空间与物理内存和物理磁盘映射的图示: ![][9] <div class="tip inlineBlock share"> 这里其实就涉及到了页面内存的概念,因为程序一般内部存在着多个段,而这些段在虚拟内存上的映射是需要遵循页面对齐的,那么对于操作系统和物理内存来讲,为了避免物理内存的浪费,通常会把多个具有相同权限的段合并在一起,而在虚拟内存上进行多次页映射,从而达到节省内存的目的,然后这里就会涉及到进程装载等问题,比如关于多次映射同一个物理内存页后,如何判断可执行文件中的段在虚拟内存页中的位置 </div> # 文件 **文件** (File) 主要用于对持久存储的抽象,并进一步扩展到为外设的抽象。具体而言,文件可理解为存放在持久存储介质(比如硬盘、光盘、U盘等)上,方便应用程序和用户读写的数据。以磁盘为代表的持久存储介质的数据访问单位是一个扇区或一个块,而在内存中的数据访问单位是一个字节或一个字。这就需要操作系统通过文件来屏蔽磁盘与内存差异,尽量以内存的读写方式来处理持久存储的数据。当处理器需要访问文件中的数据时,可通过操作系统把它们装入内存。文件管理的任务是有效地支持文件的存储、 检索和修改等操作。 下面是文件对磁盘的抽象映射图示: ![][10] 从一个更高和更广泛的层次上看,各种外设虽然差异很大,但也有基本的读写操作,可以通过文件来进行统一的抽象,并在操作系统内部实现中来隐藏对外设的具体访问过程,从而让用户可以以统一的文件操作来访问各种外设。这样就可以把文件看成是对外设的一种统一抽象,应用程序通过基本的读写操作来完成对外设的访问。 [1]: https://www.onesnowwarrior.cn/usr/uploads/2022/03/884293320.png [2]: https://www.onesnowwarrior.cn/usr/uploads/2022/03/4159923212.png [3]: https://www.onesnowwarrior.cn/usr/uploads/2022/03/1811819356.png [4]: https://www.onesnowwarrior.cn/usr/uploads/2022/03/1677588245.png [5]: https://www.onesnowwarrior.cn/usr/uploads/2022/03/2809035012.png [6]: https://www.onesnowwarrior.cn/usr/uploads/2022/03/554696742.png [7]: https://www.onesnowwarrior.cn/usr/uploads/2022/03/389689266.png [8]: https://www.onesnowwarrior.cn/usr/uploads/2022/03/3829854864.png [9]: https://www.onesnowwarrior.cn/usr/uploads/2022/03/2740905503.png [10]: https://www.onesnowwarrior.cn/usr/uploads/2022/03/569896820.png Last modification:March 24, 2022 © Allow specification reprint Support Appreciate the author AliPayWeChat Like 1 如果觉得我的文章对你有用,请随意赞赏