任务与线程:线程定义、线程状态、线程优先级、线程栈
好,咱们进入第二个核心话题——任务与线程。说实话,很多初学者一上来就被这两个词搞晕了。我当年刚接触RTOS时也困惑过:到底什么是任务?什么是线程?在Zephyr里,它们其实是一回事。
Zephyr里没有Linux那种「进程」的概念,只有线程。每个线程就是一个独立的执行流,有自己的上下文、栈空间和优先级。说白了,线程就是RTOS里干活的最小单元。
线程的定义与创建
在Zephyr里定义一个线程,通常有两种方式:静态定义和动态创建。我个人更推荐静态定义,尤其是在资源受限的MCU上。
先看一个静态定义的例子:
#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, K_NO_WAIT);
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宏会自动分配栈空间和线程控制块。你只需要告诉它栈大小、入口函数和优先级就行。嗯,我在项目里经常看到有人把栈大小设得特别大,其实没必要——够用就好,省下的RAM可以干别的。
线程的五大状态
线程不是一直在跑的。它会在不同状态间切换。Zephyr里线程有五种状态,我画个表格帮你理清楚:
| 状态 | 说明 | 典型场景 |
|---|---|---|
| 就绪(Ready) | 线程已准备好运行,等待CPU | 刚创建、被唤醒后 |
| 运行(Running) | 正在占用CPU执行 | 调度器选中它时 |
| 挂起(Suspended) | 线程被暂停,不参与调度 | 调用k_thread_suspend() |
| 等待(Waiting) | 等待某个事件或资源 | 等待信号量、消息队列 |
| 终止(Terminated) | 线程执行完毕或被强制结束 | 函数返回、调用k_thread_abort() |
为什么会这样设计?你想想看,如果所有线程都在抢CPU,那系统就乱套了。状态机让每个线程知道自己该干嘛——该等的等,该跑的跑,调度器才能高效工作。
我记得有一次调试一个I2C驱动,发现某个线程一直不响应。查了半天,原来是它卡在等待一个永远不会来的信号量上。这就是典型的「等待」状态问题。
线程优先级:谁先跑?
优先级是调度器的核心依据。Zephyr支持两种调度模式:抢占式调度和协作式调度。默认是抢占式的,也就是说高优先级的线程可以打断低优先级的。
优先级数值越小,优先级越高。比如优先级0比优先级5更「高贵」。Zephyr通常把0留给空闲线程,用户线程从1开始用。
我建议你遵循这个原则:紧急任务用高优先级,普通任务用中低优先级。千万别把所有线程都设成同一个优先级——那样调度器就变成轮询了,实时性反而下降。
曾经有个项目,同事把三个线程都设成优先级3,结果系统响应特别慢。我一看,三个线程互相抢CPU,谁都没法及时完成。改成优先级1、3、5后,问题立刻解决了。
线程栈:别让它溢出
线程栈是每个线程私有的内存区域,用来存放局部变量、函数调用信息等。栈溢出是嵌入式开发里最常见的坑之一。
Zephyr提供了栈溢出检测机制。你可以在配置里打开CONFIG_THREAD_STACK_INFO和CONFIG_THREAD_NAME,这样就能在运行时查看每个线程的栈使用情况。
// 查看线程栈使用量
size_t unused = k_thread_stack_space_get(my_tid);
printk("Thread stack unused: %d bytes\n", unused);
如果unused接近0,说明栈快满了,赶紧加大。我一般留20%的余量,防止极端情况。
另外,Zephyr的栈定义有讲究。别直接用普通的数组,要用K_THREAD_STACK_DEFINE宏:
K_THREAD_STACK_DEFINE(my_stack_area, 1024);
这个宏会做内存对齐,保证栈的正确性。我见过有人直接用static uint8_t stack[1024],结果系统跑着跑着就崩了——就是因为没对齐。
实战建议
好了,总结一下我个人的经验:
- 线程数量别太多——每个线程都要占栈空间,MCU的RAM有限。我一般控制在10个以内。
- 优先级别乱设——高优先级线程别做耗时操作,否则低优先级线程永远没机会跑。
- 栈大小要合理——先给个保守值,跑起来后用工具检测,再调整到最优。
- 状态转换要清楚——调试时多看看线程状态,很多问题一眼就能看出来。
我曾经接手过一个项目,里面开了20多个线程,每个栈都给了2KB。结果RAM不够用,系统频繁崩溃。后来我把功能合并,线程减到8个,栈也根据实际使用量调整,系统才稳定下来。
嗯,线程这块内容不少,但核心就这些。下一节我们会讲调度器的工作原理,到时候你会更清楚优先级和状态是怎么配合的。