线程间通信_信号
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 机制。
若配置已保存但接口仍报错,应依次检查:
.config或生成的rtconfig.h中是否确实存在RT_USING_SIGNALS。- 新建的
signal_test.c是否已保存到工程的applications目录并参与构建。 - 配置变更后是否重新生成工程并完整构建,而不是只编译未更新的单个文件。
本节没有新增外部硬件或电路。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)。为保留原有标题结构不改名,这里特别澄清两者:信号通过编号异步调用处理函数;信号量通过获取/释放计数实现线程同步。
本例执行过程如下:
signal_sample()动态创建线程 1,栈为 512 字节、优先级为 25、时间片为 5 tick。- 创建成功后启动线程 1;线程 1 为自己安装
SIGUSR1处理函数并解除屏蔽。 - 线程 1 循环打印
count的 0~9,每轮延时约 100 ms。 - 执行
signal_sample()的 MSH 线程延时约 300 ms。 rt_thread_kill(tid1, SIGUSR1)向线程 1 发送信号。- 内核把
SIGUSR1标为线程 1 的 pending 信号,并安排线程 1 执行thread1_signal_handler()。 - 处理函数打印
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,以及官方线程间通信文档交叉核对。