3.5.10 os_kernel_start 与 k_start 详解
3.5.10 os_kernel_start 与 k_start 详解
📚 本节导读
学习时长: 约 35 分钟
难度级别: ⭐⭐⭐⭐☆(中高级)
前置知识:
- 3.5.9 _k_sys_task_init 函数详解
- 3.5.5 k_sched_init 函数详解
- Cortex-M PendSV 异常机制基础知识
🎯 学习目标
- 理解
os_kernel_start与os_kernel_init的分工 - 理解 PRE_KERNEL_2 级别的作用
- 掌握
k_start的完整流程 - 理解
os_first_task_start的汇编实现 - 理解 PendSV 上下文切换的完整过程
- 理解调度器启动前后的系统状态变化
一、函数概述
1.1 函数定位
os_kernel_start 是 OneOS 调度器启动前的最后一步。它由 $Sub$$main(或 entry)调用,在 os_kernel_init 之后执行。执行完成后,系统从"内核初始化"阶段进入"多任务调度"阶段——这是一个不可逆的分水岭。
1.2 基本信息
| 属性 | 值 |
|---|---|
| 函数名 | os_kernel_start |
| 源文件 | kernel/source/os_startup.c(第 283-290 行) |
| 调用方 | $Sub$$main / entry(drivers/driver.c 第 182-183 行) |
| 调用时机 | os_kernel_init 之后 |
| 执行环境 | 中断上下文(PRIMASK=0,允许中断) |
1.3 与 os_kernel_init 的分工
| 对比维度 | os_kernel_init | os_kernel_start |
|---|---|---|
| 执行阶段 | 调度器启动前 | 调度器启动前 |
| 核心工作 | 初始化内核组件、创建三大系统任务 | 启动调度器,开始多任务 |
调用 _k_run_init_call | PRE_KERNEL_1 | PRE_KERNEL_2 |
| 返回值 | 无 | 无(永不返回) |
| 之后系统状态 | 单任务(主调用栈) | 多任务(各有独立栈) |
二、完整代码
2.1 os_kernel_start(第 283-290 行)
void os_kernel_start(void)
{
/* pre kernel 2 init call */
_k_run_init_call(OS_INIT_LEVEL_PRE_KERNEL_2);
/* start first task */
k_start();
}2.2 调用链
$Sub$$main() / entry() ← drivers/driver.c
├─ os_kernel_init() ← 初始化内核组件(3.5.1~3.5.9)
└─ os_kernel_start() ← 这里
├─ _k_run_init_call(PRE_KERNEL_2) ← 调度器前最后一级初始化
└─ k_start() ← 启动调度器
├─ g_os_next_task = g_os_high_task
└─ os_first_task_start() ← 汇编,触发首次上下文切换
└─ PendSV_Handler ← 恢复 sys_task 的栈帧三、分步详解
第一步:PRE_KERNEL_2 初始化
_k_run_init_call(OS_INIT_LEVEL_PRE_KERNEL_2);3.1 实际注册情况
在 STM32F103ZET6 项目中,PRE_KERNEL_2 级别只有标记函数,没有用户注册的初始化函数:
.init_call.2."" → pre_kernel_2_start ← 仅标记函数,直接 return OS_SUCCESS来源 project.map:
.init_call.2."" 0x080227c0 Section 8 os_startup.o(.init_call.2."")为什么只有标记函数?
PRE_KERNEL_2 是留给用户在调度器启动前执行最后一刻初始化的扩展点。如果用户需要在多任务开始前做某些操作(如配置 MPU、设置最后的外设状态),可以在此级别注册函数。
3.2 标记函数的作用
从 os_startup.c 可以看到:
static os_err_t pre_kernel_2_start(void)
{
return OS_SUCCESS;
}
OS_INIT_CALL(pre_kernel_2_start, OS_INIT_LEVEL_PRE_KERNEL_2, "");这些标记函数的作用是在链接器中创建段边界符号。_k_run_init_call 通过 gs_init_call_table 中的指针遍历函数数组,标记函数确保了数组的起始和结束边界清晰。
第二步:k_start() 启动调度器
void k_start(void)
{
#ifdef OS_USING_SMP
// ... SMP 初始化 ...
#else
g_os_next_task = g_os_high_task;
#endif
os_first_task_start();
/* Never come back. */
return;
}3.3 关键全局变量
| 变量 | 类型 | 说明 |
|---|---|---|
g_os_current_task | os_task_t * | 当前正在运行的任务,初始为 OS_NULL |
g_os_next_task | os_task_t * | 下一个要运行的任务 |
g_os_high_task | os_task_t * | 当前最高优先级任务(调度器缓存) |
3.4 g_os_high_task 指向谁?
在 k_start 调用前,g_os_high_task 已经通过 _k_sys_task_init 中被设置为 sys_task。具体过程:
_k_sys_task_init→os_task_create→_k_task_init→k_sched_readyq_put(task)→ 放入就绪队列k_sched_readyq_put内部比较优先级,如果新任务优先级最高,更新g_os_high_task- sys_task 优先级为 0(最高),所以
g_os_high_task指向 sys_task
// 就绪队列中的任务(按优先级):
// 优先级 0: sys_task ← g_os_high_task 指向这里
// 优先级 1: timer_task ← 如果启用了哈希桶定时器
// 优先级 7: recycle_task
// 优先级 31: idle_task3.5 os_first_task_start() 汇编详解
来源: arch/arm/armv7m/rvds/arch_task_switch_rvds.S
os_first_task_start PROC
; 1. 设置 PendSV 和 SysTick 优先级
LDR R0, =NVIC_SYSPRI2
LDR R1, =NVIC_PENDSV_PRI :OR: NVIC_SYSTICK_PRI
LDR.W R2, [R0,#0x00] ; 读取原值
ORR R1, R1, R2 ; 合并
STR R1, [R0] ; 写回
; 2. 清除 FPU 上下文(如有 FPU)
IF {FPU} != "SoftVFP"
MRS R2, CONTROL
BIC R2, #0x04 ; 清除 CONTROL.FPCA
MSR CONTROL, R2
ENDIF
; 3. 恢复 MSP(中断栈指针)
LDR R0, =interrupt_stack_addr
LDR R0, [R0]
MSR MSP, R0
; 4. 触发 PendSV 异常
LDR R0, =NVIC_INT_CTRL
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
; 5. 开启中断
CPSIE F
CPSIE I
; 6. 永不抵达此处
B .
ENDP分步解释:
| 步骤 | 指令 | 作用 |
|---|---|---|
| 1 | 设置 PendSV/SysTick 优先级 | 将 PendSV 和 SysTick 设为最低优先级(0xFF),确保它们不会抢占其他中断 |
| 2 | 清除 FPU 上下文 | 首次启动时不应该有 FPU 上下文,主动清除 |
| 3 | 恢复 MSP | 将中断栈指针恢复到初始值,防止栈溢出 |
| 4 | 触发 PendSV | 写 NVIC_INT_CTRL 的 bit28,触发 PendSV 异常 |
| 5 | 开启中断 | CPSIE F + CPSIE I 开启 Fault 和普通中断 |
| 6 | B . | 永不抵达——PendSV 触发后直接进入异常处理 |
为什么触发 PendSV 而不是直接跳转?
PendSV(可挂起系统调用)是 Cortex-M 专门为 RTOS 上下文切换设计的异常:
- 优先级最低(0xFF),确保不抢占其他中断
- 可以被挂起,等所有高优先级中断处理完后再执行
- 硬件自动保存/恢复部分寄存器(R0-R3, R12, LR, PC, PSR)
- 软件只需保存/恢复 R4-R11
第三步:PendSV_Handler 上下文切换
来源: arch/arm/armv7m/rvds/arch_task_switch_rvds.S
PendSV_Handler PROC
; 关中断保护上下文切换过程
MRS R12, PRIMASK
CPSID I
; 如果是首次启动(g_os_current_task == OS_NULL),跳过保存
LDR R1, =g_os_current_task
LDR R0, [R1]
CBZ R0, switch_to_task ; 首次启动时 R0 = OS_NULL
; === 保存当前任务上下文 ===
MRS R3, PSP
; 保存 FPU 寄存器(如有)
IF {FPU} != "SoftVFP"
TST LR, #0x10
VSTMFDEQ R3!, {D8 - D15}
ENDIF
STMFD R3!, {R4 - R11, LR} ; 保存 R4-R11, LR
STR R3, [R0, #0] ; 更新 task->stack_top
; 任务切换通知回调(可选)
IF :DEF:OS_TASK_SWITCH_NOTIFY
PUSH {R1, R12}
BL os_task_switch_notify
POP {R1, R12}
ENDIF
switch_to_task
; === 恢复下一个任务上下文 ===
LDR R3, =g_os_next_task
LDR R2, [R3] ; R2 = g_os_next_task
LDR R3, [R2, #0] ; R3 = task->stack_top
LDMFD R3!, {R4 - R11, LR} ; 恢复 R4-R11, LR
; 恢复 FPU 寄存器(如有)
IF {FPU} != "SoftVFP"
TST LR, #0x10
VLDMFDEQ R3!, {D8 - D15}
ENDIF
MSR PSP, R3 ; 恢复 PSP
; 更新当前任务指针
STR R2, [R1] ; g_os_current_task = g_os_next_task
; 恢复中断
MSR PRIMASK, R12
BX LR ; 异常返回,硬件恢复 R0-R3,R12,LR,PC,PSR
ENDP3.6 首次启动的特殊路径
首次调用 PendSV_Handler 时:
g_os_current_task = OS_NULL → CBZ R0, switch_to_task → 跳过保存直接跳到 switch_to_task,恢复 g_os_next_task(即 sys_task)的栈帧:
- 从
g_os_next_task->stack_top恢复 PSP - 从 PSP 弹出 R4-R11, LR
g_os_current_task = g_os_next_taskBX LR异常返回 → 硬件自动恢复 R0-R3, R12, LR, PC, PSR- PC 指向 sys_task 的入口函数
_k_sys_task_entry
3.7 后续上下文切换(正常路径)
后续任务切换时(非首次):
g_os_current_task ≠ OS_NULL → 保存当前任务栈帧 → 恢复下一个任务栈帧四、完整流程图
os_kernel_start()
│
├─ _k_run_init_call(PRE_KERNEL_2)
│ └─ pre_kernel_2_start() ← 标记函数,直接返回
│
└─ k_start()
├─ g_os_next_task = g_os_high_task (sys_task)
│
└─ os_first_task_start() ← 汇编
├─ 设置 PendSV/SysTick 优先级为最低
├─ 清除 FPU 上下文
├─ 恢复 MSP
├─ 触发 PendSV 异常
├─ CPSIE F / CPSIE I ← 开启中断
│
└─ PendSV_Handler() ← 异常入口
├─ g_os_current_task == OS_NULL? → 跳过保存
├─ 从 g_os_next_task->stack_top 恢复 PSP
├─ 弹出 R4-R11, LR
├─ g_os_current_task = g_os_next_task
└─ BX LR ← 异常返回
│
└─ 硬件恢复 R0-R3, R12, LR, PC, PSR
│
└─ PC = _k_sys_task_entry ← 开始执行 sys_task!五、在启动流程中的位置
5.1 完整启动流程
复位
↓
Reset_Handler → SystemInit → __main → $Sub$$main
↓
os_kernel_init() ← 3.5.1 ~ 3.5.9
├─ PRE_KERNEL_1 初始化
├─ 内核组件初始化(tickq, sched, timer, recycle, idle)
└─ _k_sys_task_init() → 创建 sys_task(优先级 0)
↓
os_kernel_start() ← 本节(3.5.10)
├─ PRE_KERNEL_2 初始化
└─ k_start() → 启动调度器
↓
[sys_task 开始运行] ← 调度器已启动
└─ _k_sys_task_entry()
├─ POST_KERNEL 初始化 ← 3.5.11
├─ PRE_DEVICE 初始化 ← 3.5.12
├─ DEVICE 初始化 ← 3.5.13
├─ COMPONENT 初始化 ← 3.5.14
├─ APPLICATION 初始化 ← 3.5.15
└─ app_entry() → main()5.2 调度器启动前后的系统状态变化
| 维度 | 启动前 | 启动后 |
|---|---|---|
| 执行模式 | 特权线程模式(MSP) | 特权线程模式(PSP) |
| 栈 | 主栈(MSP) | 各任务独立栈(PSP) |
| 任务调度 | 无 | 有(优先级抢占) |
g_os_current_task | OS_NULL | 指向 sys_task |
| SysTick | 已配置但未启动 | PendSV 中首次启动 |
| 中断 | PRIMASK=0 | PRIMASK=0 |
💡 本节总结
重点回顾
os_kernel_start两步走:PRE_KERNEL_2 初始化 →k_start()启动调度器- PRE_KERNEL_2 在本项目中为空:只有标记函数,是用户扩展点
k_start三步:设置g_os_next_task→os_first_task_start触发 PendSV → PendSV 恢复 sys_task 栈帧- 首次启动的特殊路径:
g_os_current_task == OS_NULL,跳过保存,直接恢复 - PendSV 优先级最低:确保不抢占其他中断,上下文切换在"安全时刻"进行
- 调度器启动是不可逆的分水岭:之后系统进入多任务调度模式
关键符号说明
| 符号 | 来源 | 说明 |
|---|---|---|
g_os_current_task | os_sched.c | 当前正在运行的任务指针 |
g_os_next_task | os_sched.c | 下一个要运行的任务指针 |
g_os_high_task | os_sched.c | 最高优先级任务缓存 |
PendSV_Handler | arch_task_switch_rvds.S | 上下文切换的汇编实现 |
os_first_task_start | arch_task_switch_rvds.S | 首次启动的汇编入口 |
下一步
接下来请学习:
- 3.5.11 POST_KERNEL 阶段详解 - sys_task 中的第一个初始化级别