25、SMP在x86上的实现:APIC、IOAPIC、SMP启动协议

好,咱们今天聊点硬核的。x86平台上的SMP实现,说白了就是让多个CPU核能协同工作。我最早接触这块时,也被APIC、IOAPIC这些缩写搞得头大。但别怕,咱们一步步拆开看。

APIC:每个核的“私人秘书”

APIC,全称Advanced Programmable Interrupt Controller。你可以把它想象成每个CPU核的私人秘书。这个秘书负责两件事:一是接收外部发来的中断,二是给核内发送中断。

每个CPU核都有一个Local APIC。它有自己的ID,叫APIC ID。这个ID很重要,系统用它来区分不同的核。我在项目中遇到过一个问题:某次调试时,两个核的APIC ID竟然冲突了,结果系统直接崩溃。嗯,从那以后我每次初始化都会先检查APIC ID的唯一性。

Local APIC有几个关键寄存器:

  • ID寄存器:存放APIC ID,只读
  • 版本寄存器:告诉你APIC的版本和特性
  • 任务优先级寄存器:控制哪些中断可以被处理
  • 中断命令寄存器:用来发送IPI(核间中断)

核心要点:Local APIC是每个核独立拥有的,它们之间通过系统总线通信。你想想看,如果没有这个机制,一个核怎么告诉另一个核“嘿,该你干活了”?

IOAPIC:外部中断的“总调度”

IOAPIC是Input/Output APIC的缩写。它负责收集所有外部设备的中断请求,然后分发给合适的CPU核。

为什么需要IOAPIC?因为传统的8259A中断控制器只能处理15个中断,而且只能发给一个核。在多核系统里,这显然不够用。IOAPIC可以支持最多24个中断输入,还能灵活配置每个中断发给哪个核。

IOAPIC的核心是它的重定向表。这个表有24个条目,每个条目对应一个中断输入。每个条目包含:

  • 中断向量号
  • 目标APIC ID
  • 触发模式(边沿触发或电平触发)
  • 中断极性

我个人的习惯是,在系统初始化时,把IOAPIC的所有中断都配置成发给BSP(引导处理器)。等AP(应用处理器)启动完成后,再根据负载情况动态调整。这样做的好处是避免启动阶段的中断混乱。

避坑指南:我曾经在配置IOAPIC时,忘记设置目标APIC ID的掩码位。结果中断发到了所有核上,每个核都去处理同一个中断,系统直接死锁。记住,目标APIC ID的掩码位一定要设置正确。

SMP启动协议:从BSP到AP

x86上的SMP启动遵循一套标准协议。简单来说,就是先让一个核(BSP)启动,然后由它唤醒其他核(AP)。

具体流程是这样的:

  1. BSP启动:系统上电后,所有核都开始执行复位向量。但只有BSP会继续执行BIOS代码,其他核进入等待状态。
  2. BSP初始化:BSP完成基本的硬件初始化,包括设置GDT、IDT、页表等。
  3. 发送INIT IPI:BSP向所有AP发送INIT IPI,让它们进入一个已知状态。
  4. 发送SIPI:BSP向每个AP发送SIPI(Startup IPI),指定AP的启动代码地址。
  5. AP启动:AP收到SIPI后,从指定地址开始执行启动代码,完成自身的初始化。
  6. AP就绪:AP初始化完成后,设置一个标志位,通知BSP自己已经就绪。

这里有个细节要注意:SIPI发送的目标地址必须是4KB对齐的。我刚开始做时没注意这个,结果AP启动后直接跑飞了。调试了两天才发现是地址对齐的问题。

重要提醒:发送SIPI时,BSP必须等待足够长的时间,让AP完成初始化。这个时间通常需要10毫秒左右。如果等待时间太短,AP可能还没准备好,后续的通信就会失败。

核间中断(IPI)的使用

IPI是SMP系统中核间通信的主要手段。Local APIC的中断命令寄存器(ICR)就是用来发送IPI的。

ICR是一个64位的寄存器,分为低32位和高32位。低32位包含:

  • 中断向量号
  • 传送模式(固定、最低优先级、SMI、NMI、INIT、SIPI等)
  • 目标模式(物理模式或逻辑模式)
  • 触发模式

高32位包含目标APIC ID或目标逻辑组。

我常用的IPI类型有:

  • INIT IPI:让目标核进入初始化状态
  • SIPI:启动目标核
  • 固定IPI:发送一个普通的中断
  • NMI IPI:发送不可屏蔽中断,常用于调试

实战经验:在RT-Thread的SMP实现中,我使用固定IPI来实现核间的任务迁移。当一个核上的任务需要被迁移到另一个核时,发送一个IPI,目标核收到后就会检查是否有新任务需要处理。这种方式比轮询高效得多。

启动代码示例

下面是一个简化的AP启动代码片段。注意,这段代码是用汇编写的,因为AP启动时还处于实模式:

; AP启动代码,位于0x1000地址
ap_start:
    ; 设置段寄存器
    mov ax, cs
    mov ds, ax
    mov es, ax
    
    ; 加载GDT
    lgdt [gdt_ptr]
    
    ; 切换到保护模式
    mov eax, cr0
    or eax, 1
    mov cr0, eax
    
    ; 跳转到32位代码
    jmp 0x08:ap_protected

[BITS 32]
ap_protected:
    ; 设置栈指针
    mov esp, ap_stack_top
    
    ; 初始化Local APIC
    call apic_init
    
    ; 设置就绪标志
    mov dword [ap_ready], 1
    
    ; 进入空闲循环
    .loop:
        hlt
        jmp .loop

这段代码虽然简单,但包含了AP启动的核心步骤。你想想看,如果没有这些底层操作,多核系统根本跑不起来。

总结

APIC、IOAPIC和SMP启动协议,是x86多核系统的基石。APIC让每个核有了独立的中断处理能力,IOAPIC让外部中断可以灵活分发,SMP启动协议则确保了多核的有序启动。

我个人觉得,理解这些底层机制,对做嵌入式系统开发特别有帮助。尤其是当你遇到奇怪的中断问题或启动失败时,这些知识就是你的救命稻草。

最后的小建议:如果你刚开始接触x86 SMP,建议先在一个模拟器上实验。比如用QEMU模拟多核环境,一步步调试AP的启动过程。我曾经在QEMU上花了一周时间,把整个启动流程摸透了,后来在实际硬件上调试时,基本没遇到大问题。