3. 内核对象基础:线程、消息队列、信号量与互斥量的创建与销毁

各位同学,咱们今天聊点实在的。ThreadX 的内核对象,说白了就是 RTOS 的「手脚」和「嘴巴」。线程是干活的人,消息队列是传话的信使,信号量和互斥量是管资源的保安。你如果连这几个东西的创建和销毁都搞不清楚,后面写驱动肯定要踩坑。

我个人习惯,在项目初期就把这些对象的生命周期画清楚。什么时候创建?谁来销毁?销毁后资源能不能彻底回收?这些问题想明白了,代码的稳定性至少提升一半。

3.1 线程(Thread)的创建与销毁

线程是 RTOS 里最基本的执行单元。ThreadX 里每个线程都有自己的栈空间、优先级和时间片。

3.1.1 创建线程:tx_thread_create

这个函数的原型长这样:

UINT tx_thread_create(
    TX_THREAD *thread_ptr,      // 线程控制块指针
    CHAR *name_ptr,             // 线程名称(调试用)
    VOID (*entry_function)(ULONG), // 入口函数
    ULONG entry_input,          // 入口参数
    VOID *stack_start,          // 栈起始地址
    ULONG stack_size,           // 栈大小(字节)
    UINT priority,              // 优先级(0-31,0最高)
    UINT preempt_threshold,     // 抢占阈值
    ULONG time_slice,           // 时间片(时钟滴答数)
    UINT auto_start             // 是否自动启动
);

嗯,这里要注意几个关键点:

  • 栈大小:别抠门。我见过有人给 64 字节,结果一调用函数就栈溢出。建议至少 512 字节起步,复杂任务给 1024 以上。
  • 优先级:0 最高,31 最低。驱动任务一般给高优先级,比如 5-10。别把两个任务设成同一个优先级,除非你明确要用时间片轮转。
  • 抢占阈值:这个参数很多人忽略。它限制了任务被抢占的优先级范围。比如你设优先级 10,抢占阈值 5,那只有优先级 0-5 的任务能抢你。这在临界区优化时特别好用。
我的习惯:创建线程时,我会把 preempt_threshold 设成和 priority 一样,也就是允许所有更高优先级的任务抢占。等性能调优时再收紧这个阈值。

3.1.2 销毁线程:tx_thread_delete

销毁线程比创建更讲究。函数原型:

UINT tx_thread_delete(TX_THREAD *thread_ptr);

销毁前必须确保线程已经终止。怎么终止?有两种方式:

  1. 自然返回:线程函数执行完 return。
  2. 强制终止:调用 tx_thread_terminate 或 tx_thread_suspend。
我曾经踩过的坑:直接删除一个还在运行的线程,结果系统挂死。ThreadX 要求线程必须处于 TX_COMPLETED 或 TX_TERMINATED 状态才能删除。所以正确的流程是:先终止线程,等待它进入终止状态,再调用删除。

3.2 消息队列(Queue)的创建与销毁

消息队列是任务间通信的主力。驱动里经常用它把中断收到的数据发给处理任务。

3.2.1 创建队列:tx_queue_create

UINT tx_queue_create(
    TX_QUEUE *queue_ptr,    // 队列控制块
    CHAR *name_ptr,         // 队列名称
    UINT message_size,      // 每条消息的大小(字节)
    VOID *queue_start,      // 队列存储区起始地址
    ULONG queue_size        // 队列存储区总大小(字节)
);

这里有个容易搞混的地方:message_size 和 queue_size 的关系。队列能存放的消息数量 = queue_size / message_size。比如你设消息大小 4 字节,队列总大小 64 字节,那最多存 16 条消息。

参数 说明 常见值
message_size 每条消息的字节数 4、8、16、32
queue_size 队列存储区总大小 64、128、256、512
最大消息数 queue_size / message_size 16、32、64
避坑指南:消息大小必须是 4 的倍数。ThreadX 内部按 4 字节对齐处理。如果你传 5 字节,实际会占用 8 字节,浪费空间。

3.2.2 销毁队列:tx_queue_delete

UINT tx_queue_delete(TX_QUEUE *queue_ptr);

销毁队列前,要确保没有任务在等待队列中的消息。如果有任务在 tx_queue_receive 上阻塞,删除队列会导致这些任务返回 TX_QUEUE_ERROR。我个人习惯在删除前先清空队列,或者确保所有消费者已经停止。

3.3 信号量(Semaphore)的创建与销毁

信号量分两种:计数信号量和二值信号量。ThreadX 里用同一个 API 创建,区别在于初始计数值。

3.3.1 创建信号量:tx_semaphore_create

UINT tx_semaphore_create(
    TX_SEMAPHORE *semaphore_ptr,  // 信号量控制块
    CHAR *name_ptr,               // 信号量名称
    ULONG initial_count           // 初始计数值
);

初始计数值决定了信号量的行为:

  • initial_count = 0:二值信号量,初始不可用。常用于同步,比如中断通知任务。
  • initial_count = 1:二值信号量,初始可用。常用于互斥访问(但不如互斥量专业)。
  • initial_count > 1:计数信号量。比如管理 5 个缓冲区,初始给 5。

我记得有个项目,用信号量管理 DMA 描述符池。初始计数值设成描述符数量,每次分配一个就获取一次信号量,释放时给回。简单又高效。

3.3.2 销毁信号量:tx_semaphore_delete

UINT tx_semaphore_delete(TX_SEMAPHORE *semaphore_ptr);

销毁信号量时,如果有任务在等待,这些任务会收到 TX_SEMAPHORE_ERROR。你想想看,这可能导致任务永远等不到资源。所以销毁前最好确保没有任务在等待,或者你有明确的错误处理机制。

3.4 互斥量(Mutex)的创建与销毁

互斥量和信号量最大的区别是:互斥量有优先级继承机制,能防止优先级反转。这在驱动开发里太重要了。

3.4.1 创建互斥量:tx_mutex_create

UINT tx_mutex_create(
    TX_MUTEX *mutex_ptr,    // 互斥量控制块
    CHAR *name_ptr,         // 互斥量名称
    UINT inherit            // 是否启用优先级继承
);

inherit 参数:

  • TX_INHERIT:启用优先级继承。推荐使用。
  • TX_NO_INHERIT:不启用。省一点性能,但可能出优先级反转。
我曾经踩过的坑:在中断服务函数里调用 tx_mutex_get。互斥量不能在中断里使用,因为它的优先级继承机制依赖任务调度。中断里只能用信号量。那次排查了整整一天,最后发现是新手同事把互斥量当信号量用了。

3.4.2 销毁互斥量:tx_mutex_delete

UINT tx_mutex_delete(TX_MUTEX *mutex_ptr);

销毁互斥量的规则和信号量类似。但有一点不同:互斥量可以被同一个任务多次获取(递归持有)。销毁前要确保持有者已经释放了所有层级的锁。

3.5 内核对象生命周期管理

说了这么多创建和销毁,咱们来张图总结一下:

ThreadX 内核对象生命周期 ThreadX 内核对象生命周期管理 线程 (Thread) 消息队列 (Queue) 信号量 (Semaphore) 互斥量 (Mutex) 创建阶段:分配控制块 + 分配资源(栈/存储区) 使用阶段:获取/释放、发送/接收、等待/通知 销毁阶段:确保无等待 → 调用 delete → 回收资源 关键原则: • 谁创建,谁销毁。避免跨模块管理内核对象。 • 销毁前必须确保对象处于可销毁状态(线程已终止、队列无等待者)。 • 中断服务函数中只能使用信号量,不能使用互斥量。

这张图把四个内核对象的生命周期串起来了。你仔细看,三个阶段——创建、使用、销毁——每个阶段都有各自的注意事项。

3.6 常见错误与调试技巧

最后,分享几个我实际项目中遇到的典型问题:

错误类型 现象 根因 解决方案
栈溢出 任务跑飞、异常复位 线程栈太小 用 tx_thread_stack_error_notify 注册栈溢出回调
队列满 发送失败,数据丢失 队列容量不足或消费者处理慢 增大队列或优化消费者处理速度
死锁 两个任务互相等待对方释放资源 互斥量获取顺序不一致 固定获取顺序,或使用 tx_mutex_get 超时
优先级反转 高优先级任务被低优先级任务阻塞 互斥量未启用优先级继承 创建时使用 TX_INHERIT
调试小技巧:ThreadX 提供了 tx_thread_info_get、tx_queue_info_get 等 API,可以在运行时查看内核对象的状态。我习惯在调试串口里加一个命令,输入 "info" 就打印所有线程的状态、栈使用率和队列占用情况。这比猜问题快多了。

好了,内核对象的创建和销毁就讲到这里。这些 API 看着简单,但用好了能让你的系统稳如磐石。下次咱们聊中断管理,看看怎么把中断和这些内核对象结合起来。