建议阅读本文前,先阅读下面这篇文章:
1、概述
关于多核启动的基本机制,前文中已经进行了较为详细的分析,这里不再展开。本篇文章的重点在于:在操作系统启动完成后,多核之间是如何进行协同工作的,以及各个 CPU 是如何进入多任务调度状态的。
具体来说,我们将重点关注以下问题:
- 主核如何完成系统初始化并进入调度
- 从核如何被唤醒并参与系统运行
- 多核之间如何进行同步与协作
以下内容均基于 ARMv7 架构进行说明。
2、主核启动(单核阶段)
2.1 startup.S
SylixOS 镜像的入口通常为 startup.S(由链接脚本指定)。该阶段运行在汇编环境中,主要完成系统最早期的初始化工作,包括:
- 定义异常/中断向量表
- 关 CPU 中断(IRQ、FIQ)
- 此时中断系统尚未初始化,为避免异常中断导致系统进入不可控状态,需要提前关闭
- 设置堆栈空间
- 后续将进入 C 语言环境执行,因此必须先建立基础栈环境
- 清 BSS 段
- 关闭 D-Cache/I-Cache (回写、无效)
- 无效并关闭分支预测
- 关闭 MMU(无效 TLB)
- 该步骤主要是为了避免 BootLoader(如 U-Boot)中已开启的 Cache / MMU 对当前系统造成影响,从而引发一致性问题
- 调用
bspInit,进入 C 语言初始化阶段
2.2 设置内核启动参数
c
ULONG API_KernelStartParam(CPCHAR pcParam);
函数 API_KernelStartParam 原型分析:
- 此函数成功返回
ERROR_NONE,失败返回错误号; - 参数
pcParam是启动参数,是以空格分开的一个字符串列表。
2.3 启动内核
启动内核调用的是 API_KernelStart。API_KernelStart 是一个宏,内容如下:
c
#define API_KernelStart API_KernelPrimaryStart
它对应内核里的 API_KernelPrimaryStart 函数。此函数原型如下:
c
VOID API_KernelPrimaryStart (PKERNEL_START_ROUTINE pfuncStartHook,
PVOID pvKernelHeapMem,
size_t stKernelHeapSize,
PVOID pvSystemHeapMem,
size_t stSystemHeapSize);
函数 API_KernelPrimaryStart 原型分析:
- 此函数没有返回值;
- 参数 pfuncStartHook 是系统启动中的用户回调;
- 参数 pvKernelHeapMem 是内核堆内存首地址;
- 参数 stKernelHeapSize 是内核堆大小;
- 参数 pvSystemHeapMem 是系统堆内存首地址;
- 参数 stSystemHeapSize 是系统堆大小。
✔️ 内核启动流程
API_KernelPrimaryStart 函数是系统内核的入口,只允许系统逻辑主核调用,其主要流程如下:
- 初始化内核底层组件
- 内存管理、消息队列、调度器等
- 调用
bspIntInit进行中断初始化 - 初始化内核高层组件
- 创建空闲任务、ITimer 任务、中断延迟队列、总线系统等
- ⚠️ 注意:此阶段仅创建任务,不会立即调度执行
- 调用
pfuncStartHook用户回调函数 - 调用
_KernelBootSecondary- 通知从核可以启动(通过共享变量实现)(这里只是告知从核可以启动,并不是启动从核)
- 调用
_KernelPrimaryEntry接口- 调用
bspTickInit进行系统 Tick 初始化 - 激活当前 CPU、使能调度
- 调用
archTaskCtxStart,使得系统的主核 (负责初始化的核) 进入多任务状态
- 调用
此时, SylixOS 单核启动的流程基本完成
拓展:实际上,操作系统开始运行第一个任务时,当前核 CPU 中断就会被打开。archTaskCtxCreate 创建任务上下文时,默认使能 IRQ、FIQ 中断。所以当 archTaskCtxStart 启动任务时,就会开中断
✔️ 用户启动回调
mini2440 的 BSP 里实现的回调参数为 usrStartup。其调用如下:
c
API_KernelStart(usrStartup,
(PVOID)&__heap_start,
(size_t)&__heap_end - (size_t)&__heap_start,
LW_NULL, 0);
usrStartup 函数会初始化应用相关的组件,并且创建操作系统需要执行的第一个任务 halBootThread。
c
static VOID usrStartup (VOID)
{
LW_CLASS_THREADATTR threadattr;
......
#if LW_CFG_VMM_EN > 0
halVmmInit();
#endif /* LW_CFG_VMM_EN > 0 */
#if LW_CFG_CACHE_EN > 0
halCacheInit();
#endif /* LW_CFG_CACHE_EN > 0 */
API_ThreadAttrBuild(&threadattr,
__LW_THREAD_BOOT_STK_SIZE,
LW_PRIO_CRITICAL,
LW_OPTION_THREAD_STK_CHK,
LW_NULL);
API_ThreadCreate("t_boot",
(PTHREAD_START_ROUTINE)halBootThread,
&threadattr,
LW_NULL); /* Create boot thread */
}
✔️ 第一个任务:halBootThread
halBootThread 是系统启动后的第一个实际运行任务,主要负责:
- 初始化各种设备(EMMC、网卡等)驱动、总线(SPI、I2C 等)驱动
- 初始化日志系统、符号、动态装载器等
- 调用 PSCI 接口启动其他 CPU
✔️ 关于 VMM / Cache 初始化时机
VMM 和 Cache 的初始化并未放在 halBootThread 中,而是在更早阶段完成,原因在于:
线程创建后并不会立即调度执行
而 Cache / MMU 越早启用,对系统整体性能越有利。
3、从核启动
从核通常通过 PSCI 接口启动,并传入启动入口地址(通常与主核相同,即 startup.S)。
✔️ 启动流程
从核启动后流程如下:
- 从 startup.S 进入
- 判断当前 CPU 非主核,跳转至从核入口
- 关闭 Cache / MMU / 分支预测
- 跳转至 C 函数
halSecondaryCpuMain
halSecondaryCpuMain 函数一般只需调用从核系统内核入口函数 API_KernelSecondaryStart 即可。此函数原型如下:
c
VOID API_KernelSecondaryStart(PKERNEL_START_ROUTINE pStartHook);
函数原型分析:
- 此函数没有返回值;
- 参数 pStartHook 是从核的用户回调函数,用于初始化本 CPU 基本资源,例如 MMU, CACHE, FPU 等
c
#define KN_SECONDARY_WAIT() _K_ulSecondaryHold != 0xdeadbeef
VOID API_KernelSecondaryStart (PKERNEL_START_ROUTINE pfuncStartHook)
{
KN_SMP_MB();
while (KN_SECONDARY_WAIT()) {
LW_SPINLOCK_DELAY(); /* 短延迟并释放总线 */
}
_KernelSecondaryLowLevelInit(); /* 从核底层初始化 */
_DebugHandle(__LOGMESSAGE_LEVEL, "kernel secondary cpu usrStartup...\r\n");
if (pfuncStartHook) { /* 用户是否要求需要初始化 */
pfuncStartHook(); /* 用户系统初始化 */
}
_KernelSecondaryCoreStartup(LW_CPU_GET_CUR()); /* 主核初始化完毕直接启动多任务*/
}
✔️ 主从核同步启动
API_KernelSecondaryStart 在执行过程中,会循环等待主核的启动通知(第一次等待):
c
检测 _K_ulSecondaryHold 变量
只有当主核完成关键初始化并释放该变量后,从核才继续执行。
✔️ 执行用户回调
c
VOID API_KernelSecondaryStart (PKERNEL_START_ROUTINE pfuncStartHook)
{
......
if (pfuncStartHook) { /* 用户是否要求需要初始化 */
pfuncStartHook(); /* 用户系统初始化 */
}
......
}
✔️ 进入调度
从核最终调用:
c
static VOID _KernelSecondaryCoreStartup (PLW_CLASS_CPU pcpuCur)
- 进一步等待主核运行结束(第二次等待)
- 激活当前 CPU、使能调度
- 调用
archTaskCtxStart,使得当前核进入多任务状态
在 SylixOS 里,不管是主核还是从核,当他们都进入多任务状态的时候,这两者属于对等关系,不分主从。此时,整个 SylixOS 多核系统启动完成。
3.1 同步
在多核启动过程中,一个关键问题是:各 CPU 之间的启动同步与数据一致性保证。
✔️ 启动同步(Boot Synchronization)
主核在 halBootThread 中通过 API_CpuUp 启动从核后,通常需要等待所有从核完成初始化。
在多核系统中,必须存在一个统一的"同步点",以保证:
- 所有 CPU 在一致的状态下进入调度
- 避免部分 CPU 过早运行导致资源访问异常
这一过程在系统设计中通常称为:
✅ CPU 启动同步(CPU Boot Synchronization)
或
✅ 多核启动屏障(Boot Barrier)
✔️ Cache 一致性问题(Cache Coherency)
由于每个 CPU 都具有独立的 L1 Cache,在多核启动过程中可能出现数据不一致问题。
因此,在关键同步点通常需要:
- 执行 Cache 清理(clean / invalidate)
- 确保各 CPU 看到一致的内存视图
SylixOS 中实现了 API_CacheBarrier 接口,用来多核同步 CACHE。
c
LW_API
INT API_CacheBarrier (VOIDFUNCPTR pfuncHook,
PVOID pvArg,
size_t stSize,
const PLW_CLASS_CPUSET pcpuset)
{
BOOL bLock;
ULONG i, ulCPUId, ulNumChk;
PLW_CLASS_CPU pcpu, pcpuCur;
if (!pcpuset || !pfuncHook) {
_ErrorHandle(EINVAL);
return (PX_ERROR);
}
if (LW_CPU_GET_CUR_NESTING()) { /* 不能在中断中调用 */
_ErrorHandle(ERROR_KERNEL_IN_ISR);
return (PX_ERROR);
}
ulNumChk = ((ULONG)stSize << 3);
ulNumChk = (ulNumChk > LW_NCPUS) ? LW_NCPUS : ulNumChk;
bLock = __SMP_CPU_LOCK();
ulCPUId = LW_CPU_GET_CUR_ID();
pcpuCur = LW_CPU_GET(ulCPUId);
pcpuCur->CPU_bCacheBarStart = LW_TRUE; /* 设置当前核起始同步点 */
pcpuCur->CPU_bCacheBarEnd = LW_FALSE; /* 未开始第二阶段同步 */
KN_SMP_MB();
for (i = 0; i < ulNumChk; i++) {
if (LW_CPU_ISSET(i, pcpuset)) {
pcpu = LW_CPU_GET(i);
while (!pcpu->CPU_bCacheBarStart); /* 自旋等待其他 CPU */
}
}
/* 回调函数通常都是刷 cache */
pfuncHook(pvArg); /* 执行回调函数 */
pcpuCur->CPU_bCacheBarEnd = LW_TRUE; /* 设置当前核结束同步点 */
KN_SMP_MB();
for (i = 0; i < ulNumChk; i++) {
if (LW_CPU_ISSET(i, pcpuset)) {
pcpu = LW_CPU_GET(i);
while (!pcpu->CPU_bCacheBarEnd); /* 自旋等待其他 CPU */
}
}
__SMP_CPU_UNLOCK(bLock);
return (ERROR_NONE);
}