任务与线程:线程定义、线程状态、线程优先级、线程栈

好,咱们进入第二个核心话题——任务与线程。说实话,很多初学者一上来就被这两个词搞晕了。我当年刚接触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可以干别的。

小技巧:栈大小怎么定?我一般先给个保守值(比如1024字节),然后通过Zephyr的栈溢出检测工具跑一遍,看实际用了多少,再调整到合适大小。

线程的五大状态

线程不是一直在跑的。它会在不同状态间切换。Zephyr里线程有五种状态,我画个表格帮你理清楚:

状态 说明 典型场景
就绪(Ready) 线程已准备好运行,等待CPU 刚创建、被唤醒后
运行(Running) 正在占用CPU执行 调度器选中它时
挂起(Suspended) 线程被暂停,不参与调度 调用k_thread_suspend()
等待(Waiting) 等待某个事件或资源 等待信号量、消息队列
终止(Terminated) 线程执行完毕或被强制结束 函数返回、调用k_thread_abort()

为什么会这样设计?你想想看,如果所有线程都在抢CPU,那系统就乱套了。状态机让每个线程知道自己该干嘛——该等的等,该跑的跑,调度器才能高效工作。

我记得有一次调试一个I2C驱动,发现某个线程一直不响应。查了半天,原来是它卡在等待一个永远不会来的信号量上。这就是典型的「等待」状态问题。

线程优先级:谁先跑?

优先级是调度器的核心依据。Zephyr支持两种调度模式:抢占式调度和协作式调度。默认是抢占式的,也就是说高优先级的线程可以打断低优先级的。

优先级数值越小,优先级越高。比如优先级0比优先级5更「高贵」。Zephyr通常把0留给空闲线程,用户线程从1开始用。

重点:Zephyr的优先级范围是0到CONFIG_NUM_PREEMPT_PRIORITIES-1,再加上CONFIG_NUM_COOP_PRIORITIES个协作优先级。协作优先级比所有抢占优先级都高,而且不会被抢占。

我建议你遵循这个原则:紧急任务用高优先级,普通任务用中低优先级。千万别把所有线程都设成同一个优先级——那样调度器就变成轮询了,实时性反而下降。

曾经有个项目,同事把三个线程都设成优先级3,结果系统响应特别慢。我一看,三个线程互相抢CPU,谁都没法及时完成。改成优先级1、3、5后,问题立刻解决了。

线程栈:别让它溢出

线程栈是每个线程私有的内存区域,用来存放局部变量、函数调用信息等。栈溢出是嵌入式开发里最常见的坑之一。

Zephyr提供了栈溢出检测机制。你可以在配置里打开CONFIG_THREAD_STACK_INFOCONFIG_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个,栈也根据实际使用量调整,系统才稳定下来。

嗯,线程这块内容不少,但核心就这些。下一节我们会讲调度器的工作原理,到时候你会更清楚优先级和状态是怎么配合的。