上一篇我们用epoll搭建了高并发网络服务器。在真实嵌入式系统中,信号和定时器是驱动状态机、处理异步事件的核心机制。本篇将深入信号处理的最佳实践,并引入
signalfd将信号融入事件循环,再用timerfd实现毫秒级精确定时。所有代码均可在ARM开发板上交叉编译运行。

一、引言
在系列⑤中,我们快速接触了信号的基本用法:signal、kill、alarm。但在实际项目中,signal()函数存在不可移植性和竞态条件问题,早已被sigaction取代。此外,信号处理函数中能做的事情非常有限(只允许异步信号安全函数),这使得复杂逻辑很难编写。Linux提供了signalfd,允许将信号作为文件描述符读取,完美融入epoll/select循环。结合timerfd,我们可以用统一的事件驱动模型处理所有定时和异步事件。
二、可靠信号机制:sigaction
2.1 为什么不用signal()?
signal()在不同Unix系统上行为不一致:
- 某些系统上信号处理函数执行一次后自动恢复为默认(需重新注册)
- 某些系统上会阻塞当前信号,但不会阻塞其他信号
- 无法在信号处理期间自动屏蔽指定信号
sigaction提供了精确的控制,是POSIX推荐的标准方法。
2.2 sigaction API
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
sigaction结构体:
struct sigaction {
void (*sa_handler)(int); // 信号处理函数或 SIG_DFL/SIG_IGN
void (*sa_sigaction)(int, siginfo_t *, void *); // 扩展处理函数(sa_flags & SA_SIGINFO时使用)
sigset_t sa_mask; // 处理信号期间需要额外屏蔽的信号集
int sa_flags; // 行为标志
void (*sa_restorer)(void); // 废弃,不用
};
常用sa_flags:
| 标志 | 含义 |
|---|---|
SA_SIGINFO | 使用sa_sigaction回调(可获取发送者PID等信息) |
SA_RESTART | 让被信号中断的系统调用自动重启 |
SA_NODEFER | 不自动屏蔽正在处理的信号(通常不推荐) |
SA_RESETHAND | 处理一次后恢复默认(不推荐) |
2.3 信号集操作
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
2.4 阻塞信号:sigprocmask
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how:SIG_BLOCK(增加阻塞)、SIG_UNBLOCK(解除阻塞)、SIG_SETMASK(设置阻塞集)- 阻塞的信号会排队,当解除阻塞时送达。
2.5 sigwait:同步等待信号
int sigwait(const sigset_t *set, int *sig);
- 必须先将信号阻塞,然后调用
sigwait同步等待,避免了异步处理函数的限制。
三、实战案例
实验1:sigaction 代替 signal,可靠处理 SIGINT
/**
* sigaction_demo.c —— 使用sigaction处理SIGINT
* 编译: arm-linux-gnueabihf-gcc -Wall -g -o sigaction_demo sigaction_demo.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
void sigint_handler(int sig) {
const char msg[] = "\n捕获到 SIGINT,但我不退出。按 Ctrl+\\ 发送 SIGQUIT 退出。\n";
write(STDOUT_FILENO, msg, sizeof(msg) - 1);
}
int main(void) {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = sigint_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART; // 自动重启被中断的系统调用
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
exit(EXIT_FAILURE);
}
printf("进程 PID=%d,按 Ctrl+C 测试...\n", getpid());
while (1) {
sleep(1);
write(STDOUT_FILENO, ".", 1);
}
return 0;
}
实验2:阻塞信号并使用 sigwait 同步处理
/**
* sigwait_demo.c —— 阻塞SIGUSR1,用sigwait同步处理
* 编译: arm-linux-gnueabihf-gcc -Wall -g -o sigwait_demo sigwait_demo.c -lpthread
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
int main(void) {
sigset_t set;
int sig;
pid_t pid = fork();
if (pid < 0) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
/* 子进程:睡眠1秒后向父进程发送SIGUSR1 */
sleep(1);
kill(getppid(), SIGUSR1);
_exit(0);
} else {
/* 父进程:阻塞SIGUSR1,然后同步等待 */
sigemptyset(&set);
sigaddset(&set, SIGUSR1);
sigprocmask(SIG_BLOCK, &set, NULL);
printf("父进程 PID=%d,等待 SIGUSR1...\n", getpid());
/* sigwait 会原子性地解除阻塞并等待信号,收到后重新阻塞 */
if (sigwait(&set, &sig) == 0) {
printf("收到信号: %d\n", sig);
}
wait(NULL);
}
return 0;
}
sigwait的优势:信号处理逻辑运行在正常线程上下文,可以安全调用任何函数,不再受异步信号安全限制。
四、signalfd —— 将信号变成文件描述符
4.1 原理
signalfd创建一个文件描述符,当有信号送达时,可通过read读取struct signalfd_siginfo结构,获得信号详细信息。这使得信号可以像普通fd一样被select/poll/epoll监听。
4.2 API
#include <sys/signalfd.h>
int signalfd(int fd, const sigset_t *mask, int flags);
fd:传入-1则创建新fd;也可传入已有signalfd来修改其掩码mask:要监听的信号集(这些信号应先被阻塞,否则不会送到signalfd)flags:通常0或SFD_NONBLOCK、SFD_CLOEXEC
读取的数据结构:
struct signalfd_siginfo {
uint32_t ssi_signo; // 信号编号
int32_t ssi_errno;
int32_t ssi_code;
uint32_t ssi_pid; // 发送者PID
uint32_t ssi_uid; // 发送者UID
// ... 更多字段
};
4.3 实战:signalfd 结合 epoll 处理信号
/**
* signalfd_demo.c —— 使用signalfd将信号融入epoll事件循环
* 编译: arm-linux-gnueabihf-gcc -Wall -g -o signalfd_demo signalfd_demo.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <stdint.h>
#include <sys/epoll.h>
#include <sys/signalfd.h>
int main(void) {
sigset_t mask;
int sfd, epfd;
struct epoll_event ev, events[2];
/* 1. 阻塞 SIGINT 和 SIGUSR1,防止默认处理 */
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGUSR1);
sigprocmask(SIG_BLOCK, &mask, NULL);
/* 2. 创建 signalfd */
sfd = signalfd(-1, &mask, SFD_NONBLOCK);
if (sfd == -1) {
perror("signalfd");
exit(EXIT_FAILURE);
}
/* 3. 创建 epoll 实例并注册 signalfd */
epfd = epoll_create1(0);
ev.events = EPOLLIN;
ev.data.fd = sfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev);
printf("进程 PID=%d,等待 SIGINT 或 SIGUSR1...\n", getpid());
printf("向本进程发送信号测试: kill -USR1 %d\n", getpid());
while (1) {
int nfds = epoll_wait(epfd, events, 2, -1);
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == sfd) {
struct signalfd_siginfo info;
ssize_t n = read(sfd, &info, sizeof(info));
if (n != sizeof(info)) {
perror("read signalfd");
continue;
}
printf("收到信号: %u,来自 PID=%u\n", info.ssi_signo, info.ssi_pid);
if (info.ssi_signo == SIGINT) {
printf("收到 SIGINT,退出。\n");
close(sfd);
close(epfd);
exit(0);
}
}
}
}
return 0;
}
五、timerfd —— 文件描述符形式的POSIX定时器
5.1 原理
传统的alarm和setitimer以信号方式通知超时,精度有限且难以融入epoll。timerfd将定时器抽象为文件描述符,超时时变为可读,可以像普通fd一样被epoll监听,非常适合于事件驱动架构。
5.2 API
#include <sys/timerfd.h>
int timerfd_create(int clockid, int flags);
int timerfd_settime(int fd, int flags,
const struct itimerspec *new_value,
struct itimerspec *old_value);
int timerfd_gettime(int fd, struct itimerspec *curr_value);
clockid:CLOCK_REALTIME(系统实时时钟,可被修改)或CLOCK_MONOTONIC(单调时钟,不受系统时间影响,推荐)flags:TFD_NONBLOCK、TFD_CLOEXECitimerspec结构体:
struct itimerspec {
struct timespec it_interval; // 间隔时间(0表示单次定时)
struct timespec it_value; // 首次超时时间(0表示禁用)
};
struct timespec {
time_t tv_sec; // 秒
long tv_nsec; // 纳秒
};
超时后,从timerfd读取一个8字节的uint64_t,表示超时次数(可忽略)。
5.3 实战:timerfd + epoll 实现周期任务
/**
* timerfd_demo.c —— timerfd与epoll结合实现高精度定时器
* 编译: arm-linux-gnueabihf-gcc -Wall -g -o timerfd_demo timerfd_demo.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdint.h>
#include <sys/epoll.h>
#include <sys/timerfd.h>
int main(void) {
int tfd, epfd;
struct epoll_event ev, events[1];
struct itimerspec its;
/* 1. 创建 timerfd (单调时钟) */
tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
if (tfd == -1) {
perror("timerfd_create");
exit(EXIT_FAILURE);
}
/* 2. 设置定时器:首次1秒后触发,之后每500ms周期触发 */
its.it_value.tv_sec = 1;
its.it_value.tv_nsec = 0;
its.it_interval.tv_sec = 0;
its.it_interval.tv_nsec = 500000000; // 500ms
if (timerfd_settime(tfd, 0, &its, NULL) == -1) {
perror("timerfd_settime");
exit(EXIT_FAILURE);
}
/* 3. 创建 epoll 并注册 timerfd */
epfd = epoll_create1(0);
ev.events = EPOLLIN;
ev.data.fd = tfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, tfd, &ev);
printf("定时器启动,每500ms触发一次,共触发10次\n");
int count = 0;
while (count < 10) {
int nfds = epoll_wait(epfd, events, 1, -1);
if (nfds > 0 && events[0].data.fd == tfd) {
uint64_t expirations;
ssize_t n = read(tfd, &expirations, sizeof(expirations));
if (n == sizeof(expirations)) {
printf("定时器触发!第 %d 次,超时次数=%llu\n", ++count, (unsigned long long)expirations);
}
}
}
close(tfd);
close(epfd);
printf("定时器结束\n");
return 0;
}
六、综合案例:signalfd + timerfd + epoll 一体化事件循环
下面将信号、定时器、网络socket统一用epoll管理,这是现代高性能服务器的基础框架。
/**
* event_loop_demo.c —— signalfd + timerfd + socket 的epoll事件循环
* 编译: arm-linux-gnueabihf-gcc -Wall -g -o event_loop_demo event_loop_demo.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <stdint.h>
#include <sys/epoll.h>
#include <sys/signalfd.h>
#include <sys/timerfd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define PORT 9090
#define MAX_EVENTS 10
int main(void)
{
int sfd, tfd, listen_fd, epfd;
struct epoll_event ev, events[MAX_EVENTS];
/* 1. 阻塞信号并创建signalfd */
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);
sigprocmask(SIG_BLOCK, &mask, NULL);
sfd = signalfd(-1, &mask, SFD_NONBLOCK);
/* 2. 创建定时器fd (每2秒输出心跳) */
tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
struct itimerspec its = {{2,0},{2,0}};
timerfd_settime(tfd, 0, &its, NULL);
/* 3. 创建监听socket */
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr = {AF_INET, htons(PORT), {INADDR_ANY}};
bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(listen_fd, 5);
/* 4. 创建epoll并注册所有fd */
epfd = epoll_create1(0);
ev.events = EPOLLIN;
ev.data.fd = sfd; epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev);
ev.data.fd = tfd; epoll_ctl(epfd, EPOLL_CTL_ADD, tfd, &ev);
ev.data.fd = listen_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);
printf("事件循环启动,端口 %d。每2秒打印心跳,按Ctrl+C退出。\n", PORT);
int running = 1;
while (running) {
int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == sfd) {
struct signalfd_siginfo info;
read(sfd, &info, sizeof(info));
if (info.ssi_signo == SIGINT || info.ssi_signo == SIGTERM) {
printf("\n收到退出信号,退出循环。\n");
running = 0;
}
} else if (events[i].data.fd == tfd) {
uint64_t exp;
read(tfd, &exp, sizeof(exp));
printf("[心跳] 系统运行正常...\n");
} else if (events[i].data.fd == listen_fd) {
int client = accept(listen_fd, NULL, NULL);
if (client != -1) {
char *msg = "Welcome\n";
send(client, msg, strlen(msg), 0);
close(client);
}
}
}
}
close(sfd);
close(tfd);
close(listen_fd);
close(epfd);
return 0;
}
七、对比与最佳实践
| 方法 | 优势 | 劣势 |
|---|---|---|
signal() | 简单 | 不可靠,不可移植 |
sigaction() | 可靠、可控 | 异步安全限制 |
sigwait() | 同步处理,无限制 | 需要一个线程阻塞等待 |
signalfd | 融入事件循环,读取详细信息 | 需要先阻塞信号 |
alarm/setitimer | 简单 | 精度低,信号中断 |
timerfd | 高精度、融入epoll、可多定时器 | Linux特有 |
推荐组合:signalfd + timerfd + epoll 构成现代Linux事件驱动架构的基础。
八、总结与思考题
本篇深入掌握了信号处理的高级方法,并将信号和定时器统一到epoll事件循环中,为你构建健壮的嵌入式应用打下坚实基础。
核心要点:
- 用
sigaction代替signal,设置SA_RESTART避免系统调用被中断 - 阻塞信号 +
sigwait实现同步处理,突破异步安全限制 signalfd将信号转化为fd,完美融入epolltimerfd提供高精度、多实例的定时器,与epoll天然集成- 事件循环统一管理所有IO、信号、定时,是服务器框架的核心
思考题:
- 如果不在使用
signalfd前阻塞信号,会发生什么? - 在综合案例中,如果定时器回调里执行了长时间操作,会影响事件循环吗?如何解决?
- 如何用
timerfd实现一个“延迟执行”任务(类似setTimeout)?
欢迎在评论区留下你的思考。下一篇我们将打造综合项目:基于ARM的智能家居网关(多线程+串口+网络+数据库)。
参考资料
man sigaction,man signalfd,man timerfd_create- 《UNIX环境高级编程(APUE)》第10章 Signals
- Linux内核文档
Documentation/signalfd.txt


被折叠的 条评论
为什么被折叠?



