xv6 附录A
我们可以把PC抽象为三部分:CPU,内存和I/O设备。
CPU:负责计算和执行指令
内存:用于保存计算用的指令和数据
I/O设备:外部世界,包含键盘,屏幕,硬盘和网卡等。CPU和内存自成一体,它们也需要与外界通信,OS负责充当中介,管理这些外部设备。
xv6 操作系统,就是一个运行在 CPU 上的、驻留在内存中的程序,它的工作就是管理好 CPU、内存,以及所有的 I/O 设备。
补一下CPU工作循环相关的硬件的概念:
程序计数器:一个特殊的寄存器,它的唯一的工作就是保存吓一跳要执行的指令的内存地址。
内存:存储着成千上万的机器指令和数据。
指令寄存器:CPU内部的一个临时存储区,用于存放当前正在被解码和执行的指令。
通用寄存器:CPU内部的极高速存储,它们是 CPU 进行计算时(如加法、减法)存放操作数和结果的地方。
总线:连接CPU和内存的导线。
地址总线:CPU用它来指定要访问的内存地址。
数据总线:用于在CPU和内存之间传输数据或指令。
控制总线:CPU用它来发送读或写的命令。
CPU(中央处理单元,或处理器)其实只是在执行一个非常简单的循环。假设程序计数器当前值为0x80100040,这意味着CPU准备好要执行存放在内存地址 0x80100040 处的指令。
1. 取指 (FETCH)
目标: 将位于 0x80100040 地址的“机器指令”从内存(RAM)取回到 CPU 内部。
CPU -> 地址总线: CPU 的控制单元将
0x80100040这个值(来自 PC)放置到地址总线上。CPU -> 控制总线: CPU 在控制总线上发出一个“内存读取”(Memory Read) 信号。
内存响应: 内存(RAM)检测到地址总线上的地址和控制总线上的“读取”信号。它从自己的
0x80100040存储单元中取出数据(这个数据就是“机器指令”,比如0x00508093)。内存 -> 数据总线: 内存将这个指令(
0x00508093)放置到数据总线上。CPU 接收: CPU 通过数据总线接收到这个指令,并将其存入 CPU 内部的指令寄存器 (IR) 中。
2. 程序计数器增加 (PC Increment)
目标: 让 PC 指向“下一条”指令,为下一次循环做准备。
几乎在取指的同时,CPU 内部的逻辑单元(不是主 ALU)会自动将 PC 的值增加一个指令的长度(例如,在 RISC-V 中是 4 字节)。
计算:
PC=0x80100040+4=0x80100044。关键: 此时,PC 已经指向了下一条指令 (
0x80100044),尽管当前指令 (0x80100040) 甚至还没有开始执行。这确保了循环的连续性。
3. 解码 (DECODE)
目标: 搞清楚 0x00508093 这条指令到底是要“干什么”。
指令寄存器 (IR) 中的
0x00508093被送入 CPU 的指令解码器(一个复杂的逻辑电路)。解码器根据指令集架构(ISA,如 x86 或 RISC-V)的规则,将这个 32 位数字拆分开。
例如 (RISC-V): 解码器会识别出:
操作码 (Opcode): 这是
addi指令(立即数加法)。源寄存器 (Source): 需要读取
a0寄存器。立即数 (Immediate): 需要使用数字
5。目标寄存器 (Destination): 结果要写入
a1寄存器。
激活电路: 解码器根据这些信息,向 CPU 的其他部分(如 ALU、寄存器堆)发送控制信号,告诉它们下一步该做什么(“准备好,我们要从
a0读,要和一个 5 相加,然后把结果写入a1”)。
4. 执行 (EXECUTE)
目标: 实际执行 addi a1, a0, 5 这个操作。
读寄存器: 控制单元命令通用寄存器堆,将
a0寄存器中的值发送到 ALU (算术逻辑单元) 的输入端 A。送立即数: 控制单元将解码出的立即数
5发送到 ALU 的输入端 B。ALU 计算: 控制单元命令 ALU 执行“加法”操作。ALU 计算出(
a0的值 + 5)的结果。写寄存器: 控制单元将 ALU 的计算结果通过内部总线,发送回通用寄存器堆,并命令将其写入到
a1寄存器中。
5. 不断反复 (REPEAT)
当“执行”步骤完成后,一个时钟周期(或多个周期)过去了。
CPU 的控制单元回到第 1 步 (FETCH)。
这一次,它查看 PC (程序计数器),发现 PC 的值已经是
0x80100044(在第 2 步中被更新了)。于是,CPU 开始从
0x80100044地址取下一条指令。循环往复。
寄存器,ALU(算术逻辑单元),CU(控制单元)是构成CPU(中央处理器)的三大核心部件:
| 部件 (Component) | 角色 (Role) | 主要功能 (Primary Function) |
| 寄存器 (Registers) | 临时存储 (Storage) | 保存数据:高速保存 CPU 正在处理的单个数据(如变量、地址、指令)。 |
| ALU (算术逻辑单元) | 执行者 (Execution) | 执行计算:执行所有数学运算(加、减)和逻辑运算(与、或、非、比较)。 |
| CU (控制单元) | 指挥官 (Control) | 指挥协调:解码指令,并生成控制信号,指挥寄存器和ALU在何时、做什么。 |
三者的协同工作流程
我们以一条简单的 RISC-V 指令 add a1, a0, s0 (意思是:a1 = a0 + s0) 为例,看看它们三者是如何在时钟驱动下协同工作的:
CU (控制单元) - 取指与解码
CU首先执行“取指”操作,从内存(通过程序计数器pc寄存器)获取add a1, a0, s0这条机器指令。CU的“解码器”部分识别出:这是一条 R-Type(寄存器-寄存器)加法指令。CU立即知道它需要指挥以下动作:从
a0读。从
s0读。命令
ALU做加法。往
a1写。
CU (指挥) -> 寄存器 (读)
CU向寄存器堆发送控制信号,命令它:“在下一个时钟周期,允许读取a0和s0寄存器的值”。CU同时配置 ALU:“准备执行‘加法’操作”。
寄存器 -> ALU (执行)
a0的值被送到 ALU 的输入端 A。s0的值被送到 ALU 的输入端 B。ALU(在
CU的“加法”命令下)执行 A + B 的计算,得到结果。
CU (指挥) -> ALU -> 寄存器 (写)
ALU将计算结果放在它的输出端。CU向寄存器堆发送控制信号:“在下一个时钟周期,允许写入a1寄存器”。ALU的输出结果(a0 + s0的值)被送入并存放在a1寄存器中。
然后讨论一下时钟周期的事情吧,其实在网卡那里已经接触过这个概念了,在PC内部,CPU,内存,总线等都连接到同一个系统时钟上,和网卡那里解决的时钟不同步的问题不同,它们共享同一个时钟。当时钟的滴答声响起时,所有时序逻辑的部件(主要是寄存器)会同时采样或锁存它们输入端的数据。在两次滴答声之间,信号正物理地从一个部件传输到另一个部件,比如,ALU这是就忙于计算,它的输出正在从上一次的计算结果变化为这一次的计算结果,到下一次滴答声之前,它可以稳定电压并输出,届时这个电压又会被寄存器的输入端读取。时钟周期的长度,由最慢的哪个单周期工作决定(也有多周期的指令,但是我不管了)。工程师会精确计算出关键路径,然后把时钟周期设定为略大于这个值。
然后探讨速度和成本的事情(你可能在很多地方看到过,我反正是):数据存放在哪里?
第1层 (塔尖): 寄存器 (Registers)
寄存器在CPU内部,速度极快(CPU访问寄存器的时间短到可以在一个时钟周期内完成,它和CPU的速度相匹配),但极小(只有几十个)。
第2层 (中间): 缓存 (Cache)
由于寄存器和主存在读写速度和大小上的巨大差异,大多数处理器,包括 x86,都在芯片上的缓存中保存了最近使用的主存数据,缓存在CPU芯片上,但在核心之外。缓存是主存和寄存器在速度和大小上的折衷。通常 x86 对操作系统隐藏了缓存,所以我们只需要考虑寄存器和主存两种存储器,不用担心主存的层次结构引发的差异(即缓存由硬件全自动管理,操作系统不需要也不能用软件代码区直接控制它)。
第3层 (塔底): 主存 (RAM)
慢,便宜。CPU和主存之间是最特殊,最快的连接,它不是IO总线,而是通过内存总线(超高速直连通道,当然,高速只是相对于计算核心与其他外部设备通过IO总线连接)
我们已经知道,在取指的过程中,CPU先读取寄存器中的内存地址,然后把地址放到地址总线上,接着在控制总线上发出“内存读取”的指令,然后内存检测到地址总线上的地址和控制总线上的指令,输出数据。可以看到,地址是CPU用来指定其操作位置的唯一机制。
与此同时,CPU需要读写两个地方,主存和I/O设备,当CPU发出一个请求到地址比如0x3F8时,硬件系统需要知道这个请求是发给主存还是发给I/O设备。为解决这个问题有两个方案。
方案一端口映射:
概念: 硬件架构(CPU 和芯片组)定义了两个各自独立的地址空间 (Address Spaces)。
内存地址空间 (Memory Address Space): 一个巨大的地址范围(例如 0 到 4GB)。
I/O 端口空间 (I/O Port Space): 一个额外的、小得多的地址范围(例如 0 到 65535)。
如何区分: 地址
0x3F8在“内存空间”和“I/O 空间”中同时存在,它们是两个完全不同的物理位置。 CPU 必须使用不同的机器指令来声明它想访问哪个空间:当 CPU 执行
mov,load,store指令时,它的控制单元 (CU) 被硬编码(wired)为只访问“内存地址空间”。当 CPU 执行特殊的
in或out指令时,它的 CU 被硬编码为只访问“I/O 端口空间”。(这也是xv6所采用的方案)
硬件实现(如文所述): 当 CPU 执行指令时:
mov [0x3F8]:CPU 将0x3F8放在地址总线上,同时在一条特殊的控制总线引脚上输出信号 1(代表“内存访问”)。主存 (RAM) 会响应这个请求。out 0x3F8, al:CPU 将0x3F8放在地址总线上,同时在那条特殊的控制总线引脚上输出信号 0(代表“I/O 访问”)。I/O 设备(串口)会响应这个请求。
方案二内存映射:
概念: 硬件架构只定义一个统一的地址空间,即内存地址空间。
如何区分: 系统不需要区分。系统设计师(硬件层面)在启动时就**“保留” (Reserve)** 了一部分内存地址。
例如,总地址空间是 0 到 64GB。
地址
0x00_0000...到0x0F_FFFF...(前 40GB) 被映射 (Mapped) 到物理 RAM 芯片。地址
0xF0_0000...到0xF0_0FFF...(中间的一小块) 被保留,并映射到网卡的控制寄存器。
硬件实现: CPU 只使用
load和store指令(RISC-V 根本没有in/out指令)。 主板上的地址解码器(Address Decoder,芯片组的一部分)负责“路由”请求:CPU 执行
store(存数据) 到地址0x08_1234...。地址解码器看到这个地址,“识别出”它属于RAM 的范围,于是将请求转发给主存 (RAM)。
CPU 执行
store(存数据) 到地址0xF0_0008...。地址解码器看到这个地址,“识别出”它属于网卡的保留范围,于是将请求转发给网卡,而不是主存 (RAM)。
