3. 线程创建与管理:k_thread_create、线程属性、线程生命周期
好,咱们今天聊聊 Zephyr 里最核心的东西——线程。说白了,一个 RTOS 干的事就是调度线程。你想想看,如果没有线程,你的单片机就只能跑一个死循环,啥也干不了。
我个人习惯把线程理解成「一个独立的任务单元」。每个线程有自己的栈、自己的优先级、自己的状态。Zephyr 里创建线程的 API 就是 k_thread_create,这个函数我用了不下几百次了,今天把它的门道给你讲透。
3.1 线程的两种创建方式
Zephyr 支持两种创建线程的方式:静态创建和动态创建。嗯,这里要注意,静态创建是 Zephyr 推荐的方式,因为嵌入式系统里资源有限,静态分配更可控。
3.1.1 静态创建(编译时分配)
用宏 K_THREAD_DEFINE 来定义。这种方式在编译时就分配好了栈空间和线程控制块。
#include <zephyr/kernel.h>
/* 线程栈大小 */
#define MY_STACK_SIZE 1024
/* 线程优先级 */
#define MY_PRIORITY 5
/* 定义线程 */
K_THREAD_DEFINE(my_tid, MY_STACK_SIZE,
my_thread_entry, NULL, NULL, NULL,
MY_PRIORITY, 0, 0);
void my_thread_entry(void *arg1, void *arg2, void *arg3)
{
while (1) {
printk("Hello from my thread!\n");
k_sleep(K_SECONDS(1));
}
}
这里 K_THREAD_DEFINE 的第一个参数是线程 ID,后面会用它来操作线程。我在项目中遇到过一个问题:栈大小设得太小,结果线程跑着跑着就栈溢出了,系统直接挂掉。所以栈大小一定要算清楚,别抠门。
3.1.2 动态创建(运行时分配)
用 k_thread_create 函数。这种方式灵活,但需要你自己管理内存。
struct k_thread my_thread;
K_THREAD_STACK_DEFINE(my_stack_area, MY_STACK_SIZE);
void create_my_thread(void)
{
k_thread_create(&my_thread, my_stack_area,
K_THREAD_STACK_SIZEOF(my_stack_area),
my_thread_entry, NULL, NULL, NULL,
MY_PRIORITY, 0, K_NO_WAIT);
}
你看,动态创建需要先声明一个 struct k_thread 变量和一个栈区域。我个人建议:除非你有特殊需求,否则优先用静态创建。为什么?因为静态创建在编译时就确定了内存布局,不容易出内存碎片的问题。
3.2 线程属性详解
创建线程时,有一堆参数要填。我刚开始学的时候也被搞晕过,咱们一个一个说。
| 参数 | 说明 | 我的建议 |
|---|---|---|
| 优先级 | 数值越小优先级越高,0 是最高 | 别设太多优先级层级,够用就行 |
| 栈大小 | 单位是字节,要包含上下文切换开销 | 留 20% 余量,别卡太死 |
| 选项 | 如 K_ESSENTIAL、K_FP_REGS 等 |
默认 0 就够用,除非你知道自己在干啥 |
| 延迟启动 | 线程创建后是否立即运行 | 用 K_NO_WAIT 立即启动 |
优先级陷阱:Zephyr 的优先级分 cooperative(协作式)和 preemptive(抢占式)。优先级小于 0 的是协作式线程,它们不会被其他线程抢占,除非主动让出 CPU。优先级大于等于 0 的是抢占式线程。
我曾经踩过一个坑:把一个关键任务设成了抢占式优先级,结果被其他线程打断了,导致数据采集出现了时间偏差。后来改成协作式优先级,问题就解决了。所以选优先级时,要想清楚你的线程需不需要被抢占。
3.3 线程生命周期
一个线程从创建到销毁,会经历几个状态。我画个图给你看:
创建 → 就绪 → 运行 → 阻塞 → 就绪 → 运行 → ... → 终止
具体来说,有这几个状态:
- 就绪(Ready):线程已经准备好运行,就等 CPU 分配时间片
- 运行(Running):正在占用 CPU 执行代码
- 阻塞(Blocked):在等某个事件,比如信号量、消息队列、延时等
- 挂起(Suspended):被
k_thread_suspend暂停了 - 终止(Terminated):线程执行完毕或被
k_thread_abort强制结束
小技巧:用 k_thread_foreach 可以遍历所有线程,查看它们的状态。调试时特别好用。
3.4 线程控制操作
创建完线程,你还需要管理它。常用的操作有这些:
/* 启动线程(如果创建时设置了延迟启动) */
k_thread_start(my_tid);
/* 暂停线程 */
k_thread_suspend(my_tid);
/* 恢复线程 */
k_thread_resume(my_tid);
/* 终止线程 */
k_thread_abort(my_tid);
/* 让线程自己退出 */
k_thread_abort(k_current_get());
嗯,这里要注意:k_thread_abort 会立即终止线程,不会做资源清理。如果你的线程申请了动态内存,最好在线程退出前自己释放掉。
警告:千万不要在中断服务函数里调用 k_thread_abort!我曾经这么干过,结果系统直接死锁了。中断里只能发信号,不能直接操作线程生命周期。
3.5 实战经验:线程栈大小怎么算?
这个问题我经常被问到。说实话,没有标准答案,但有个经验公式:
- 先估算函数调用深度。比如你的线程调用了 5 层函数,每层大概 20 字节的局部变量,那就是 100 字节
- 加上中断上下文保存开销。ARM Cortex-M 大概需要 100 字节左右
- 加上线程控制块开销。Zephyr 的
struct k_thread大概 200 字节 - 最后乘以 1.5 的安全系数
举个例子:如果你的线程逻辑比较简单,1024 字节通常够用。如果涉及浮点运算或者大数组,建议 2048 起步。
我记得有一次,一个同事把栈设成了 512 字节,结果线程跑着跑着就莫名其妙重启了。查了两天才发现是栈溢出,把线程控制块给覆盖了。从那以后,我养成了一个习惯:在调试阶段把栈设大一点,等稳定了再优化。
3.6 线程的延迟启动
有时候你不想让线程创建后立即运行,而是等某个条件满足后再启动。这时候可以用延迟启动:
K_THREAD_DEFINE(my_tid, MY_STACK_SIZE,
my_thread_entry, NULL, NULL, NULL,
MY_PRIORITY, 0, K_FOREVER);
/* 等某个条件满足后启动 */
if (some_condition) {
k_thread_start(my_tid);
}
这个特性在系统初始化时特别有用。比如你先创建所有线程,等外设初始化完成后再逐个启动。这样能避免线程在系统还没准备好时就乱跑。
3.7 线程的选项标志
创建线程时有个 options 参数,我常用的有这几个:
K_ESSENTIAL:标记为关键线程。如果这个线程异常终止,系统会直接 panicK_FP_REGS:需要浮点寄存器支持。如果你的线程用到了浮点运算,一定要加这个K_INHERIT_PERMS:继承创建者的权限
我个人习惯:除非必要,否则 options 传 0 就行。加太多选项反而会增加上下文切换的开销。
好了,关于线程创建和管理,核心内容就这些。你想想看,其实线程就是 RTOS 的基本单元,把它的生命周期搞清楚了,后面学信号量、消息队列这些就顺了。下一节咱们聊聊线程间的同步与通信,那才是真正有意思的地方。