嵌入式Linux应用开发系列⑧:信号处理进阶——sigaction、signalfd与高精度定时器timerfd

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


在这里插入图片描述

一、引言

在系列⑤中,我们快速接触了信号的基本用法:signalkillalarm。但在实际项目中,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);
  • howSIG_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_NONBLOCKSFD_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 原理

传统的alarmsetitimer以信号方式通知超时,精度有限且难以融入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);
  • clockidCLOCK_REALTIME(系统实时时钟,可被修改)或CLOCK_MONOTONIC(单调时钟,不受系统时间影响,推荐)
  • flagsTFD_NONBLOCKTFD_CLOEXEC
  • itimerspec结构体:
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事件循环中,为你构建健壮的嵌入式应用打下坚实基础。

核心要点

  1. sigaction代替signal,设置SA_RESTART避免系统调用被中断
  2. 阻塞信号 + sigwait 实现同步处理,突破异步安全限制
  3. signalfd将信号转化为fd,完美融入epoll
  4. timerfd提供高精度、多实例的定时器,与epoll天然集成
  5. 事件循环统一管理所有IO、信号、定时,是服务器框架的核心

思考题

  1. 如果不在使用signalfd前阻塞信号,会发生什么?
  2. 在综合案例中,如果定时器回调里执行了长时间操作,会影响事件循环吗?如何解决?
  3. 如何用timerfd实现一个“延迟执行”任务(类似setTimeout)?

欢迎在评论区留下你的思考。下一篇我们将打造综合项目:基于ARM的智能家居网关(多线程+串口+网络+数据库)


参考资料

  • man sigaction, man signalfd, man timerfd_create
  • 《UNIX环境高级编程(APUE)》第10章 Signals
  • Linux内核文档 Documentation/signalfd.txt
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值