一般而言,UEFI 的主要作用是检测和初始化设备,加载操作系统的引导程序,然后将控制权交给操作系统,整个过程不需要大量的运算,在单个 CPU 核上运行单线程程序已经可以满足需求,因此,EDK2 没有提供多线程机制。
如果需要在 UEFI 应用中支持多任务,则可以使用 EFI_MP_SERVICES_PROTOCOL
启动从 CPU,也可以自己实现一个多线程库,在每个CPU上运行并发任务。
多处理器服务
在多处理器系统中,系统初始化时,总是由一个处理器执行这些初始化指令,这个 CPU 称为 BSP ( Boot-Strap Processor )。系统中 BSP 之外的 CPU 称为 AP ( Application Processor )。
EFI_MP_SERVICES_PROTOCOL 功能及用法
PI (Platform Initalization) 标准中定义了 EFI_MP_SERVICES_PROTOCOL
(简称 MP 服务),这个 Protocol 用于在多处理器系统中管理处理器,包括查询处理器信息、启动或停止AP、设定 BSP,
除了 WhoAmI
服务外,EFI_MP_SERVICES_PROTOCOL
中的其他服务都只能在 BSP 上调用,在 AP上调用这些服务时会返回EFI_DEVICE_ERROR
错误。
1.查询服务
EFI_MP_SERVICES_PROTOCOL
提供的查询服务有三个:
GetNumberOfProcessors // 用于查询系统中的逻辑处理器个数,以及启用的逻辑处理器
GetProcessorInfo // 用于获取指定处理器的相关信息
WhoAmI // 用于获取当前处理器的编号
GetNumberOfProcessors
服务的函数原型:
其功能是查询指定逻辑处理器有关多处理器的信息。此服务仅返回多处理器相关信息(如处理器编号、处理器状态等),不提供平台相关的处理器信息(如Cache 大小、处理器频率等)。
查询时通过处理器编号(参数ProcessorNumber)指定待查询的处理器。处理器编号从0
开始。系统启动后,UEFI 把系统中的处理器从 0
开始编号,ProcessorNumber 就是这个编号 EFI_MP_SERVICES_PROTOCOL
中其他服务中的 ProcessorNumber 意义与之相同。如果指定的处理器编号超出系统中存在的处理器个数,则返回EFI_NOT_FOUND
。
GetProcessorInfo
服务返回的处理器信息存放在EFI_PROCESSOR_INFORMATION
结构体内:
其中,ProcessorId
是由系统硬件决定的地址。对 IA32 和 x64 处理器来说,它与本地 APIC ID
地址相同,低 8
位有效。对安腾处理器来说,低 16
位地址有效。要注意 ProcessorId
与 ProcessorNumber
(处理器编号)不同。
StatusFlag
是处理器状态:
第 0
位是 BSP 位(PROCESSOR_AS_BSP_BIT
),为 0
时说明该处理器是 AP;为 1
时说明该处理器是 BSP;
第 1
位是 EnabIed 位(PROCESSOR_ENABLED_BIT
),0
表示该处理器被禁用;1
表示该处理器被启用;
第 2
位是 Health 位(PROCESSOR_HEALTH_STATUS_BIT
),0
表示该处理器出现故障;1
表示该处理器正常。
3~31
位必须是 0
。
处理器信息还包含一个域 Location
,它是 EFI_CPU_PHYSICAL_LOCATION
类型的变量,表示处理器的物理地址,用于给每一个 CPU 定位。在一个计算机系统中,可能有多颗处理器(一颗处理器称为一个 Package
),每颗处理器可能有多个物理核(物理核称为 Core
),每个物理核又可能有多个逻辑核(逻辑核称为Thread
)。所有的编号都从0
开始。
EFI_CPU_PHYSICAL_LOCATION
结构体:
对于一个逻辑处理器,有三种地址:
1)ProcessNumber
,作为 MPService
服务的参数,假设系统中有 n
个处理器,ProcessNumber
在 0~n-1
之间,称为处理器编号;
2)Location
是由 Package
、Core
、Thread
组成的三元组,可给一个逻辑处理器定位;
3)Processorld
,由系统硬件决定的地址。
WhoAmI
用于获得当前处理器的编号 (ProcessorNumber
)。
2.管理处理器
EFI_MP_SERVICES_PROTOCOL
中用于管理处理器的服务有以下 4
个:
StartupThisAP
服务用在指定的AP
上执行传入的函数;StartupAIlAPS
服务用于在所有AP
上执行传入的函数;‘SwitchBSP
服务用于选定某个AP
作为BSP
;EnableDisableAP
服务用于启用或禁用某个AP
。
(1)StartupThisAP 服务
StartupThisAP
服务只能在 BSP上执行,用于在指定的 AP(此 AP 必须没有被禁用)上执行调用者提供的函数。调用者可以指定Timeout
时间。
如果逾期时 AP 上的任务还未完成,那么 AP 上的任务将被终止;
如果 AP 未处于 IDLE
状态,那么该服务会返回 EFI_NOT_READY
;
如果 AP 未处于 Enable
状态,那么该服务会返回EFI_INVALID_PARAMETER
;
此服务有阻塞和异步两种模式。参数 WaitEvent
为NULL
时,该服务使用阻塞模式;否则,使用异步模式。
阻塞模式下,该服务向 AP 发出指令后,等待 AP 把调用者提供的函数执行完毕,然后该服务成功返回。若 AP 完成任务前已逾期,则逾期时服务返回 EFI_TIMEOUT
。
异步模式下,该服务向 AP 发出指令后立即返回。AP 执行完任务后触发该服务提供的事件 WaitEvent
,并将Finished
标志设为TRUE
;若 AP 完成任务前已逾期,则逾期时终止 AP上的任务,触发事件 WaitEvent
,并将Finished
标志设为FALSE
。
系统事件 EFI_EVENT_GROUP_READY_TO_BOOT
被触发后,该服务仅提供阻塞模式。
(2)StartupAIlAPS
StartupAllAPs
用于在所有的 AP上执行指定的函数。如果有任何 AP 未处于 IDLE
状态,则该函数返回EFI_NOT_READY
。如果参数SingleThread
为TRUE
,则 AP 会依次执行Procedure
函数,前一 AP 从 Procedure
返回后下一 AP 才会执行Procedure
。如果参数SingleThread
为FALSE
,则所有的 AP 同时执行 Procedure
,开发者负责 Procedure
的线程安全。
StartupAIlAPS
服务有阻塞和异步两种模式。参数 WaitEvent
为NULL
时,该服务使用阻塞模式;否则,使用异步模式。
阻塞模式下,所有 AP 从 Procedure
返回或超时后,StartupAllAPs
返回。FailedCpuList
列表返回未成功执行 Procedure
的 CPU 的编号,系统为 FailedCpuList
分配内存,调用者负责释放该内存。
异步模式下,所有 AP 从 Procedure
返回或超时后,WaitEvent
事件触发,系统同样会设置 FailedCpuList
列表。
(3)SwitchBSP服务
SwitchBSP
用于将指定的 AP 切换为 BSP。成功切换后,该服务返回EFI_SUCCESS
,新的 BSP 将无缝接管原 BSP 的工作。当系统事件EFI_EVENT_GROUP_READY_TO_BOOT
触发后,该服务不再有效。
(4)EnableDisableAP 服务
EnableDisableAP
服务用于启动或禁用指定的 AP。
启动 AP 的过程
1.通过发送 IPIs 消息启动 AP
每个处理器 Core 上都有一个 APIC (Advanced Programmable Interrupt Controller) 芯片,称为本地 APIC。它接收来自本地或外部的中断信号,发送给 Core 上的处理器处理这个中断。
这些中断信号包括:来自处理器 Core 中断引脚的信号、来自 I/O APIC 的中断、来自性能监视寄存器的中断、本地 APIC 时钟中断、本地 APIC 错误中断、温度传感器中断、来自其他处理器的中断 (Inter-Processor Interrupts,简称 IPIS)。
AP 的启动是通过在 BSP 上向 AP 发送特定的 IPIs 完成的,而发送 IPIs 是通过写本地 APIC 的 ICR(Interrupt Command Register)完成的。
多处理器系统启动时,首先根据 MP 初始化协议选择一个处理器为 BSP,其他处理器作为 AP。BSP 执行初始化代码,初始化系统的 APIC 环境,建立系统的全局数据结构,初始化 AP(向 AP 发送 INIT-SIPI-SIPI IPIs
)。等 BSP 和 AP 初始化完成后,BSP 会执行 OS Loader 加载操作系统。
AP 重置(收到 INIT IPI
中断信号)或加电后,首先简单地进行自我配置,然后等待来自 BSP 的启动信号(来自 BSP 的 IPI),这个启动信号称为 SIPI。AP 收到 SIPI 后,会从实模式 0x000XX000
地址处开始执行。XX
是 SIPI 消息中的 8bit
的地址向量。
(1)寄存器 ICR 及 IPI 类型
发送 IPI 是通过写寄存器 ICR 完成的,ICR是 64 位寄存器,可分为两个 32 位寄存器。其中低 32 位的寄存器偏移为0x300
,高32位的寄存器偏移为0x310
。
可用 IPI 类型(DeliveryMode)共有7种:
0
:向目的处理器发送 Vector 指定的中断。
1
:同 000,但仅向目标处理器中优先级最低的处理器发送。
2
:向目标处理器发送 SMI 中断,Vector 必须是 0。
4
:向目标处理器发送 NMI中断,Vector被忽略。
5
:向目标处理器发送 INIT 中断(称为 INITIPI),目标处理器收到 INITIPI 后进行初始化,然后等待 SIPI。
6
:向目标处理器发送 SIPI(startup) 中断,目标处理器收到 SIPI 后执行 Vector 指定的代码。
7
:向目标处理器发送信号,目标处理器收到信号后向外部 8259A 控制器发出请求从而获得 Vector。
(2)向目标处理器发送 IPI
发送 IPI 是通过写寄存器 ICR 实现的:
a.写 ICR 高 32 位的寄存器XAPIC_ICR_HIGH_OFFSET
,该寄存器包含了目的处理器的 APCI 地址;
b.写 ICR 低 32 位的寄存器XAPIC_ICR_LOW_OFFSET
,写入该寄存器后 IPI 被发送,目的 CPU 收到消息后,发送方的XAPIC_ICR_LOW_OFFSET
的 DeliveryStatus
会设置为 0
;
c.检查XAPIC_ICR_LOW_OFFSET
的 DeliveryStatus
,直到该值变为 0
。
Sendlpi
函数实现了这个流程:
(3)启动 AP 的 IPI 序列
启动 AP 是通过向 AP 发送 INIT-SIPI-SIPI
列完成的,SendInitlpi
函数用于发送 INIT IPI 消息。在 SendInitlpi
函数中,首先构造 INIT IPI 消息,然后调用 SendIpi
函数发送该消息。
SendInitSipiSipi
函数用于发送 INIT-SIPI-SIPI 序列,发送过程分为三步:发送 INIT IPI 消息,发送 SIPI 消息,再次发送 SIPI 消息。在 SIPI 消息中包含了 AP 启动向量 StartupRoutine
。目标处理器接收 IPI 序列后 SendInitSipiSipi
函数返回。目标 AP 接收到 INIT-SIPI-SIPI IPI
序列后,从实模式物理地址 StartupRoutine
处开始执行代码。StartupRoutine
必须在 1MB
地址之内,并且必须按 4KB
对齐。
2.AP 启动向量
使用函数 SendInitSipiSipi(TargetCpu, mStartup Vector)
启动一个 AP。AP 收到启动消息后会执行 mStartupVector
。
启动向量 mStartupVector
是一段代码的基址,在程序中它定义为 EFI_PHYSICAL_ADDRESS
类型的变量。地址 mStartupVector
必须在1MB
地址之内,并且必须按4KB
对齐。
(1)BSP 首先要为启动向量mStartupVector
分配内存,AllocateStartupVector
函数在物理地址为0x02000~0x7F000
的内存中找到可供使用的内存并分配给mStartupVector
使用。
(2)BSP 使用PrepareAPStartupVector
函数用于初始化启动向量mStartupVector
。该函数首先取得启动代码的地址、大小等信息,这些信息包含在 MP_ASSEMBLY_ADDRESS_MAP
类型的变量 AddressMap
中。然后根据启动代码的大小调用AllocateStartupVector(…)
为启动向量mStartupVector
分配内存。接着将启动代码复制到mStartupVector
指向的内存,并设置启动向量中的相关跳转地址,为 AP分配 GDT(全局描述符表) 和 IDT(中断描述符表) 并存入中断向量。
(3)AsmGetAddressMap
取得启动代码地址和大小。
多线程
UEFI Spec 没有提供多线程机制,如果在开发 UEFI 应用的过程中需要多线程,那么需要用户自己实现多线程库。
编写多线程库,线程的两个核心内容:
(1)线程切换时机
切换分两种,一种是主动切换,另一种是被动切换。当某个线程需要等待某个事件的发生,如网络应用等待远程服务器响应,可以主动切换到其他线程。当线程时间片用完时,需要切换到其他线程,这是被动切换。
(2)线程切换机制
要理解线程切换机制,首先要清楚每个线程执行过程中都拥有哪些资源。每个线程拥有独立的栈,还拥有执行上下文即寄存器环境。将旧的线程切换到新的线程时,需要保存旧线程的寄存器上下文,加载新线程的寄存器上下文,在切换寄存器上下文的同时发生了栈的切换。寄存器切换使用 SetJump/LongJump
函数。
1.生成线程:分配线程所需的资源,包括线程结构体及栈,以及初始化栈及线程寄存器上下文。
2.调度线程:线程的被动调度在定时器回调函数中完成。因此,在创建线程之前要先初始化Timer。
3.等待线程结束:在主线程退出之前一定要确保其他线程已经结束,并关闭定时器事件。因为主线程结束意味着程序结束,若不关闭定时器时间,会造成系统崩溃。
《UEFI原理与编程》。。。。有些东西,等用到的时候再学吧,这里大概了解一下。。。