写这篇的目的:跟着这几篇文章,了解CPU的工作原理。
Chapter 0: Intro
执行程序时,计算机在做什么?
程序是直接运行在CPU上吗?
系统调用(syscalls)指令是如何工作的?它们究竟是什么?
多个程序如何同时执行?
Chapter 1: The "Basics"
How Computers Are Architected
CPU负责所有计算。
第一个批量生产的CPU是Intel4004。
RAM是计算机的主内存库。CPU从RAM中读取机器码。CPU存储指令指针,以便记忆读取到内存中的位置。指令指针被放在寄存器中。
Processors Are Naive
CPU将机器码文件加载到RAM中,并指示CPU将指令指针调准到目标位置。CPU继续按照通常的取执周期进行。
CPU只能见到当前的指令指针和一些内部状态。进程完全是操作系统级别的抽象概念,CPU本身并不直接理解or追踪进程。
一些疑问:
- 如果CPU不知道多处理,只能顺序执行,那多个程序同时执行是如何做到的?
- 如果程序直接运行在CPU上,CPU又可以直接访问RAM,为什么代码不能访问其他进程的内存?
- 什么机制阻止了你的进程运行任何指令并对你的计算机做任何事情?
大多数内存访问实际上都经过一层重定向,重新映射整个地址空间。
Two Rings to Rule Them All
两种模式:内核模式和用户模式。
内核模式下,CPU可以执行任何支持的指令并访问任何内存。用户模式下,只允许执行指令的子集,I/O和内存访问是受限的,不可更改某些CPU设置。内核与驱动程序会运行在内核模式下,应用程序则运行在用户模式下。
What Even is a Syscall?
应用程序之所以在用户模式下运行,是因为计算机不能完全信任外来的应用程序,万一应用程序中出现恶意代码,会导致计算机运行异常。但是,用户程序也需要能够输入输出、访问内存并与操作系统进行交互。运行在用户模式下的应用程序,必须想操作系统内核寻求帮助。系统调用是一系列特殊过程,它允许程序从用户空间跳转到内核空间,从程序代码跳转到操作系统代码。
用户空间到内核空间的控制传输是通过一种成为软件中断的处理器特性来实现的:
- 启动过程中,操作系统会将一个称为中断向量表的表存储在RAM中,并将其注册到CPU。中断向量表将中断号映射到处理程序代码指针。
- 然后,用户空间程序可以使用类似INT这样的指令,告诉处理器在中断向量表中查找指定的中断号码,切换到内核模式,然后将指令指针跳转到存储在中断向量表中的内存地址。
系统调用是一种包装器 APIs。目前我们了解的系统调用的内容:
- 处于用户模式的应用程序不能直接访问 I/O 或内存。它们必须通过操作系统才能和外界进行交互。
- 程序可以通过特殊的机器指令(比如 INT 和 IRET)将控制权委托给操作系统。
- 程序无法直接切换特权级别;软件中断是安全的,因为处理器已经被操作系统预先配置好了,可以跳到操作系统代码的哪个位置。中断向量表(IVT)只能在内核模式下配置。
触发系统调用时,程序需要向操作系统传递数据;操作系统需要知道在执行系统调用本身需要的任何数据时,要执行哪个特定的系统调用,例如要打开哪个文件名。传递数据的机制因操作系统和体系结构而异,但通常是在触发中断之前将数据放入某些寄存器或堆栈中。
在不同操作系统中发起系统调用的差异意味着:程序员自己为每个程序都实现一套系统调用是非常不切实际的。这也意味着不能改变操作系统的中断处理机制,以免破坏运行在旧有系统上的程序。
因此操作系统们基于这些中断,抽象出一个可复用的抽象层。高级库函数中包含了需要的汇编指令,这在类 Unix 系统上由 libc 提供,Windows 系统上由 ntdll.dll 提供。调用这些函数不会切换到内核模式,这只是标准的函数调用。在库的内部,汇编代码会将控制权转移到内核模式,这种实现与平台关联程度甚于包装库子例程。
The Need for Speed / Let’s Get CISC-y
CISC——Complex instruction set computer 复杂指令集
Intel 和 AMD 在 x86-64 系统调用指令上并不兼容。
RISC——Reduced instruction set computer 精简指令集
精简指令集注重兼容性。AArch64 就是精简指令集的一种。苹果的 Silicon 芯片就是 AArch64 体系结构的。
第一章总结
- 处理器在一个获取执行的无限循环中,这里不存在任何操作系统或程序概念。处理器的模式通常存储在寄存器中,该模式决定了执行哪些指令。操作系统代码运行在内核模式,在运行程序的时候切换到用户模式。
- 执行二进制程序时,操作系统切换到用户模式,处理器的指针指向内存的接入点。因为程序只有用户模式,要想与外界交互就必须通过操作系统,而系统调用就是程序与外界交互的方式。
- 程序通过调用共享库函数唤起这些程序调用。这些包装机器码用于软件中断或特定于体系结构的系统调用指令,这些系统调用指令将控制权转移到操作系统内核和模式切换代码并返回程序码。
Chapter 2: Slice Dat Time
通过定时器芯片完成从程序代码到内核代码的切换。
Hardware Interrupts
软件中断用来控制用户程序访问操作系统的,而硬件中断是通过定时器芯片来实现的。
多任务处理是通过硬件中断来完成的:
- 在跳转到程序代码前,操作系统会设定定时器芯片以便一段时间后出发中断。
- 操作系统切换到用户模式,调转到待执行的程序指令。
- 预设时间到来时,它会触发硬件中断,使得操作系统转换到内核模式,执行系统代码。
- 操作系统能够记住离开程序时的所处位置,以便能够找到下次执行程序代码时的位置。
这被称为抢占式多任务处理。
Timeslice Calculation
时间片是操作系统调度程序在抢占进程之前允许其运行的持续时间。选择时间片的最简单方法是为每个进程分配相同的时间片,可能在 10ms 范围内,并按顺序循环执行任务。这被称为固定时间片轮询调度。
对于固定时间片调度的一种微小改进是选择一个延时目标——进程响应的理想最长时间。假设有合理数量的进程,目标延迟就是进程在被抢占后恢复执行所需的时间。
时间片的计算方法是将目标延迟时间除以任务总数;这种方法比固定时间片调度更好,因为它可以避免在进程较少的情况下进行浪费的任务切换。如果目标延迟为 15ms,有 10 个进程,则每个进程的运行时间为 1.5ms。如果只有 3 个进程,则每个进程都能获得更长的 5ms 时间片,同时还能达到目标延迟。
进程切换的计算成本很高,因为它需要保存当前程序的整个状态,然后再恢复另一个程序。过了某个点,过小的时间片会导致进程切换过快,从而产生性能问题。通常的做法是给时间片持续时间一个下限(最小粒度)。这就意味着,当有足够多的进程使最小粒度生效时,就会超出目标延迟。
作者写作这一系列文章时,Linux 的调度程序使用 6ms 的目标延迟和 0.75ms 的最小粒度。
Linux 使用的调度程序名为“完全公平调度程序(Completely Fair Scheduler,CFS)”。
每次操作系统抢占进程时,都需要加载新程序保存的执行上下文,包括其内存环境。这是通过告诉 CPU 使用不同的页表,即从“虚拟”地址到物理地址的映射来实现的。这种系统也能够防止程序互相访问对方内存。
Note #1: Kernel Preemptability
不仅用户模式下存在抢占是调度,内核模式也存在这种运行机制。
Note #2: A History Lesson
合作多任务是抢占式多任务的前身。
Chapter 3: How to Run a Program
主要讲述 x86-64 架构的 Linux。