线程间通信_信号

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 1 Likes: 0 Comments: 0

261.2.12线程间通信_信号

// ==================== applications/signal_test.c ==================== // 本段是课程复制到工程并实际运行的信号示例业务代码
#include <rtthread.h> // 引入 RT-Thread 的线程、信号、延时、内核打印和 MSH 导出接口
#define THREAD_PRIORITY         25 // 定义示例线程优先级为 25;数值越小,线程优先级越高
#define THREAD_STACK_SIZE       512 // 定义示例线程栈大小为 512 字节
#define THREAD_TIMESLICE        5 // 定义同优先级线程轮转时的时间片为 5 个 OS tick
static rt_thread_t tid1 = RT_NULL; // 定义线程 1 的动态线程句柄并初始化为空
void thread1_signal_handler(int sig) // 定义线程 1 的信号处理函数,参数是实际收到的信号编号
{ // thread1_signal_handler 函数体开始
    rt_kprintf("thread1 received signal %d\n", sig); // 打印收到信号的线程名称和信号编号
} // thread1_signal_handler 函数体结束
static void thread1_entry(void *parameter) // 定义线程 1 的入口函数
{ // thread1_entry 函数体开始
    int cnt = 0; // 初始化循环计数器
    rt_signal_install(SIGUSR1, thread1_signal_handler); // 为当前线程的 SIGUSR1 安装指定处理函数
    rt_signal_unmask(SIGUSR1); // 解除当前线程对 SIGUSR1 的屏蔽,使该信号可以被递送
    while (cnt < 10) // 让线程主体循环执行 10 次
    { // 计数循环体开始
        rt_kprintf("thread1 count : %d\n", cnt); // 打印本轮计数值,依次为 0~9
        cnt++; // 计数值加 1,为下一轮打印做准备
        rt_thread_mdelay(100); // 让线程延时约 100 ms,使调度和信号插入过程便于观察
    } // 计数循环体结束
} // thread1_entry 返回,线程 1 的任务执行完毕
int signal_sample(void) // 定义可由 MSH 命令调用的信号示例入口函数
{ // signal_sample 函数体开始
    tid1 = rt_thread_create("thread1", // 动态创建名为 thread1 的线程,并把句柄保存到 tid1
                            thread1_entry, RT_NULL, // 指定入口函数,不向入口函数传递参数
                            THREAD_STACK_SIZE, // 指定线程栈大小为 512 字节
                            THREAD_PRIORITY, THREAD_TIMESLICE); // 指定优先级为 25、时间片为 5 tick
    if (tid1 != RT_NULL) // 仅在线程创建成功、句柄非空时执行启动操作
        rt_thread_startup(tid1); // 启动线程 1,使其安装信号处理函数并开始计数
    rt_thread_mdelay(300); // 让当前执行 signal_sample 的 MSH 线程延时约 300 ms
    rt_thread_kill(tid1, SIGUSR1); // 向线程 1 发送 SIGUSR1;这里的 kill 表示发信号,不等于直接杀死线程
    return 0; // 向 MSH 返回 0,表示示例命令执行完毕
} // signal_sample 函数体结束
MSH_CMD_EXPORT(signal_sample, signal sample); // 把 signal_sample 注册为同名 MSH 命令,并提供帮助文本
// ==================== applications/main.c ==================== // 本段是课程最终工程中的主线程业务代码
#include <rtthread.h> // 引入 RT-Thread 的主线程和毫秒延时接口
#define DBG_TAG "main" // 设置 rtdbg 日志标签为 main
#define DBG_LVL DBG_LOG // 设置 rtdbg 日志级别
#include <rtdbg.h> // 引入 RT-Thread 调试日志宏
int main(void) // RT-Thread 用户主线程入口函数
{ // main 函数体开始
    int count = 1; // 初始化课程用于维持主线程循环的计数变量
    while (count++) // 课程用自增变量维持主线程循环,通常不会主动退出
    { // 主线程循环体开始
        //LOG_D("Hello RT-Thread!"); // 课程把周期日志注释掉,避免干扰信号实验输出
        rt_thread_mdelay(1000); // 主线程每次延时约 1000 ms,让其他线程和 shell 正常运行
    } // 主线程循环体结束
    return RT_EOK; // 理论上的正常返回值;本例通常不会执行到这里
} // main 函数体结束

源码说明: 上方依次收录课程配套 11SIGNAL 工程最终的 applications/signal_test.c 与 applications/main.c 业务代码。原文件的版权头和说明性注释被替换成逐行初学者注释,宏、变量、函数、参数、调用顺序、字符串和被注释掉的 LOG_D 均保持课程原样;没有把 RT-Thread 的信号、线程或 MSH 库实现抄入笔记。

一、信号

信号(signal)是一种异步通知机制:发送方把一个信号编号送给目标线程,内核把它记为待处理状态,并在条件允许时让目标线程执行与该编号关联的处理函数。它常用于异常通知、紧急状态和“不方便让线程一直轮询”的事件。

课程把信号类比为“软中断”,这个比喻有助于入门,但不能把两者完全等同:

对比项硬件中断RT-Thread 信号
触发来源外设、引脚、定时器等硬件线程调用 rt_thread_kill() 等软件接口
分发依据中断号和中断向量表信号编号和目标线程的信号处理表
执行上下文硬件异常/中断上下文目标线程上下文,内核为其安排信号处理流程
调度影响通常立即打断普通线程仍受内核状态和线程调度影响,可唤醒挂起的目标线程
栈使用体系结构规定的中断栈或当前栈会占用目标线程额外的栈空间

因此,thread1_signal_handler() 可以借“中断处理函数”帮助理解,但它不是 STM32 NVIC 中登记的硬件 ISR。信号处理函数会异步插入目标线程的正常执行流程,仍应保持简短,并谨慎访问共享状态或不可重入资源。

“异步”也不等于目标线程永远不会阻塞。它表示发送方无需与接收方同时停在一对收发接口上:

  • 目标线程正在运行或就绪时,内核安排它进入信号处理流程。
  • 目标线程处于挂起状态时,可被信号唤醒并处理通知。
  • 信号被屏蔽时先保持 pending,解除屏蔽后才有机会递送。

信号更像“某类事件已经发生”的通知,而不是数据容器。本例处理函数只收到整数信号编号,没有像消息队列那样收到一份结构体数据。RT-Thread 5.0.2 用位图记录 pending 信号;同一编号尚未处理时再次发送,会更新已有信号信息而不是累计多个独立计数。因此,需要可靠保存每次事件或传输数据时,应使用消息队列、邮箱、计数信号量等更合适的机制。

RT-Thread 信号常见操作包括:

操作接口作用
安装处理函数rt_signal_install()建立“信号编号 → 处理函数”的关系
屏蔽信号rt_signal_mask()暂不向当前线程递送指定信号
解除屏蔽rt_signal_unmask()允许当前线程处理指定信号,并检查待处理信号
发送信号rt_thread_kill()向指定线程投递某个信号编号
同步等待rt_signal_wait()主动等待信号集合中的某个信号或等待超时

1.1 信号安装

课程没有从零编写信号例程,而是从 RT-Thread 官方线程间通信文档复制示例,保存为 applications/signal_test.c。这符合本节重点:先理解信号的使用链路,再考虑内核实现。

安装接口为:

rt_sighandler_t rt_signal_install(int signo,
                                  rt_sighandler_t handler);
  • signo:要安装的信号编号,本例为 SIGUSR1。
  • handler:信号到达时调用的函数,本例为 thread1_signal_handler。
  • 返回值:此前为该编号安装的处理函数;参数无效时返回 SIG_ERR。

最重要的细节是:rt_signal_install() 内部通过 rt_thread_self() 取得当前线程,所以处理函数安装到调用者自己身上。本例必须在 thread1_entry() 内调用,才能把 SIGUSR1 安装给线程 1;如果改在 signal_sample() 中调用,安装对象就会变成执行 MSH 命令的 shell 线程。

线程首次安装信号时,内核还要为该线程动态分配信号处理函数表。信号功能会增加内核代码、线程控制信息以及处理时的栈开销,资源紧张的系统不应在完全不用信号时无条件启用。

本例处理函数的签名固定为:

void thread1_signal_handler(int sig);

其中 sig 是实际递送的编号。本工程所采用的头文件配置中 SIGUSR1 的数值为 30,所以串口打印 thread1 received signal 30。程序逻辑应使用宏名而不是直接写 30,因为不同平台或 C 库的信号编号可能不同。

安装处理函数还不够,源码随后调用:

rt_signal_unmask(SIGUSR1);

它把当前线程对 SIGUSR1 的接收状态设为允许。若已有一个同号信号处于 pending 状态,解除屏蔽时内核也会尝试递送它。课程把 unmask 类比为“开中断使能”便于理解,但它只改变该线程的软件信号掩码,不会操作 STM32 的 NVIC 或外设中断位。

1.2 配置界面打开信号

课程新建 11SIGNAL 工程,把示例文件加入工程后首次编译出现信号接口未定义,是因为新工程默认没有启用信号组件。课程在 RT-Thread Studio 的 RT-Thread Settings 中搜索 signal,打开“Enable signals/使能信号”,保存配置后重新构建。

最终配套工程的 .config 中可以确认:

CONFIG_RT_USING_SIGNALS=y
CONFIG_RT_VER_NUM=0x50002
CONFIG_RT_TICK_PER_SECOND=1000

它们分别表示启用信号、RT-Thread 版本为 5.0.2、系统每秒 1000 个 tick。配置最终还启用了动态堆、FinSH/MSH 与 UART1 控制台。

原稿中“开启信号会同时添加信号量、互斥量、事件、邮箱和消息队列”不准确。这些功能只是配置界面中相邻的独立内核选项;打开信号对应的是定义 RT_USING_SIGNALS 并纳入信号相关代码,不等于自动开启所有其他 IPC 机制。

若配置已保存但接口仍报错,应依次检查:

  1. .config 或生成的 rtconfig.h 中是否确实存在 RT_USING_SIGNALS。
  2. 新建的 signal_test.c 是否已保存到工程的 applications 目录并参与构建。
  3. 配置变更后是否重新生成工程并完整构建,而不是只编译未更新的单个文件。

本节没有新增外部硬件或电路。UART1 仅作为 MSH 命令输入和打印输出通道,信号本身是纯软件机制。

1.3 信号应用案例

烧录后,main() 只执行延时循环,不会自动运行示例。下面的宏把示例注册到 MSH:

MSH_CMD_EXPORT(signal_sample, signal sample);

因此应在 msh > 提示符输入:

signal_sample

这里的实际命令是带下划线的 signal_sample,不是原稿 OCR 得到的 “signal simple”。按回车后,MSH 调用同名 C 函数。

1.3.1 例题: 信号量使用

原小标题写成“信号量使用”,但本例操作的是信号(signal),不是具有计数值的信号量(semaphore)。为保留原有标题结构不改名,这里特别澄清两者:信号通过编号异步调用处理函数;信号量通过获取/释放计数实现线程同步。

本例执行过程如下:

  1. signal_sample() 动态创建线程 1,栈为 512 字节、优先级为 25、时间片为 5 tick。
  2. 创建成功后启动线程 1;线程 1 为自己安装 SIGUSR1 处理函数并解除屏蔽。
  3. 线程 1 循环打印 count 的 0~9,每轮延时约 100 ms。
  4. 执行 signal_sample() 的 MSH 线程延时约 300 ms。
  5. rt_thread_kill(tid1, SIGUSR1) 向线程 1 发送信号。
  6. 内核把 SIGUSR1 标为线程 1 的 pending 信号,并安排线程 1 执行 thread1_signal_handler()。
  7. 处理函数打印 thread1 received signal 30,返回后线程 1 继续原来的计数流程。

rt_thread_kill() 的名字容易误导:在本例中它并不直接删除或终止 tid1,而是采用与 POSIX kill() 相似的命名,向目标线程发送指定信号。接口为:

int rt_thread_kill(rt_thread_t tid, int sig);
  • tid:目标线程句柄,本例为 tid1。
  • sig:信号编号,必须与安装和解除屏蔽时使用的编号匹配。
  • 成功返回 RT_EOK,信号编号无效时返回 -RT_EINVAL。

SIGUSR1 与 SIGUSR2 是为用户用途保留的两个信号宏。本例只安装 SIGUSR1;若发送另一个编号,就不会自动转入为 SIGUSR1 登记的处理函数。RT-Thread 还定义了其他信号编号,但它们各有既定语义,应用不应随意把所有编号都当作自由的用户事件编号。

课程演示把 rt_signal_unmask(SIGUSR1) 暂时注释后,计数仍会继续,但处理函数不再在演示期间打印。这说明线程主体运行与该信号是否允许递送是两回事。更精确地说,被屏蔽的信号可能保持 pending;如果线程以后再次解除屏蔽且仍然存活,内核会尝试处理该 pending 信号。

典型输出形如:

thread1 count : 0
thread1 count : 1
thread1 count : 2
thread1 received signal 30
thread1 count : 3
...
thread1 count : 9

处理函数插入计数输出的精确位置受调度和 tick 边界影响,不应把“必定恰好在 2 与 3 之间”当作接口保证。实验要观察的是:信号在计数线程正常运行过程中异步插入处理,之后线程继续原来的工作。

本例还有几个教学简化点:

  • 只在线程创建成功时调用 rt_thread_startup(),但 300 ms 后无条件调用 rt_thread_kill(tid1, ...)。若创建失败,tid1 仍为空,内核断言可能触发;正式代码应在失败路径直接返回。
  • 没有检查 rt_thread_startup()、rt_thread_kill() 和 rt_signal_install() 的返回结果。
  • 512 字节栈同时承担线程普通调用和信号处理额外栈帧,复杂处理函数需要重新评估栈余量。
  • 处理函数内只打印一行,适合作为机制演示;正式应用应避免在异步处理路径中执行不可控的长耗时逻辑。

1.4 消息队列源码分析

课程结尾短暂打开消息队列的内核结构,说明继续追踪会涉及消息大小、消息池、空闲链表和等待线程链表等大量实现细节,因此建议初学阶段先掌握 API 的正确使用,再逐步阅读源码。

上一节用到的 rt_mq_t 本质是指向 struct rt_messagequeue 的指针。RT-Thread 5.0.2 的控制块主要记录:

成员类别作用
parent继承 IPC 对象的公共管理信息和接收等待线程链表
msg_pool指向消息槽内存池
msg_size、max_msgs、entry记录单条上限、总槽数和当前已有消息数
msg_queue_head、msg_queue_tail维护待接收消息链表的队头和队尾
msg_queue_free维护尚未占用的空闲消息槽链表
suspend_sender_thread队列满且使用等待发送接口时,保存被挂起的发送线程

普通发送大致是“从空闲链表取槽 → 复制数据 → 接到待收队列尾部”,接收则是“从待收队列头部取槽 → 复制给接收者 → 放回空闲链表”。理解这三条链表的角色,就能解释容量、FIFO、满队列失败和阻塞唤醒;本节不需要把整份库实现抄入业务笔记。

核对依据: 本节代码、配置、命令和实验现象取自课程视频、转录文本及配套 11SIGNAL 工程;信号安装对象、掩码、pending 合并、递送上下文与接口返回值已与 RT-Thread 5.0.2 的 signal.c、rtthread.h、rtdef.h、sys/signal.h,以及官方线程间通信文档交叉核对。