本文是「Zephyr 内核从入门到精通」系列第 07 篇,也是内核篇开篇。基础篇解决「会用」,内核篇深入「懂原理」。
本篇是保姆级重写版:不仅讲透 Zephyr 微内核的整体架构(线程状态机、调度规则、启动流程),还给一个从零可编译、可烧录、可在串口看到现象的完整工程,亲手验证「高优先级抢占」「阻塞让出 CPU」,再带你用 Thread Analyzer 看每个线程到底用了多少栈。每一步都标清楚「写在哪、怎么跑、应该看到什么」,并在文末给一张 12 条的强化版排错表。
新手也能照着做出来;老手也能顺着源码入口深挖。建议先点赞收藏,配合源码阅读。
目录
- 一、写在前面:本篇你将亲手做出什么
- 二、内核全貌:六大组件
- 三、线程:状态机视角 + TCB(含源码字段)
- 四、调度器:优先级、抢占、时间片(调度点全列举)
- 五、启动流程:从复位到调度(源码入口逐行)
- 六、【实战一】完整可运行工程:两线程验证抢占与阻塞让出
- 七、【实战二】Thread Analyzer:看清每个线程用了多少栈
- 八、逐步拆解 + 预期输出(含串口示例)
- 九、常见报错排查(强化版 12 条)
- 十、源码阅读清单
- 十一、总结与下一篇预告
一、写在前面:本篇你将亲手做出什么
很多人看完调度器的文档,仍然回答不上来一个最朴素的问题:
「我两个线程都在
while(1),到底谁先跑、什么时候切换、k_msleep期间 CPU 去哪了?」
本篇就用两个真实工程把这件事看见:
- 实战一:一个高优先级线程 + 一个低优先级线程,靠串口打印,肉眼看到「高优先级一就绪就抢占」「调用
k_msleep阻塞时 CPU 立刻让给别人」。 - 实战二:在同一个工程上打开 Thread Analyzer,串口周期性打印每个线程的
unused / stack_size,你就再也不用「拍脑袋」给栈大小。
工程结构是标准的 Zephyr 应用三件套:
my_thread_demo/
├── CMakeLists.txt # 告诉构建系统:项目叫啥、编译哪些源文件
├── prj.conf # 内核/驱动的功能开关(Kconfig)
└── src/
└── main.c # 业务代码:main 线程 + 两个用户线程
提示:本篇所有命令在 Zephyr 现行版本(v3.x / v4.x) 下验证。板名一律用 v2 斜杠格式(例如
nrf52840dk/nrf52840、stm32f4_disco、qemu_cortex_m3)。没有开发板也没关系——文中给了 QEMU 仿真跑法,纯命令行就能看到现象。
二、内核全貌:六大组件
Zephyr 是一个微内核,核心代码集中在 kernel/ 目录。可以把内核拆成六大块:
| 组件 | 职责 | 源码位置 |
|---|---|---|
| 线程 Thread | 执行单元(CPU 的「使用者」) | kernel/thread.c |
| 调度器 Scheduler | 决定「下一刻谁占用 CPU」 | kernel/sched.c |
| 同步 Sync | 信号量 / 互斥量 / 事件 | kernel/sem.c、kernel/mutex.c |
| 数据传递 IPC | 消息队列 / FIFO / 邮箱 / 管道 | kernel/msg_q.c、kernel/queue.c |
| 时间管理 Timing | tick / 定时器 / 超时 | kernel/timeout.c、kernel/timer.c |
| 内存管理 Memory | 内存槽 / 堆 | kernel/mem_slab.c、lib/heap |
这六块里,线程 + 调度器是地基——理解了它们,其它组件(信号量、消息队列)本质都是「让线程在某个条件上阻塞/唤醒」的封装。本篇就死磕这两块。
三、线程:状态机视角 + TCB
3.1 线程的四个状态
一个线程在生命周期中,永远处于下面四种状态之一(之间会迁移):
- Ready(就绪):万事俱备,在「就绪队列」里排队,等调度器翻牌;
- Running(运行):正占着 CPU 在执行(单核同一时刻只有一个);
- Blocked(阻塞 / 等待):在等某个条件——等超时(
k_msleep)、等信号量(k_sem_take)、等队列数据等。关键:阻塞态线程完全不消耗 CPU; - Suspended(挂起):被
k_thread_suspend()显式按了「暂停键」,要靠k_thread_resume()才能回来。
状态迁移的几条主线(对照图 2 看):
新建 ──k_thread_start/创建即启动──▶ Ready
Ready ──被调度器选中──▶ Running
Running ──时间片耗尽/被高优先级抢占──▶ Ready
Running ──k_msleep / k_sem_take(等不到) / 等队列──▶ Blocked
Blocked ──超时到 / 信号量被 give / 队列有数据──▶ Ready
任意态 ──k_thread_suspend──▶ Suspended ──k_thread_resume──▶ Ready
Running ──k_thread_abort / 函数返回──▶ 终止
一句话记住 RTOS 的精髓:阻塞态线程不占 CPU,调度器立刻把 CPU 切给其它就绪线程。这正是 RTOS 与裸机「忙等
delay()死循环空转」的本质区别——delay()在烧 CPU,k_msleep()在让 CPU。
3.2 线程控制块(TCB):struct k_thread
每个线程都对应内核里的一个 struct k_thread,俗称 TCB(Thread Control Block)。它就是这个线程的「档案」,里面记着调度器需要的一切(定义见 include/zephyr/kernel/thread.h,节选):
struct k_thread {
struct _thread_base base; /* 调度核心字段:见下 */
struct _callee_saved callee_saved; /* 上下文切换要保存的寄存器 */
void *init_data;
/* ... */
#if defined(CONFIG_THREAD_NAME)
char name[CONFIG_THREAD_MAX_NAME_LEN]; /* 线程名,调试/Analyzer 用 */
#endif
struct k_thread *next_thread; /* 全局线程链表,遍历用 */
/* ... */
};
其中调度最相关的字段在 struct _thread_base 里:
struct _thread_base {
/* ... */
uint8_t thread_state; /* 当前状态位:是否就绪/阻塞/挂起/dead */
int8_t prio; /* 优先级,数值越小越高,可为负 */
/* ... 就绪队列的红黑树/链表节点等 ... */
};
栈和 TCB 是分开的两样东西:
- 栈用
K_THREAD_STACK_DEFINE(stack, size)单独定义,是线程跑函数、存局部变量的「工作内存」;- TCB(
struct k_thread)是「档案」。 线程切换(context switch)的本质,就是:保存当前线程的寄存器到它的 TCB,再从目标线程 TCB 恢复寄存器。这段是架构相关汇编,位于arch/<arch>/core/。
四、调度器:优先级、抢占、时间片
调度器核心在 kernel/sched.c。它要回答的唯一问题是:「此刻,哪个就绪线程该上 CPU?」答案永远是一句话——优先级最高的那个就绪线程。围绕这句话,有三组规则。
4.1 优先级:数值越小越高,负数=协作式
这是新手最容易反的地方,务必记牢:
数值越小 → 优先级越高(prio = -2 比 prio = 3 高)
负数 → 协作式(cooperative):不可被抢占,必须自己主动让出 CPU
非负数 → 抢占式(preemptible):可以被更高优先级线程抢占
优先级范围由两个 Kconfig 决定:
CONFIG_NUM_COOP_PRIORITIES=16 # 协作式优先级个数(对应 -16 ~ -1)
CONFIG_NUM_PREEMPT_PRIORITIES=15 # 抢占式优先级个数(对应 0 ~ 14)
- 协作式优先级实际取值范围:
-CONFIG_NUM_COOP_PRIORITIES~-1 - 抢占式优先级实际取值范围:
0~CONFIG_NUM_PREEMPT_PRIORITIES - 1
协作式 vs 抢占式怎么选? 抢占式(非负)适合绝大多数业务线程,响应及时;协作式(负数)只在你确实不希望被打断的场景用(例如一段不能被切走的处理逻辑),但用不好就会「饿死」别人。新手默认用非负优先级即可。
4.2 抢占:高优先级一就绪,立刻切
抢占式调度下,只要有一个比当前运行线程优先级更高的线程变成就绪态,调度器就会在「调度点」立刻切过去。典型场景:在 ISR 里 k_sem_give() 唤醒了一个高优先级线程,中断返回时就切给它。
4.3 时间片:同优先级线程轮流坐庄
如果两个线程优先级一样且都是抢占式,默认情况下谁先跑就一直跑(除非它自己阻塞)。打开时间片,让它们公平轮转:
CONFIG_TIMESLICING=y
CONFIG_TIMESLICE_SIZE=10 # 每个时间片 10ms
CONFIG_TIMESLICE_PRIORITY=0 # 仅对该优先级数值 >= 0 的抢占式线程生效
4.4 调度点:到底什么时刻会发生线程切换?(必背)
很多人以为「线程随时会被切走」,其实切换只发生在调度点(scheduling point)。把这几个记住,你就能预测程序行为:
- 当前线程阻塞:调用
k_msleep、k_sem_take(K_FOREVER)等不到、等队列等——主动让出; - 更高优先级线程就绪:抢占式下立即切(被抢占);
- 时间片耗尽:同优先级抢占式线程之间轮转(需开
CONFIG_TIMESLICING); - 中断返回(ISR exit):若 ISR 中唤醒了更高优先级线程,返回时切过去;
- 主动让出:调用
k_yield(),把 CPU 让给同优先级或更高的就绪线程。
反例(不会切换):一个非负优先级线程在
while(1)里纯空转,既不阻塞也不k_yield,又没开时间片、又没有更高优先级线程就绪——那它会一直霸占 CPU,比它低的线程永远跑不到。这就是「低优先级线程看起来卡死」的常见根因。
4.5 idle 线程:没人要 CPU 时谁在跑?
当没有任何用户线程就绪时,内核运行 idle 线程(系统自带)。配合 CONFIG_PM(电源管理),idle 时可进入低功耗。idle 线程优先级最低,永远不会饿死别人。
五、启动流程:从复位到调度(源码入口)
搞懂启动流程,才能回答「为什么 main() 不是程序总入口」「为什么有的外设在 main 里还没准备好」。
① 复位向量(arch/<arch>/core/reset.S 等)
│ 设置初始栈指针、清 BSS、拷 data、配基础时钟
▼
② z_cstart() ← 内核 C 语言入口,位于 kernel/init.c
│ 初始化内核基础设施
│ 按「初始化级别」依次调用所有设备/SYS_INIT 的 init 函数:
│ EARLY → PRE_KERNEL_1 → PRE_KERNEL_2 → POST_KERNEL → APPLICATION
▼
③ 启动多线程调度(switch 到第一个就绪线程)
▼
④ 运行 main 线程(z_main_thread)→ 调用你写的 main()
│ 在 main() 里 k_thread_create / K_THREAD_DEFINE 创建用户线程
▼
⑤ 调度器进入稳态:按优先级在各就绪线程之间切换
对应到源码 kernel/init.c,核心是 z_cstart() 里这段「分级初始化」:
/* kernel/init.c —— 概念示意,按级别遍历初始化项 */
void z_cstart(void)
{
/* ... 内核早期初始化 ... */
z_sys_init_run_level(INIT_LEVEL_EARLY);
z_sys_init_run_level(INIT_LEVEL_PRE_KERNEL_1);
z_sys_init_run_level(INIT_LEVEL_PRE_KERNEL_2);
/* ... 内核对象就绪后,启动调度,POST_KERNEL/APPLICATION 在多线程上下文执行 ... */
switch_to_main_thread(...); /* 切到 main 线程,main() 在这里被调用 */
}
几个必须纠正的认知误区:
main()是一个普通线程(main 线程),不是裸机意义上「程序从这里开始」。它和你创建的用户线程平起平坐,只是内核帮你提前建好了。main()返回 ≠ 程序结束。main 线程返回后,它自己终止,但其它线程照常运行。所以多线程程序里,main 经常只负责「创建线程 + 初始化」,然后就可以返回(或自己进入工作循环)。- 初始化级别决定外设就绪顺序。
DEVICE_DT_DEFINE/SYS_INIT注册的 init 函数,按PRE_KERNEL_1 → PRE_KERNEL_2 → POST_KERNEL → APPLICATION的级别、以及同级内的 priority 数值排序执行。调试「某外设在 main 里还没好」时,先查它注册在哪个级别。
六、【实战一】完整可运行工程:两线程验证抢占与阻塞让出
下面这个工程可直接复制使用。它干两件事:
thread_low(低优先级,prio=7):循环打印 "LOW running",每轮k_msleep(500);thread_high(高优先级,prio=4):循环打印 "HIGH running",每轮k_msleep(1000)。
现象预期:每当 high 线程从睡眠中醒来(变为就绪),由于它优先级更高,会立刻抢占正在跑的 low 线程;而它睡着(阻塞)时,CPU 立刻让给 low 线程。
6.1 CMakeLists.txt
位置:放在工程根目录
my_thread_demo/CMakeLists.txt
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(my_thread_demo)
target_sources(app PRIVATE src/main.c)
6.2 prj.conf
位置:工程根目录
my_thread_demo/prj.conf作用:打开打印、线程名、时间片(方便后续观察)。
# 串口打印(printk 走它)
CONFIG_PRINTK=y
CONFIG_BOOT_BANNER=y
# 给线程命名,调试/Analyzer 时能看到名字
CONFIG_THREAD_NAME=y
# 同优先级轮转(本例两线程优先级不同,先开着不影响)
CONFIG_TIMESLICING=y
CONFIG_TIMESLICE_SIZE=10
6.3 src/main.c
位置:
my_thread_demo/src/main.c
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
/* ---- 优先级定义:数值越小越高(都为非负 = 抢占式)---- */
#define PRIO_HIGH 4 /* 高优先级线程 */
#define PRIO_LOW 7 /* 低优先级线程(数值大 = 优先级低)*/
/* ---- 栈大小:先给 1024 字节,实战二会用 Analyzer 验证够不够 ---- */
#define STACKSIZE 1024
/* 为两个线程各定义一块栈 */
K_THREAD_STACK_DEFINE(stack_high, STACKSIZE);
K_THREAD_STACK_DEFINE(stack_low, STACKSIZE);
/* 两个 TCB(线程控制块)实例 */
static struct k_thread tcb_high;
static struct k_thread tcb_low;
/* 高优先级线程:醒来就抢占 low,干完活睡 1s(阻塞,让出 CPU)*/
static void thread_high(void *p1, void *p2, void *p3)
{
ARG_UNUSED(p1); ARG_UNUSED(p2); ARG_UNUSED(p3);
while (1) {
printk("[HIGH] running (prio=%d), about to sleep 1000ms\n", PRIO_HIGH);
k_msleep(1000); /* 阻塞 -> CPU 立刻让给 LOW */
}
}
/* 低优先级线程:能跑就跑,每轮睡 500ms */
static void thread_low(void *p1, void *p2, void *p3)
{
ARG_UNUSED(p1); ARG_UNUSED(p2); ARG_UNUSED(p3);
while (1) {
printk(" [LOW] running (prio=%d), sleep 500ms\n", PRIO_LOW);
k_msleep(500);
}
}
int main(void)
{
printk("=== Zephyr thread demo: preempt & block-yield ===\n");
/* 动态创建高优先级线程,K_NO_WAIT 表示创建后立即可调度 */
k_thread_create(&tcb_high, stack_high, STACKSIZE,
thread_high, NULL, NULL, NULL,
PRIO_HIGH, 0, K_NO_WAIT);
k_thread_name_set(&tcb_high, "thread_high");
k_thread_create(&tcb_low, stack_low, STACKSIZE,
thread_low, NULL, NULL, NULL,
PRIO_LOW, 0, K_NO_WAIT);
k_thread_name_set(&tcb_low, "thread_low");
/* main 是普通线程,创建完线程它就可以返回——其它线程继续跑 */
printk("main(): threads created, main thread will now return.\n");
return 0;
}
6.4 怎么编译、烧录、看输出
操作位置:在工程根目录
my_thread_demo/下打开终端(已source过 Zephyr 环境)。
方式 A:真实开发板(以 nRF52840 DK 为例,把板名换成你的):
# -p always = 每次都全新构建,避免旧缓存坑人
west build -p always -b nrf52840dk/nrf52840 .
west flash
烧录后用串口工具(PuTTY / 串口助手 / west espressif monitor 等,波特率通常 115200)连上板子的调试串口。
方式 B:没有开发板,用 QEMU 仿真(纯命令行,强烈推荐先用它验证逻辑):
west build -p always -b qemu_cortex_m3 .
west build -t run
# 输出直接打印在当前终端,Ctrl+A 然后 X 退出 QEMU
七、【实战二】Thread Analyzer:看清每个线程用了多少栈
栈给小了,程序会随机崩溃——崩在哪、什么时候崩全凭运气,极难定位。Thread Analyzer 能周期性打印每个线程「用了多少 / 总共多少」,让你心里有数。
7.1 在 prj.conf 追加配置
位置:还是同一个工程的
prj.conf,在实战一基础上追加下面几行即可:
# ===== Thread Analyzer =====
CONFIG_THREAD_ANALYZER=y
CONFIG_THREAD_ANALYZER_AUTO=y # 自动起一个线程周期打印,无需自己调用
CONFIG_THREAD_ANALYZER_AUTO_INTERVAL=5 # 每 5 秒打印一次
CONFIG_THREAD_ANALYZER_USE_PRINTK=y # 用 printk 输出(最省事)
CONFIG_THREAD_NAME=y # 打印里带线程名(实战一已开)
# 统计栈用量需要内核记录「栈水位」
CONFIG_INIT_STACKS=y
CONFIG_THREAD_STACK_INFO=y
说明:
CONFIG_THREAD_ANALYZER_AUTO=y会自动创建一个分析线程,按AUTO_INTERVAL周期打印,你不用在代码里调用任何函数。如果想手动控制,可改用thread_analyzer_print()。
7.2 重新编译运行
west build -p always -b qemu_cortex_m3 .
west build -t run
八、逐步拆解 + 预期输出
把上面两个实战「一步步」拆开,每步说清做什么 / 为什么 / 预期看到什么。
步骤 1:先跑实战一,看抢占与让出
- 做什么:用 6.4 的命令编译运行实战一工程。
- 为什么:先不上 Analyzer,专注观察调度行为,干扰最少。
- 预期串口输出(节选,时间轴大致如下):
*** Booting Zephyr OS build vX.X.X ***
=== Zephyr thread demo: preempt & block-yield ===
[HIGH] running (prio=4), about to sleep 1000ms <- main 创建后,HIGH 优先级最高,先跑
[LOW] running (prio=7), sleep 500ms <- HIGH 睡了(阻塞),CPU 让给 LOW
main(): threads created, main thread will now return.
[LOW] running (prio=7), sleep 500ms <- HIGH 还在睡(1000ms),LOW 醒了又跑(500ms周期)
[HIGH] running (prio=4), about to sleep 1000ms <- HIGH 1000ms 到,醒来=就绪,立刻"抢占"
[LOW] running (prio=7), sleep 500ms
[LOW] running (prio=7), sleep 500ms
[HIGH] running (prio=4), about to sleep 1000ms
...
- 怎么读这段输出(核心结论):
- HIGH 先于 LOW 出现:创建时两者都就绪,HIGH 优先级高,先上 CPU;
- HIGH 一调用
k_msleep就进入 Blocked,CPU 立刻让给 LOW(验证「阻塞让出 CPU」); - LOW 周期 500ms、HIGH 周期 1000ms,所以每出现一次 HIGH,中间大约能看到两次 LOW;
- HIGH 每次睡醒都打断 LOW(即便 LOW 正想打印),这就是「高优先级抢占」。
步骤 2:把优先级故意写反,复现「高频坑」
- 做什么:把
PRIO_HIGH改成 7、PRIO_LOW改成 4,再跑。 - 为什么:亲眼看「数值越小越高」反着写会怎样。
- 预期:你以为的「高」线程反而很少被运行/被另一个抢占,行为和命名完全相反。看完记得改回来。
步骤 3:上 Thread Analyzer,看栈用量
- 做什么:按第七节追加配置,重新编译运行。
- 为什么:确认
STACKSIZE 1024到底够不够,能不能缩。 - 预期 Thread Analyzer 打印(示例行,数值随平台不同):
Thread analyze:
thread_high : STACK: unused 712 usage 312 / 1024 (30 %); CPU: 0 %
thread_low : STACK: unused 720 usage 304 / 1024 (29 %); CPU: 0 %
main : STACK: unused 880 usage 144 / 1024 (14 %);
idle : STACK: unused 220 usage 36 / 256 (14 %);
- 怎么读:
unused= 这块栈从没被用到的剩余字节数;/ 1024是stack_size;usage(或unused的反面)= 实际用到的峰值;- 看
unused别太小(比如只剩几十字节就危险了,要调大栈);也别太大(一直剩 700+,可以适当调小省 RAM)。 - 经验法则:让
unused保持在总量的 20%~40% 较稳妥。
小贴士:Analyzer 自身那个分析线程也会出现在列表里;
CPU %需要开启统计相关配置才有意义,本例聚焦栈,看 STACK 那段即可。
九、常见报错排查(强化版)
这一节是排错速查表。遇到诡异现象先来这里对号入座。
| # | 现象 / 报错 | 根本原因 | 解决办法 |
|---|---|---|---|
| 1 | 程序随机崩溃 / HardFault / 重启,没规律 | 栈溢出(STACKSIZE 给小了),踩坏了相邻内存 | 开 Thread Analyzer(第七节)看 unused,把对应线程栈调大;优先怀疑「函数里有大局部数组 / 深递归 / 大 printf」的线程 |
| 2 | 「高优先级」线程反而很少跑 / 行为和命名相反 | 优先级方向写反——数值越小才越高 | 高优先级用更小的数值(如 4 < 7);命名别误导自己 |
| 3 | 某个低优先级线程完全跑不到(像卡死) | 有个非负优先级线程在 while(1) 里纯空转,不阻塞也不 yield,霸占 CPU | 让占用线程加 k_msleep/阻塞,或 k_yield();或开 CONFIG_TIMESLICING 让同优先级轮转 |
| 4 | 在中断里调用 k_msleep / k_sem_take 后系统挂死或报错 | ISR(中断上下文)不能阻塞 | 改用「中断发信号 → 线程处理」:ISR 里只 k_sem_give(),工作线程 k_sem_take(K_FOREVER) |
| 5 | main() 一返回,以为整个程序就停了 | 误以为 main 是程序总入口 | main 只是普通线程,返回后其它线程继续;要让 main 常驻就自己写 while(1) |
| 6 | 串口完全没有输出 | 没开 CONFIG_PRINTK / 串口外设没就绪 / 波特率不对 / 接错调试口 | prj.conf 加 CONFIG_PRINTK=y;确认波特率(多为 115200)、接的是调试串口;先用 QEMU 排除硬件 |
| 7 | 协作式(负优先级)线程一直占着 CPU 不放 | 协作式线程不可被抢占,只能自己让出 | 在协作式线程里主动 k_yield() 或调用阻塞 API;非必要别用负优先级 |
| 8 | 改了 prj.conf / 代码后现象没变化 | 用了旧构建缓存 | 永远带 -p always:west build -p always -b <board> . |
| 9 | west flash 报找不到板 / probe,或 west build 报未知 board | 板名格式不对(v1 旧名) / 调试器没连 | 用 v2 斜杠格式(如 nrf52840dk/nrf52840);west boards 查可用板名;检查 J-Link/ST-Link 连接 |
| 10 | Thread Analyzer 没有任何打印 | 缺少必要 Kconfig,或没开 AUTO | 同时需要 CONFIG_THREAD_ANALYZER=y、CONFIG_THREAD_ANALYZER_AUTO=y、CONFIG_THREAD_ANALYZER_USE_PRINTK=y、CONFIG_THREAD_NAME=y |
| 11 | 两个同优先级线程,一个一直跑、另一个挨饿 | 没开时间片,先跑的线程不阻塞就一直占着 | 开 CONFIG_TIMESLICING=y + 设 CONFIG_TIMESLICE_SIZE;或让线程主动 k_msleep/k_yield |
| 12 | 多线程访问同一个全局变量,数据偶发错乱 | 临界区没保护,被切换打断了「读-改-写」 | 用 k_mutex / k_sem 保护;极短临界区可 k_sched_lock()/k_sched_unlock() 或 k_spinlock(注意锁内不能阻塞、尽量短) |
十、源码阅读清单
配合本篇,按这个顺序读源码收益最大:
kernel/init.c——z_cstart(),看启动流程与设备分级初始化顺序(对应第五节);kernel/sched.c—— 调度核心算法:怎么选下一个线程、抢占判断、时间片(对应第四节);kernel/thread.c——k_thread_create、线程生命周期与状态迁移(对应第三节);include/zephyr/kernel/thread.h——struct k_thread/struct _thread_base字段(TCB 真身);include/zephyr/kernel.h—— 线程 / 同步 / IPC 的 API 与宏(K_THREAD_DEFINE、k_sem_*等);arch/<your_arch>/core/—— 上下文切换、中断处理的架构相关汇编。
十一、总结与下一篇预告
- 内核六大组件,地基是线程 + 调度器;
- 线程以四态状态机运作(Ready/Running/Blocked/Suspended),阻塞不占 CPU 是 RTOS 高效的本质;
- 调度铁律:优先级最高的就绪线程上 CPU——数值越小越高,负数=协作式(不可抢占),非负=抢占式;切换只发生在调度点(阻塞 / 高优先级就绪 / 时间片耗尽 / 中断返回 /
k_yield); - 启动流程:复位 →
z_cstart(按级别初始化设备)→ 调度器 → main 线程;main是普通线程,返回不等于程序结束; - 实战要点:优先级方向别写反、用 Thread Analyzer 定栈大小、ISR 绝不阻塞、临界区要保护。
下一篇《Zephyr 线程模型》预告:手把手讲静态 K_THREAD_DEFINE 与动态 k_thread_create 两种创建方式的差异、线程传参(那三个 void*)、优先级与启动延迟,并把本篇的 demo 升级成「信号量同步的生产者-消费者」,写出你真正能用在项目里的多线程程序。
如果这篇帮到你,点赞 + 收藏 + 关注三连,是我继续更新内核篇最大的动力。内核相关疑问欢迎评论区交流,看到必回。