6、信号的高级应用:实时信号的概念与特点
信号这东西,很多人学完基础就觉得够了。但说实话,标准信号那套机制,在真正的高并发、低延迟场景下,根本不够用。我当年接手一个工业控制项目,设备上报数据频繁丢失,查来查去,问题就出在标准信号排队机制上——信号丢失了。后来换成实时信号,问题才彻底解决。
这一章,咱们就聊聊实时信号。它不是什么新概念,但很多嵌入式工程师用得少,甚至不知道。你想想看,如果面试时你能把实时信号的排队机制讲清楚,面试官会怎么看你?
6.1 实时信号的概念与特点
实时信号(Realtime Signal)是 POSIX.1b 标准引入的。它解决了标准信号的两个核心痛点:信号丢失和无法携带数据。
标准信号(1~31)有个问题——如果同一个信号多次到达,内核可能只记录一次。为什么?因为标准信号用位图表示,一个位只能表示「有」或「没有」。你发 10 次 SIGUSR1,进程可能只收到 1 次。
实时信号(SIGRTMIN ~ SIGRTMAX)就不一样了。它用队列管理,每个信号都排队。你发 10 次,它就收 10 次,一个不落。
实时信号的核心特点:
- 排队机制:信号不会丢失,按发送顺序排队
- 携带数据:可以附带一个整数或指针值
- 优先级:信号编号越小,优先级越高
- 可靠交付:内核保证每个信号都被递送
我记得第一次用实时信号时,心里还犯嘀咕:这玩意儿跟标准信号到底差多少?后来做了个压力测试,标准信号在 1000 次发送中丢了 300 多次,实时信号一次没丢。嗯,差距就在这里。
6.2 实时信号的发送:sigqueue
发送实时信号,不能用 kill() 了。得用 sigqueue()。这个函数比 kill 多了一个参数——可以传数据。
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
// union sigval 定义:
union sigval {
int sival_int;
void *sival_ptr;
};
说白了,sigqueue 就是 kill 的升级版。它不仅能发信号,还能带个「小纸条」过去。
我习惯这么用:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "用法: %s <pid> <value>\n", argv[0]);
exit(1);
}
pid_t pid = atoi(argv[1]);
int val = atoi(argv[2]);
union sigval sv;
sv.sival_int = val;
if (sigqueue(pid, SIGRTMIN, sv) == -1) {
perror("sigqueue");
exit(1);
}
printf("已向进程 %d 发送实时信号,附带值: %d\n", pid, val);
return 0;
}
小提示:发送实时信号时,建议使用 SIGRTMIN + n 的形式,不要直接用数字。不同系统上 SIGRTMIN 的值可能不同,Linux 上是 34。
6.3 实时信号的接收与排队
接收端怎么处理?用 sigaction(),配合 SA_SIGINFO 标志。这样信号处理函数就能拿到发送方传来的数据。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
void handler(int sig, siginfo_t *info, void *context) {
// info->si_value 就是发送方传来的数据
printf("收到信号: %d, 附带值: %d, 发送方 PID: %d\n",
sig, info->si_value.sival_int, info->si_pid);
}
int main() {
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_sigaction = handler;
act.sa_flags = SA_SIGINFO; // 关键!必须加这个标志
// 阻塞所有实时信号,防止处理函数被重入
sigfillset(&act.sa_mask);
if (sigaction(SIGRTMIN, &act, NULL) == -1) {
perror("sigaction");
return 1;
}
printf("接收进程 PID: %d\n", getpid());
printf("等待实时信号...\n");
// 让进程一直运行
while (1) {
pause();
}
return 0;
}
这里有个坑,我踩过。如果你不加 SA_SIGINFO,那 sa_sigaction 根本不会被调用,系统会调用 sa_handler。而且你拿不到数据。我曾经因为这个 bug 调试了一下午,最后发现是标志位没设。
关于排队,内核是怎么做的?每个进程有一个实时信号队列。队列长度有限制,可以通过 /proc/sys/kernel/rtsig-max 查看。默认是 65536 个。如果队列满了,sigqueue() 会返回 EAGAIN。
注意:实时信号的排队是有上限的。如果短时间内发送大量信号,队列满了后面的信号会被丢弃。虽然概率低,但高并发场景下要考虑这个边界。
6.4 信号在 IPC 中的应用场景
信号能用来做进程间通信吗?能,但有限制。我一般只在以下场景用:
- 通知事件:比如子进程告诉父进程「我准备好了」
- 传递小数据:用实时信号带一个整数,比如文件描述符、状态码
- 控制命令:比如让进程重新加载配置、暂停、退出
但说实话,信号不适合传大量数据。你想想看,一个信号只能带一个整数或指针,能传多少东西?真要传复杂结构体,还是用共享内存或消息队列吧。
我做过一个项目,用实时信号做心跳检测。主进程每隔 1 秒给工作进程发一个实时信号,附带一个递增的序列号。工作进程收到后回复。如果连续 3 次没收到回复,主进程就认为工作进程挂了,自动拉起。这个方案简单可靠,比用 socket 轻量多了。
6.5 信号驱动 I/O
信号驱动 I/O 是个好东西。它让你不用轮询,不用 select/poll/epoll,文件描述符有数据时,内核直接发信号通知你。
怎么用?三步走:
- 设置信号处理函数
- 用
fcntl()设置文件描述符为信号驱动模式 - 指定接收信号的进程
#include <stdio.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
int fd;
void sigio_handler(int sig, siginfo_t *info, void *context) {
char buf[1024];
ssize_t n;
// 注意:信号处理函数中要小心使用非异步信号安全的函数
// read 是安全的
n = read(fd, buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
printf("收到数据: %s", buf);
}
}
int main() {
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_sigaction = sigio_handler;
act.sa_flags = SA_SIGINFO;
sigaction(SIGIO, &act, NULL);
// 打开一个文件或设备
fd = open("/dev/input/event0", O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}
// 设置当前进程为文件描述符的属主
fcntl(fd, F_SETOWN, getpid());
// 设置信号驱动 I/O 模式
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_ASYNC | O_NONBLOCK);
printf("等待输入事件...\n");
while (1) {
pause();
}
close(fd);
return 0;
}
信号驱动 I/O 适合那些「数据不频繁但实时性要求高」的场景。比如键盘输入、鼠标事件、串口数据。但要注意,信号处理函数里不能做太多事,否则会影响系统响应。
我的经验:信号驱动 I/O 在嵌入式设备上特别好用。比如一个温度传感器,可能几分钟才上报一次数据。用信号驱动,CPU 可以睡大觉,数据来了再干活。省电又高效。
6.6 SIGCHLD 信号与子进程回收
SIGCHLD 这个信号,搞 Linux 编程的应该不陌生。子进程退出时,内核会给父进程发这个信号。父进程收到后,调用 wait() 或 waitpid() 回收子进程,防止僵尸进程。
但很多人处理 SIGCHLD 时有个误区——以为只处理一次就够了。我见过一个案例,父进程 fork 了 10 个子进程,结果只回收了 3 个,剩下 7 个成了僵尸。为什么?因为 SIGCHLD 信号可能合并。
正确的做法是:在信号处理函数里循环调用 waitpid(),直到没有子进程退出为止。
#include <stdio.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
void sigchld_handler(int sig) {
int status;
pid_t pid;
// 循环回收,直到没有子进程退出
// WNOHANG 表示非阻塞
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
printf("回收子进程 %d, 退出状态: %d\n", pid, WEXITSTATUS(status));
}
}
int main() {
struct sigaction act;
act.sa_handler = sigchld_handler;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_RESTART | SA_NOCLDSTOP;
// SA_NOCLDSTOP 表示子进程暂停时不触发 SIGCHLD
sigaction(SIGCHLD, &act, NULL);
// fork 多个子进程
for (int i = 0; i < 5; i++) {
pid_t pid = fork();
if (pid == 0) {
// 子进程
printf("子进程 %d 启动\n", getpid());
sleep(2);
exit(i);
}
}
// 父进程继续做其他事
while (1) {
sleep(1);
}
return 0;
}
避坑指南:我曾经在 SIGCHLD 处理函数里只调了一次 waitpid(),结果线上服务器跑了一周后,ps 一看,好几百个僵尸进程。从那以后,我养成了「循环回收」的习惯。另外,记得加 SA_NOCLDSTOP,否则子进程被暂停也会触发信号,没必要。
6.7 定时器信号:setitimer
定时器信号,说白了就是让内核到时间了给你发个信号。Linux 提供了 setitimer() 和 alarm() 两种方式。alarm 太简单,只能设一次。setitimer 可以设三种定时器:
| 定时器类型 | 信号 | 行为 |
|---|---|---|
| ITIMER_REAL | SIGALRM | 真实时间,进程睡眠也计时 |
| ITIMER_VIRTUAL | SIGVTALRM | 用户态 CPU 时间 |
| ITIMER_PROF | SIGPROF | 用户态 + 内核态 CPU 时间 |
我平时用得最多的是 ITIMER_REAL。比如做超时检测、心跳包发送、定时任务。
#include <stdio.h>
#include <signal.h>
#include <sys/time.h>
#include <unistd.h>
#include <string.h>
void timer_handler(int sig) {
static int count = 0;
count++;
printf("定时器触发 %d 次\n", count);
}
int main() {
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_handler = timer_handler;
sigemptyset(&act.sa_mask);
sigaction(SIGALRM, &act, NULL);
struct itimerval timer;
// 初始间隔:1 秒
timer.it_value.tv_sec = 1;
timer.it_value.tv_usec = 0;
// 重复间隔:2 秒
timer.it_interval.tv_sec = 2;
timer.it_interval.tv_usec = 0;
if (setitimer(ITIMER_REAL, &timer, NULL) == -1) {
perror("setitimer");
return 1;
}
printf("定时器已启动,1 秒后首次触发,之后每 2 秒触发一次\n");
while (1) {
pause();
}
return 0;
}
这里有个细节:it_value 是第一次触发的时间,it_interval 是后续的间隔。如果 it_interval 设为 0,就变成一次性定时器。
小技巧:setitimer 的精度是微秒级,但实际精度取决于内核的时钟频率。一般嵌入式 Linux 的时钟频率是 100Hz,也就是 10 毫秒精度。如果你需要更高精度,可以考虑 POSIX 定时器 timer_create()。
6.8 信号的综合案例
说了这么多,咱们来个综合案例。这个案例模拟一个简单的任务调度器:
- 主进程管理多个工作进程
- 用实时信号给工作进程派发任务
- 用 SIGCHLD 回收子进程
- 用 setitimer 做定时心跳检测
#include <stdio.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define WORKER_NUM 3
#define HEARTBEAT_INTERVAL 3
// 工作进程函数
void worker_process(int id) {
printf("[工作进程 %d] 启动,PID: %d\n", id, getpid());
// 设置实时信号处理
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_sigaction = [](int sig, siginfo_t *info, void *ctx) {
int task_id = info->si_value.sival_int;
printf("[工作进程 %d] 收到任务: %d\n", getpid(), task_id);
// 模拟任务处理
sleep(1);
printf("[工作进程 %d] 任务 %d 完成\n", getpid(), task_id);
};
act.sa_flags = SA_SIGINFO;
sigaction(SIGRTMIN, &act, NULL);
while (1) {
pause();
}
}
// 心跳超时处理
void heartbeat_handler(int sig) {
printf("[主进程] 心跳检测...\n");
// 这里可以检查工作进程是否存活
}
int main() {
pid_t workers[WORKER_NUM];
// 设置 SIGCHLD 处理
struct sigaction chld_act;
memset(&chld_act, 0, sizeof(chld_act));
chld_act.sa_handler = [](int sig) {
int status;
pid_t pid;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
printf("[主进程] 回收子进程 %d\n", pid);
}
};
sigaction(SIGCHLD, &chld_act, NULL);
// 设置心跳定时器
struct sigaction timer_act;
memset(&timer_act, 0, sizeof(timer_act));
timer_act.sa_handler = heartbeat_handler;
sigaction(SIGALRM, &timer_act, NULL);
struct itimerval timer;
timer.it_value.tv_sec = HEARTBEAT_INTERVAL;
timer.it_value.tv_usec = 0;
timer.it_interval.tv_sec = HEARTBEAT_INTERVAL;
timer.it_interval.tv_usec = 0;
setitimer(ITIMER_REAL, &timer, NULL);
// 创建工作进程
for (int i = 0; i < WORKER_NUM; i++) {
pid_t pid = fork();
if (pid == 0) {
worker_process(i);
exit(0);
} else {
workers[i] = pid;
printf("[主进程] 创建工作进程 %d, PID: %d\n", i, pid);
}
}
// 模拟派发任务
int task_id = 0;
while (1) {
sleep(5);
printf("\n[主进程] 派发新任务...\n");
for (int i = 0; i < WORKER_NUM; i++) {
union sigval sv;
sv.sival_int = task_id++;
sigqueue(workers[i], SIGRTMIN, sv);
}
}
return 0;
}
这个案例把本章的知识点串起来了。你想想看,实际项目中,这种模式很常见——一个主控进程管理多个工作进程,用信号做通信和控制。
我当年做的一个视频监控系统,就是类似的架构。主进程负责调度,工作进程负责采集、编码、存储。用实时信号传递控制命令,用 SIGCHLD 监控子进程状态,用定时器做保活检测。这套方案跑了三年,没出过问题。
总结一下本章的核心:
- 实时信号解决了标准信号的丢失问题,支持排队和数据传递
- sigqueue 发送,sigaction 接收,别忘了 SA_SIGINFO
- 信号驱动 I/O 适合低频率、高实时性的场景
- SIGCHLD 处理要循环回收,防止僵尸进程
- setitimer 做定时器,精度微秒级,够用
信号这东西,用好了是利器,用不好是坑。多写多练,慢慢就熟了。
公众号:蓝海资料掘金营,微信deep3321