3. 内核启动与调度:tx_kernel_enter()启动流程,线程调度策略
好,咱们今天聊点实在的。ThreadX 怎么跑起来的?又是怎么决定先跑哪个任务的?这两个问题搞清楚了,后面写多线程代码心里就有底了。
3.1 内核启动:tx_kernel_enter() 到底干了什么
很多初学者拿到 ThreadX 的 demo,上来就找 main 函数。结果发现 main 里就干了两件事:初始化硬件、调用 tx_kernel_enter()。然后呢?然后就没有然后了——main 函数再也没回来过。
我刚开始接触 ThreadX 时也觉得奇怪:这函数进去就不出来了?后来扒了一下源码才明白,tx_kernel_enter() 本质上是一个「有去无回」的入口。它把 CPU 的控制权彻底交给了内核调度器。
3.1.1 启动流程拆解
咱们把 tx_kernel_enter() 的启动流程拆成几步来看:
- 关中断 —— 内核启动期间不允许任何中断打扰,这是规矩。
- 初始化系统时钟 —— 配置定时器,为时间片轮转和延时服务做准备。
- 初始化就绪队列 —— 把所有线程的优先级位图清空,准备迎接线程加入。
- 调用
tx_application_define()—— 这是用户自己写的函数,在里面创建线程、队列、信号量等资源。 - 启动第一个线程 —— 从就绪队列中挑出优先级最高的线程,恢复它的上下文,然后跳转执行。
关键点:tx_application_define() 是在内核启动过程中被调用的。这意味着你在里面创建的所有对象,都会在调度开始前就绪。我曾经见过有人在这个函数里做延时操作,结果系统直接卡死——因为调度器还没开始工作呢。
下面这张图把整个流程串起来了:
3.1.2 启动代码示例
一个典型的 ThreadX 工程入口长这样:
/* 硬件初始化 */
void system_setup(void) {
/* 配置时钟、GPIO、UART 等 */
}
/* 用户定义函数:创建所有线程和资源 */
void tx_application_define(void *first_unused_memory) {
/* 创建线程 */
tx_thread_create(&thread_led, "LED Thread",
thread_led_entry, 0,
stack_led, STACK_SIZE,
5, 5, 1, TX_AUTO_START);
tx_thread_create(&thread_uart, "UART Thread",
thread_uart_entry, 0,
stack_uart, STACK_SIZE,
8, 8, 1, TX_AUTO_START);
}
/* 主函数 */
int main(void) {
system_setup(); /* 硬件初始化 */
tx_kernel_enter(); /* 进入内核,永不返回 */
return 0; /* 这行永远不会执行到 */
}
个人习惯:我一般会在 tx_application_define() 里先创建调试用的串口线程,优先级设得最低。这样万一系统跑飞了,还能通过串口打印点信息出来。你想想看,要是连调试通道都没开,出了问题只能干瞪眼。
3.2 线程调度策略:谁先跑?跑多久?
调度器启动后,就面临一个核心问题:这么多线程,先跑谁? ThreadX 的调度策略可以概括为两句话:优先级高的先跑,同优先级的轮流跑。
3.2.1 抢占式调度
抢占式调度,说白了就是「后来者可以抢跑」。当一个高优先级线程就绪时,它会立刻打断当前正在运行的低优先级线程。
举个例子:
- 线程 A 优先级 10,正在运行
- 线程 B 优先级 5(数值越小优先级越高),因为某个事件就绪了
- 调度器检测到 B 的优先级更高,立即保存 A 的上下文,切换到 B
- 等 B 运行完毕或阻塞,再恢复 A 继续跑
我在项目中遇到过一个问题:一个低优先级的线程正在操作共享缓冲区,结果被高优先级线程抢占了。高优先级线程也去操作同一个缓冲区,数据就乱套了。嗯,这就是典型的「优先级反转 + 临界区保护不当」问题。后面讲互斥量时会细说。
3.2.2 时间片轮转
同优先级的线程怎么处理?ThreadX 用的是时间片轮转。每个线程可以指定一个时间片(以系统时钟 tick 为单位),到期后自动切换到下一个同优先级的就绪线程。
| 概念 | 说明 | 配置方式 |
|---|---|---|
| 时间片 | 线程一次连续运行的最大 tick 数 | tx_thread_create() 的 time_slice 参数 |
| 默认值 | 0 表示不使用时间片(一直运行直到被抢占或阻塞) | 由用户指定 |
| 轮转触发 | 同优先级队列中,当前线程时间片耗尽,切换到下一个 | 自动触发 |
注意:时间片轮转只对同优先级线程有效。如果系统里所有线程优先级都不同,时间片设置就没意义了——高优先级线程会一直占着 CPU,低优先级的根本轮不到。
3.2.3 调度策略对比
| 调度策略 | 触发条件 | 适用场景 | 潜在问题 |
|---|---|---|---|
| 抢占式 | 高优先级线程就绪 | 实时性要求高的任务(如中断响应) | 优先级反转、临界区竞争 |
| 时间片轮转 | 同优先级线程时间片耗尽 | 多个同等重要的任务轮流执行 | 时间片设置不合理导致频繁切换 |
| 协作式(ThreadX 也支持) | 线程主动放弃 CPU | 简单系统、避免竞态 | 线程「霸占」CPU 导致其他线程饿死 |
3.3 调度器内部机制:就绪队列与位图
ThreadX 的调度器为什么这么快?因为它用了一个很巧妙的数据结构——优先级位图。
每个优先级对应一个 bit,32 个优先级就用 32 个 bit。哪个优先级上有就绪线程,对应的 bit 就置 1。调度器找最高优先级线程时,只需要做一次位运算就能找到第一个置 1 的 bit。
我当年第一次看到这个设计时,忍不住拍了一下大腿——这比遍历链表快太多了!在资源受限的 MCU 上,这种 O(1) 复杂度的调度算法简直是救命稻草。
3.3.1 调度决策流程
- 系统时钟 tick 中断触发
- 中断服务程序调用调度器入口
- 调度器扫描优先级位图,找到最高优先级的就绪线程
- 如果当前线程优先级不是最高,保存当前线程上下文
- 恢复目标线程的上下文
- 跳转到目标线程继续执行
避坑指南:我曾经在一个项目中把线程优先级设得过于密集(比如 1、2、3、4 各一个线程),结果高优先级的线程稍微一忙,低优先级的就饿死了。后来我调整了策略:优先级跨度拉大,关键任务用高优先级,普通任务用中优先级,后台任务用低优先级。这样调度器的负担也轻了。
3.4 实际项目中的调度策略选择
说了这么多理论,咱们聊聊实际项目中怎么选。
- 硬实时任务(比如电机控制、PWM 输出):用最高优先级,抢占式调度,时间片设为 0(一直运行直到阻塞)。
- 通信任务(比如 UART 接收、CAN 报文处理):中高优先级,时间片可以设小一点,保证多个通信通道公平。
- 人机交互(比如按键扫描、LCD 刷新):中低优先级,时间片适中,别让界面卡顿就行。
- 后台任务(比如日志记录、状态统计):最低优先级,时间片设大一点,有空就跑跑。
嗯,说白了就是一句话:紧急的优先级高,不紧急的优先级低,同等重要的用时间片轮转。