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_ESSENTIALK_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 实战经验:线程栈大小怎么算?

这个问题我经常被问到。说实话,没有标准答案,但有个经验公式:

  1. 先估算函数调用深度。比如你的线程调用了 5 层函数,每层大概 20 字节的局部变量,那就是 100 字节
  2. 加上中断上下文保存开销。ARM Cortex-M 大概需要 100 字节左右
  3. 加上线程控制块开销。Zephyr 的 struct k_thread 大概 200 字节
  4. 最后乘以 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:标记为关键线程。如果这个线程异常终止,系统会直接 panic
  • K_FP_REGS:需要浮点寄存器支持。如果你的线程用到了浮点运算,一定要加这个
  • K_INHERIT_PERMS:继承创建者的权限

我个人习惯:除非必要,否则 options 传 0 就行。加太多选项反而会增加上下文切换的开销。

好了,关于线程创建和管理,核心内容就这些。你想想看,其实线程就是 RTOS 的基本单元,把它的生命周期搞清楚了,后面学信号量、消息队列这些就顺了。下一节咱们聊聊线程间的同步与通信,那才是真正有意思的地方。