一个操作系统的设计与实现——第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引导程序需要满足以下两个条件:

  1. 只能使用20位内存地址
  2. 对齐到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.sAP.s的编译与链接命令。

本章代码26/Kernel.c测试了多处理器环境下的任务切换。现在, 当多次加载Test.c任务后,所有任务的总运行时间应明显加快。

至此,我们已经实现了一个64位多处理器操作系统。

posted @ 2024-09-01 10:29  樱雨楼  阅读(10)  评论(0编辑  收藏  举报