x86 体系结构基础——特权级、中断异常、内存与总线

本文目标:给机器学习系统工程师/虚拟化初学者打底——把 x86 上"软件如何与硬件协作"的全链路讲清。从特权级 ring0~ring3 出发,串到用户态/内核态切换、中断与异常、MMU 与页表、DMA、PCIe 总线、最后到 IOMMU。读完你能解释"为什么虚拟化需要 VT-x/EPT、为什么容器隔离要靠内核、为什么 GPU 能直接读写内存"。


一、全景:一次系统调用穿越了多少层

先给一张全景,后面逐层拆:

1
2
3
4
5
6
7
8
9
10
应用(用户态, ring3)
│ write(fd, buf, n) ← 软中断/syscall 指令

内核(内核态, ring0) ← 特权切换: 保存现场、查页表、设驱动
│ 驱动程序

设备(网卡/GPU/磁盘, DMA) ← 通过 PCIe 总线直接读写内存
│ ← MMU 翻译 CPU 地址, IOMMU 翻译设备地址

物理内存(DRAM)

这五件事——特权级、用户/内核态、中断异常、MMU 页表、DMA/PCIe/IOMMU——就是 x86 软硬件协作的全部地基。下面逐个讲。


二、特权级 ring0~ring3

x86 提供 4 个特权级(Protection Rings),用 CPL(Current Privilege Level,当前特权级)表示 CPU 当前在哪个环:

1
2
3
4
ring0  ← 内核 / hypervisor:可执行所有指令、访问所有寄存器与硬件
ring1 ← 系统服务(实际很少用)
ring2 ← 设备驱动(实际很少用)
ring3 ← 应用程序:受限,不能直接碰硬件

现代操作系统只用了 ring0 和 ring3 两级(Linux/Windows 都是),ring1/ring2 历史上设计给微内核用,实际基本闲置。所以日常说的"内核态=ring0、用户态=ring3"。

2.1 为什么需要特权级

核心矛盾:应用不可信,但应用又必须能用硬件资源(网络、磁盘、内存)。解决方式是把指令分成两类:

  • 普通指令:任何环都能执行(加法、访存、逻辑运算)。
  • 特权指令 / 敏感指令:只有 ring0 能执行,ring3 执行会触发 General Protection Fault(#GP,异常 13) 陷入内核。例如:
    • hlt(停 CPU)、cli/sti(关/开中断)
    • 修改 CR0/CR3(控制寄存器、页表基址)
    • lgdt/lidt(加载 GDT/IDT)
    • IN/OUT(端口 IO,操作设备寄存器)
    • WRMSR/RDMSR(读写 model-specific register)

2.2 DPL 与访问控制

每个段描述符(在 GDT/LDT 里)有 DPL(Descriptor Privilege Level,描述符特权级),表示"允许哪个最低级别的 CPL 访问它"。访问规则简化为:

允许访问    max(CPL,RPL)DPL\text{允许访问} \iff \max(\text{CPL}, \text{RPL}) \leq \text{DPL}

RPL(Requested Privilege Level)是段选择子里可由软件设定的字段,用来"自降特权"防止越权。

面试要点:ring 机制的本质 = 给指令分级 + 给段描述符标 DPL,硬件在执行时做 CPL vs DPL 校验,不满足就 #GP 陷入内核。这是"应用不能直接碰硬件"的硬件级保障。

2.3 64 位下的"扁平模型"

x86-64 进入长模式后,段机制大幅弱化——代码段/数据段基址都设为 0、界限都设为全空间,叫"扁平内存模型"。所有段覆盖整个 64 位地址空间,CS/DS 只是装个 DPL 标签做权限,寻址完全交给分页(MMU 页表)。所以现代 x86 的隔离主要靠页表,不是靠分段


三、用户态与内核态、系统调用

"用户态/内核态"是 ring0/ring3 在操作系统语义层的叫法。

3.1 两种执行上下文

  • 用户态(ring3):跑应用代码,权限受限,不能直接执行特权指令、不能直接访问物理硬件、不能访问内核内存。
  • 内核态(ring0):跑内核代码,全权。

3.2 特权级切换:系统调用

应用要读文件、发包,必须进内核。过程:

  1. 应用把参数放进寄存器(Linux:rax 放系统调用号,rdi/rsi/rdx... 放参数),执行 syscall 指令(64 位;老式用 int 0x80 软中断)。
  2. syscall 由硬件完成特权级切换:
    • 保存用户态 RIP/RFLAGS 到内核栈(或 MSRs)。
    • IA32_LSTAR MSR 加载内核入口地址,跳过去。
    • CPL 从 3 变 0,切到内核栈。
  3. 内核执行系统调用处理函数,校验参数(关键安全点:内核绝不信任用户指针,要 copy_from_user 校验)。
  4. 返回时执行 sysret/iret,恢复用户态现场。

3.3 切换的代价

每次系统调用都有:上下文切换 + 特权级切换 + 栈切换 + TLB 可能失效 的开销,约几百纳秒到微秒级。这就是为什么:

  • 高吞吐网络要 io_uring/DPDK/zero-copy 绕开系统调用。
  • 内存数据库要把数据常驻用户态避免反复 read
  • RDMA 要 kernel bypass(参见 RDMA 那篇)。

面试金句:“ring0/ring3 是硬件特权级,用户态/内核态是 OS 语义;系统调用是两者唯一的合法桥梁,桥梁的代价就是上下文切换——所有绕内核技术(RDMA、DPDK、io_uring)本质都是少过这座桥。”


四、中断与异常

CPU 在执行时会被两类事件打断:中断(异步,外部来)和异常(同步,当前指令引发)。

4.1 区别

中断(Interrupt) 异常(Exception)
来源 外部硬件(网卡、定时器、键盘) CPU 执行当前指令引发
时机 异步,指令边界 同步,当前指令
举例 网卡收包 IRQ、时钟 tick 缺页 #PF、除零 #DE、非法指令 #UD、#GP

4.2 中断向量与 IDT

x86 用 0~255 号向量标识每个中断/异常。关键向量:

  • 0 #DE 除零、6 #UD 非法指令、13 #GP 通用保护、14 #PF 缺页、
  • 32~255 是 可屏蔽硬件中断(由 IO-APIC/Local APIC 投递)。

IDT(Interrupt Descriptor Table,中断描述符表):一张 256 项的表,每项是一个门描述符(中断门/陷阱门),记录该向量的处理函数地址 + DPL。IDTR 寄存器存 IDT 基址。

4.3 处理流程(中断/异常通用)

  1. CPU 确定向量号 NN
  2. 查 IDT[NN] 拿到处理函数地址和目标 CS(决定目标特权级)。
  3. 若需要特权级提升(如用户态中断进内核),切换栈到内核栈。
  4. 压入 RFLAGS/RIP/CS(和可能的错误码)。
  5. 跳到处理函数执行。
  6. iret 返回,恢复现场和特权级。

4.4 缺页异常 #PF(重点,连虚拟化)

访问一个未映射或权限不对的虚拟页时触发 #PF,CR2 寄存器存引发缺页的虚拟地址。内核 page fault 处理:

  • 地址合法但页不在内存 → 从磁盘换入(demand paging)。
  • 地址非法 → SIGSEGV 段错误杀进程。
  • 写只读页但 COW(copy-on-write)→ 复制页改可写。

虚拟化关联:在 **EPT(扩展页表)**下,Guest 的地址翻译有两级——GVA→GPA(Guest 页表)和 GPA→HPA(EPT)。Guest 页表缺页走 Guest 自己的 #PF;EPT 缺页(GPA 没映射到物理页)触发 EPT Violation,陷入 hypervisor 处理。两者截然不同,是 KVM 内存虚拟化的核心。

4.5 中断控制器:PIC → APIC

  • 老 PIC(8259):最多 15 个中断。
  • APIC:Local APIC(每 CPU 一个,处理本地中断、IPI 核间中断)+ IO-APIC(收设备中断转发给 CPU)。多核必备。
  • MSI/MSI-X:设备直接写 Local APIC 寄存器发中断,绕过 IO-APIC 和电平信号, PCIe 设备主流方式。中断数更多、更快、更省中断引脚。

五、MMU 与页表(虚拟内存的地基)

现代系统里"内存隔离"主要靠分页:每个进程有自己的虚拟地址空间,靠页表翻译到物理地址,页表项带权限位,越权访问 → #PF。

5.1 虚拟地址 → 物理地址翻译

x86-64 用 4 级页表(48 位虚拟地址,普通 4KB 页;5 级页表支持 57 位):

1
2
3
4
5
6
7
8
9
10
虚拟地址(48bit): [ 签名(高位) | PML4(9) | PDPT(9) | PD(9) | PT(9) | 页内偏移(12) ]

CR3 ──► PML4 表(512项)
│ 选第 PML4 项

PDPT 表 ──► 选第 PDPT 项

PD 表 ──► 选第 PD 项

PT 表 ──► 选第 PT 项 ──► 物理页基址 + 偏移 = 物理地址

每级 9 位索引(512 项),页内偏移 12 位(4KB)。**每张表 4KB,一个进程完整映射 48 位空间要 4 级表全展开——实际靠 lazy 分配,按需建表。

5.2 页表项 PTE 的权限位(隔离核心)

PTE 关键位:

  • R/W:读写位。写只读页 → #PF。
  • U/S:User/Supervisor 位。U=1 用户态可访问;U=0 只有 ring0 能访问。这就是"内核内存对用户态不可见"的硬件机制——内核页 PTE 的 U=0。
  • NX:No-eXecute,不可执行位。数据页设 NX 防代码注入(DEP/W^X)。
  • P:Present,页是否在内存。P=0 触发缺页。
  • A/D:Accessed/Dirty,访问/写过位。

5.3 TLB 与 PCID

页表是 4 级内存查表,每次访存要 4 次内存读 → 太慢。TLB(Translation Lookaside Buffer) 是 CPU 内缓存最近翻译的 PTE,命中就一步拿到物理地址。TLB 失效(miss)才走 4 级表。

  • 进程切换换页表(CR3)→ TLB 大量失效。PCID(Process Context ID) 给每个进程一个 ID,TLB 项带 PCID 标记,切换时不刷别的进程的 TLB,加速上下文切换。
  • 大页(2MB/1GB HugePages):减少页表层级、减少 TLB miss,适合大内存工作负载(数据库、ML 显存映射)。

5.4 虚拟内存的意义

  • 隔离:进程地址空间独立,互不踩踏。
  • 按需分配mmap 只建 VMA,真访问才分配物理页(demand paging)。
  • 共享:共享库、COW fork 都靠映射同一物理页。
  • 交换:内存压力下换出到磁盘,突破物理内存上限(但慢)。

六、DMA:设备直接读写内存

6.1 没有 DMA 的世界

CPU 用 IN/OUT 指令逐字节/逐字从设备寄存器读数据到寄存器再存内存——PIO(Port IO)。吞吐极低、CPU 全程搬运。1Gbps 网卡能把 CPU 跑满。

6.2 DMA 的解法

DMA(Direct Memory Access,直接内存访问):让设备里的 DMA 引擎自己读写内存,CPU 只下命令"把这块内存发出去"或"收到数据放到这块内存",然后 CPU 去干别的,DMA 完成后用中断通知 CPU。

数据流:

1
2
3
4
5
6
7
CPU 下发 DMA 描述符(含内存地址、长度、方向)


DMA 引擎 ──直接读写──► 物理内存(DRAM) ← CPU 不参与搬运


完成 → 中断通知 CPU

6.3 DMA 用的地址是物理地址

关键点:DMA 引擎直接操作物理地址,不走 CPU MMU 页表。也就是说设备看到的是物理地址(或经 IOMMU 翻译后的 IOVA,见下文)。所以驱动要给设备物理地址或 IOVA,不能给虚拟地址。这就是为什么驱动要先把用户态虚拟地址 pin 住并翻译(参见 RDMA MR 注册)。

6.4 一致性与缓存问题

  • DMA coherence:DMA 写内存时,CPU cache 里可能有旧数据。x86 默认 cache coherent(硬件保证 DMA 与 cache 一致),软件省心;某些架构(ARM、嵌入式)需要显式 cache flush。
  • 流式映射 vs 一致性映射:Linux DMA API 区分 dma_alloc_coherent(一致性,自动同步)和 dma_map_single(流式,需手动 sync)。

七、PCIe 总线:设备挂在哪里

DMA 设备通过 PCIe(Peripheral Component Interconnect Express) 连到 CPU。

7.1 拓扑

1
2
3
4
5
6
7
8
      CPU ── Root Complex(RC) ──┬── Switch ──┬── Endpoint(网卡/GPU/SSD)
│ └── Endpoint
└── Endpoint(直连RC的设备)

RC: 根复合体, CPU 内存控制器的 PCIe 入口
Switch: PCIe 交换机, 多端口转发, 内部不暴露给软件
Endpoint: 终端设备(网卡/GPU/NVMe), 每个有独立配置空间
Bridge: 桥接器

7.2 配置空间与 BAR

每个 PCIe 设备有 256B/4KB 配置空间,含厂商/设备 ID、BAR(Base Address Register)。BAR 是设备向系统申请一段地址空间(MMIO 空间)的寄存器——CPU 往这段 MMIO 地址写数据,就等于写设备寄存器。这就是 CPU 访问设备的两种方式:

  • MMIO:把设备寄存器映射到一段物理地址,CPU 用普通访存指令访问(主流,现代设备)。
  • Port IO:用 IN/OUT 指令访问独立端口地址(老式设备,如 8259 PIC、IDE)。

7.3 PCIe 的两种事务

  • TLP(Transaction Layer Packet):PCIe 上传的报文,分 Memory Read/Write、Configuration、Message 等。
  • P2P(Peer-to-Peer):两个 endpoint 之间直接传数据,不经过主内存(CPU 不参与)。GPU Direct RDMA 就是 PCIe P2P——网卡直接 DMA GPU 显存。要求两个设备在 PCIe 拓扑上能 P2P(最好同 Switch 下)。

7.4 MSI/MSI-X

PCIe 设备用 MSI/MSI-X 发中断:设备写一个特定内存地址(Local APIC 的 MMIO 区域),APIC 自动给 CPU 发中断。比传统 INTx 引脚快、中断数多(MSI-X 可达 2048 个向量),网卡每收发队列一个中断、GPU Direct 一个完成中断都用它。


八、IOMMU:给 DMA 上权限(虚拟化的安全基石)

8.1 问题:DMA 不受 ring 限制

DMA 绕开 CPU,也绕开 ring0/ring3——设备一旦拿到物理地址,能读写整块物理内存,包括内核。如果一台机器上多个 VM 共享设备,或一个设备分给某 VM,怎么办?需要给 DMA 也加一层"页表 + 权限",这就是 IOMMU(Intel 称 VT-d,AMD 称 AMD-Vi,ARM 称 SMMU)。

8.2 IOMMU 做两件事

  1. 地址翻译:设备用的是 IOVA(I/O Virtual Address),IOMMU 有一张类似页表的表,把 IOVA 翻译成 HPA。驱动给设备的是 IOVA,IOMMU 翻译后真去访问物理内存。
  2. 隔离与权限:只允许设备访问它被授权的 IOVA 范围,越权 → IOMMU 阻断并报错(fault)。这样一个 VM 的设备只能访问这个 VM 的内存

8.3 IOMMU 的关键能力

  • VFIO(设备直通):把一块物理设备独占地分配给一个 VM,VM 里跑原生驱动直接操作真硬件,性能近裸机。靠 IOMMU 保证这个 VM 的设备 DMA 只能碰到这个 VM 的内存,不会越界到 host 或别的 VM。GPU 直通就是 VFIO + IOMMU。
  • SR-IOV:把一块物理网卡切成多个 VF(Virtual Function),每个 VF 是一个轻量 PCIe 设备,各有独立配置空间和 DMA 队列,分给不同 VM/容器,硬件级隔离。云网络高频。
  • 共享 IOMMU 组:同 IOMMU 组的设备互相可见、可 P2P,跨组不可见——GPU Direct RDMA 要求网卡和 GPU 在同一 IOMMU 组(同一 PCIe Switch 下的 P2P 才安全)

8.4 DMA Remapping 与中断重映射

  • DMA Remapping:IOMMU 的核心,按设备 BDF(Bus/Device/Function)查对应翻译表,重写 DMA 地址。
  • Interrupt Remapping:设备 MSI/MSI-X 的中断也经 IOMMU 校验和重映射后再投递给 CPU,防止恶意设备伪造中断。虚拟化中断隔离靠它。

8.5 串起虚拟化全图

把今天所有概念连起来看"一个 VM 怎么发出一个网络包":

1
2
3
4
5
6
7
8
9
VM 里 Guest 应用调用 send()
→ Guest 内核(在 VMX non-root, ring0)执行
→ 通过 virtio 把数据放进 virtqueue(Guest 物理内存)
→ 触发 VM-Exit(CPU 虚拟化: VMCS 保存现场) 回到 host 的 QEMU/KVM
→ host 把数据通过 tap/网卡发出
网卡 DMA 读 host 内存: 地址经 IOMMU 翻译+校验
→ 网卡发完触发 MSI-X 中断 → IOMMU 中断重映射 → 投递给 CPU
→ CPU 进内核态(ring0)处理中断
→ 唤醒等待的应用(ring3)

每一步都用到了今天讲的:特权级、用户/内核态、中断、MMU 页表、DMA、PCIe、IOMMU。这就是为什么这些是虚拟化和系统开发的地基。


九、面试速答清单

Q1:ring0/ring3 是什么,为什么需要?

x86 的 4 级特权(实际用 2 级),ring0 内核全权、ring3 应用受限。硬件在执行特权/敏感指令时校验 CPL vs 段描述符 DPL,不满足触发 #GP 陷入内核。作用是防止不可信应用直接碰硬件和内核内存。

Q2:系统调用是怎么从用户态进内核态的?

应用执行 syscall 指令,硬件完成特权级切换:保存 RIP/RFLAGS、从 LSTAR MSR 取内核入口、CPL 3→0、切内核栈;内核处理完用 sysret/iret 返回。代价是上下文+特权+栈切换,所以高吞吐场景要绕开系统调用(RDMA/DPDK/io_uring)。

Q3:中断和异常的区别?缺页是哪个?

中断异步来自外部设备,异常同步由当前指令引发。缺页 #PF 是异常——访问未映射/越权虚拟页时由 MMU 触发,CR2 存出错地址。内核按合法/非法/COW 分流处理。

Q4:页表怎么实现内存隔离?

每进程独立页表,PTE 有 U/S 位:内核页 U=0,用户态访问就 #PF。R/W 位管读写、NX 位管执行。进程切换换 CR3 → TLB 失效(PCID 可缓解)。虚拟内存靠这套做隔离、按需分页、共享、交换。

Q5:DMA 为什么快?它用的什么地址?

DMA 让设备 DMA 引擎直接搬数据进/出内存,CPU 不参与逐字搬运,只在开始下命令、结束收中断时参与。DMA 用物理地址(或经 IOMMU 的 IOVA),不走 CPU MMU 页表,所以驱动要给设备物理地址/IOVA。

Q6:IOMMU 解决什么问题?和 MMU 什么关系?

MMU 管 CPU 访存的翻译隔离,但 DMA 绕开 MMU 也能读写物理内存。IOMMU 是"给设备用的 MMU"——按设备 BDF 查表把 IOVA 翻译成物理地址,并校验权限,越权报 fault。这样设备直通给 VM 时,VM 的 DMA 只能碰自己的内存。GPU 直通、SR-IOV、GPU Direct RDMA 的安全地基都是 IOMMU。

Q7:GPU Direct RDMA 为什么要求网卡和 GPU 在同一 PCIe Switch 下?

GDR 走 PCIe P2P(网卡直接 DMA GPU 显存,不经主内存)。P2P 要求两端在 PCIe 拓扑上可达且在同一 IOMMU 组(同 Switch 下最安全、性能最好);跨 PCIe root/跨 NUMA 会引入桥接和地址翻译开销,性能下降明显。


十、一张图收口

1
2
3
4
5
特权:  ring3(用户) ──syscall──► ring0(内核) ──驱动──► 设备
中断: 设备 ─MSI-X─► IOMMU中断重映射 ─► Local APIC ─► CPU进内核态处理
访存: CPU用虚拟地址 ─► MMU(4级页表) ─► 物理内存 [PTE: U/S R/W NX]
DMA : 设备用IOVA ─► IOMMU(DMA Remapping) ─► 物理内存 [按BDF隔离]
总线: CPU─RC─Switch─Endpoint(网卡/GPU/NVMe) BAR申请MMIO, P2P同组可直连

记住主线:特权级把指令分了层,中断异常是软硬件的打断通道,MMU 页表管 CPU 访存隔离,DMA 让设备直连内存,PCIe 是设备挂载的血管,IOMMU 是给 DMA 上的锁。 这五块是 x86 软硬协作的地基,也是虚拟化、容器、RDMA、GPU 直通等所有上层技术的根。


参考资料

  • Intel ® 64 and IA-32 Architectures Software Developer’s Manual, Vol.3(特权级/中断/分页/VMX/EPT 权威来源)
  • Intel ® Virtualization Technology for Directed I/O (VT-d) Specification(IOMMU)
  • PCI Express Base Specification
  • 《深入理解 Linux 内核》《Linux 设备驱动程序》
  • kernel.org 文档:Documentation/x86/, Documentation/driver-api/