一个操作系统的设计与实现——第26章 多处理器(下):应用处理器引导
26.1 应用处理器
当计算机启动时,不管其中有多少个CPU,都只有一个CPU会真正启动,这个CPU就称为引导处理器(Bootstrap Processor,BSP);而其他CPU会等待被BSP唤醒,这些CPU就称为应用处理器(Application Processor, AP)。
BSP可以在任意时刻向所有AP广播唤醒信号,当AP接受到信号后,就会启动BIOS,随后跳转到操作系统为其提供的引导程序处继续执行。具体来说,唤醒功能由LAPIC提供,想要唤醒AP时,BSP应先向0xfee00300
发送固定的0x000c4500
,再向同一地址发送0x000c4600 | AP引导程序的第12~19位
。也就是说,AP引导程序需要满足以下两个条件:
- 只能使用20位内存地址
- 对齐到
0x1000
在我们的操作系统中,AP引导程序位于硬盘的1号扇区,并加载到0x8000
处,能够满足上述条件。
需要指出的是:笔者发现在bochs
中,上述两次发送不能连续进行,而是需要在中间添加一个"跳转到下一行"的指令,这可能是bochs
的一个bug。这个指令的机器语言为:db 0xeb, 0x0
。
AP的引导时机是可以自由选择的。这意味着:AP引导程序可以使用各种现成的组件,如PML4、GDTR、IDTR等,这就使得AP引导程序是非常简单的,其是BSP引导过程的简化版。
26.2 LAPIC的开关
默认状态下,只有BSP的LAPIC是打开的,可以直接使用,而AP的LAPIC是关闭的。LAPIC的开关位于0xfee000f0
地址处的第8位,将其置1即可打开LAPIC。
26.3 AP的自我识别
每个CPU都有一个编号,BSP的编号是0,AP的编号从1开始向后顺延。编号由LAPIC提供,位于0xfee00020
地址处的第24~31位。
26.4 引导AP前的准备
每个CPU的寄存器是独立的,但内存是共享的。因此,PML4,GDT,IDT等位于内存中的组件均可在CPU之间共享,而GDTR,IDTR可使用sgdt/sidt
存入内存后共享。
TSS用于获取任务的0特权级栈,由于每个CPU都会运行一个任务,因此TSS不能在CPU之间共享,而是应该为每个CPU分别安装一个。
请看本章代码26/Mbr.s
。
第156~158行,在GDT中再安装3个TSS描述符。这些TSS的地址从0xffff800000092000 + 128
开始向后顺延,每个TSS的大小拓展到128字节。
接下来,请看本章代码26/Task.hpp
。
第10行,定义CPU的数量为4。也就是说,我们的操作系统固定使用1个BSP和3个AP。
第16行,将所有TSS清零。
第18~21行,关闭所有TSS的IO位图功能。
26.5 AP引导程序的实现
AP的引导是在操作系统启动后期才开始的,此时的操作系统处于IA32-e模式,使用的是64位ELF格式的代码,但AP处于实模式,不能使用64位ELF格式的代码。因此,AP的引导分为两阶段进行:
- 第一阶段:利用现成的GDT和PML4快速进入IA32-e模式。这一阶段的引导程序是独立的,其位于硬盘的1号扇区
- 第二阶段:在IA32-e模式下继续引导。这段引导程序是内核的一部分
请看本章代码26/AP.h
。
第3行,声明了apInit
函数。这个函数是用汇编语言实现的。
接下来,请看本章代码26/AP.s
。
第10行,定义CPU的数量为4。
第18~21行,为AP引导程序准备信息,这些信息如下表所示:
地址 | 字节数 | 含义 |
---|---|---|
0x7e00 |
10 | GDTR |
0x7e10 |
10 | IDTR |
0x7e20 |
8 | AP的第二阶段引导程序的入口 |
第23~26行,将AP的第一阶段引导程序从1号扇区读取到0x8000
处。
第28~31行,向AP广播唤醒信号。
第33~36行,等待所有AP引导完成。这段代码的原理见下。
接下来,请看本章代码26/APBoot.s
。
APBoot.s
是AP的第一阶段引导程序,其相当于简化版的Mbr.s
。
第3行,直接从0x7e00
加载GDTR。GDTR已经由BSP准备完毕。
第5~24行,进入保护模式并初始化所有段寄存器。
第26~27行,直接安装BSP已经准备好的PML4。
第29~42行,进入IA32-e模式。
第48行,跳转到AP的第二阶段引导程序。
接下来,请看本章代码26/AP.s
。
apBoot64
函数是AP的第二阶段引导程序,其实现可以对照Kernel.c
,如下表所示:
BSP | AP |
---|---|
printInit |
不需要 |
memoryInit |
不需要 |
intInit |
需要初始化APIC,并lidt [0x7e10] |
taskInit |
需要安装TR、IA32_GS_BASE 以及内核任务 |
fsInit |
不需要 |
keyboardInit |
不需要 |
syscallInit |
需要,可直接调用syscallInit 函数 |
apInit |
不需要 |
shellInit |
不需要 |
sti |
需要 |
for (;;) |
需要 |
deleteTask |
不需要 |
hlt |
需要 |
第46~47行,直接从0x7e00
加载64位的GDTR,从0x7e10
加载64位的IDTR。GDTR与IDTR已经由BSP准备完毕。
第49~51行,取得AP的编号。
第53~56行,计算AP的TCB地址。计算公式为:TCB地址 == 0xffff8000000a0000 - (AP编号 + 1) * 0x1000
。
第58行,安装0特权级栈。
第60~61行,打开LAPIC。
第62~64行,设定LAPIC定时器。
第66~70行,设定IO APIC中的时钟中断。
第72~74行,安装TR。计算公式为:tr选择子 == (AP编号 * 2 + 7) << 3
。
第76~83行,安装IA32_GS_BASE
。其值等于AP的TSS地址,计算公式为:gs段基址 == TSS地址 == 0xffff800000092000 + AP编号 * 128
。
第85~87行,安装内核任务。
第89行,安装快速系统调用。
至此,AP引导完毕。
第91行,在总线锁定状态下将apInitFlag
加1。apInitFlag
是一个初值为1的计数器,当其值增加到4时,就表示所有AP均引导完毕。
第93行,打开中断。
至此,AP也能参与时钟中断,并切换到其他任务执行了。现在的taskQueue
中一共存在5个永不退出的任务,它们分别是:
- 一个带有任务回收功能的任务
- 三个无操作任务
- 一个外壳程序
在任意时刻,这些任务被哪个CPU执行是未知的,也就是说,BSP与AP现在已经没有区别了。
第95~98行,挂起AP。构成上述三个无操作任务
中的一个。
第100~101行,定义AP初始化计数器apInitFlag
。
26.6 编译与测试
首先,bochs
在默认状态下只有一个CPU。在其配置文件中增加一行:cpu: count=4
,即可使bochs
启用4个CPU。
本章代码26/Makefile
增加了APBoot.s
与AP.s
的编译与链接命令。
本章代码26/Kernel.c
测试了多处理器环境下的任务切换。现在, 当多次加载Test.c
任务后,所有任务的总运行时间应明显加快。
至此,我们已经实现了一个64位多处理器操作系统。