signalfd系统调用及示例

好的,我们来深入学习 signalfd 和 signalfd4 系统调用

signalfd

1. 函数介绍

在传统的 Linux 信号处理中,我们使用 sigaction 来设置信号处理函数。当信号到达时,内核会中断程序的正常执行流程,转而去执行我们注册的处理函数。这是一种异步的处理方式。

但是,有时候我们希望用一种同步基于文件描述符 (File Descriptor) 的方式来处理信号。这样做的好处是:

  1. 统一 I/O 模型:可以将信号处理集成到 selectpollepoll 等 I/O 多路复用机制中。程序可以像等待文件描述符就绪一样等待信号。
  2. 避免信号处理函数的复杂性:信号处理函数有诸多限制(只能调用异步信号安全函数),并且容易引入竞态条件。使用 signalfd 可以在程序的主循环中处理信号,避免这些问题。
  3. 获取更详细的信号信息:可以从文件描述符中读取到完整的 signalfd_siginfo 结构体,包含信号的所有信息。

signalfd (Signal File Descriptor) 系统调用就是为此而设计的。它创建一个特殊的文件描述符,该描述符用于接收由你指定的信号集中的信号。当这些信号中的任何一个到来时,这个文件描述符就会变为“可读”状态。你可以用 read() 从这个文件描述符中读取信号信息。

signalfd64 是 signalfd 的一个更新的、64 位兼容的版本。在现代 glibc 和内核中,用户空间程序通常调用的是 signalfd,而它在底层可能会根据系统架构和内核版本自动选择使用 signalfd 或 signalfd64。对于我们编程来说,直接使用 signalfd 即可。

简单来说,signalfd 就是把信号“变成”了可以像文件一样读取的数据流,让你可以用处理文件 I/O 的方式来处理信号。

2. 函数原型

#include <sys/signalfd.h> // 包含系统调用声明和相关结构体

// glibc 提供的标准接口
int signalfd(int fd, const sigset_t *mask, int flags);

注意:底层系统调用可能有 signalfd 和 signalfd64 之分,但 glibc 会为我们处理好兼容性问题。

3. 功能

创建或修改一个信号文件描述符,该描述符可以用来接收由 mask 参数指定的信号集中的信号。

4. 参数

  • fd:
    • int 类型。
    • 如果是 -1,则表示创建一个新的 signalfd
    • 如果是一个已存在的 signalfd 的文件描述符,则表示修改该 signalfd 的信号掩码。
  • mask:
    • const sigset_t * 类型。
    • 一个指向信号集的指针。这个信号集定义了你希望通过这个 signalfd 接收的信号。在创建或修改 signalfd 之前,你必须先使用 sigprocmask() 将这些信号阻塞掉。这是 signalfd 能正常工作的前提。
  • flags:
    • int 类型。
    • 用于修改 signalfd 行为的标志位。常用的标志有:
      • SFD_CLOEXEC: 在执行 exec() 系列函数时自动关闭该文件描述符。
      • SFD_NONBLOCK: 使 read() 操作变为非阻塞模式。如果当前没有信号可读,read() 会立即返回 -1 并设置 errno 为 EAGAIN 或 EWOULDBLOCK

5. 返回值

  • 成功: 返回一个有效的信号文件描述符(一个非负整数)。
  • 失败: 返回 -1,并设置全局变量 errno 来指示具体的错误原因。

6. 错误码 (errno)

  • EINVALflags 参数包含无效标志,或者 fd 是一个已存在的 signalfd 但 mask 为 NULL
  • EMFILE: 进程已打开的文件描述符数量达到上限。
  • ENFILE: 系统已打开的文件描述符数量达到上限。
  • ENOMEM: 内核内存不足。
  • EBADFfd 参数不是 -1,也不是一个有效的 signalfd 文件描述符。

7. 读取信号信息

一旦 signalfd 创建成功并有信号到达,你就可以使用 read() 系统调用从该文件描述符中读取信号信息。

struct signalfd_siginfo si;
ssize_t s = read(sfd, &si, sizeof(si));
if (s != sizeof(si)) {
    // Handle error
}
// Now you can access signal information in 'si'
printf("Got signal %d from PID %d\n", si.ssi_signo, si.ssi_pid);

struct signalfd_siginfo 结构体包含了丰富的信号信息,例如:

  • ssi_signo: 信号编号。
  • ssi_errno: 伴随信号的错误码。
  • ssi_code: 信号产生的原因。
  • ssi_pid: 发送信号的进程 ID (如果适用)。
  • ssi_uid: 发送信号的用户 ID (如果适用)。
  • ssi_fd: 与信号相关的文件描述符 (如果适用,如 SIGIO)。
  • ssi_band: 与 SIGIO/SIGURG 相关的带外数据。
  • ssi_overrun: 实时信号队列溢出的数量。
  • ssi_trapno: 导致信号产生的陷阱号。
  • ssi_status: 退出状态或信号 (对于 SIGCHLD)。
  • ssi_int / ssi_ptr: 通过 sigqueue() 发送的伴随数据。
  • ssi_utime / ssi_stime: 用户和系统 CPU 时间。
  • ssi_addr: 导致信号产生的内存地址。

8. 相似函数或关联函数

  • sigaction: 传统的信号处理方式,设置信号处理函数。
  • sigprocmask: 用于设置/查询信号屏蔽字。在使用 signalfd 之前必须先用它来阻塞要监听的信号。
  • read: 用于从 signalfd 中读取信号信息。
  • pollselectepoll_wait: I/O 多路复用函数,可以监听 signalfd 文件描述符的可读事件。
  • close: 关闭 signalfd 文件描述符。

9. 示例代码

下面的示例演示了如何使用 signalfd 来同步处理信号,并将其集成到 poll 系统调用中。

#define _GNU_SOURCE // 启用 GNU 扩展
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>     // 信号处理相关
#include <sys/signalfd.h> // signalfd 相关
#include <poll.h>       // poll 相关
#include <string.h>
#include <errno.h>
#include <sys/wait.h>   // wait 相关

int main() {
    sigset_t mask;
    int sfd, j;
    struct pollfd fds[2]; // 监听 signalfd 和 标准输入
    struct signalfd_siginfo si;
    ssize_t s;
    char buf[1024]; // 用于读取标准输入

    printf("--- Demonstrating signalfd ---\n");
    printf("PID: %d\n", getpid());
    printf("Try sending signals:\n");
    printf("  kill -USR1 %d\n", getpid());
    printf("  kill -USR2 %d\n", getpid());
    printf("  Or press Ctrl+C (SIGINT) or Ctrl+\\ (SIGQUIT)\n");
    printf("  Type 'exit' and press Enter to quit.\n");
    printf("  This program uses poll() to wait for signals or input.\n");

    // 1. 创建要监听的信号集
    sigemptyset(&mask);
    sigaddset(&mask, SIGUSR1);
    sigaddset(&mask, SIGUSR2);
    sigaddset(&mask, SIGINT);  // Ctrl+C
    sigaddset(&mask, SIGQUIT); // Ctrl+\

    // 2. 阻塞这些信号
    // 这是使用 signalfd 的关键前提!
    // 必须先阻塞信号,这样信号才不会被默认处理或由 sigaction 处理
    // 而是排队等待 signalfd 读取
    if (sigprocmask(SIG_BLOCK, &mask, NULL) == -1) {
        perror("sigprocmask");
        exit(EXIT_FAILURE);
    }
    printf("Blocked signals: SIGUSR1, SIGUSR2, SIGINT, SIGQUIT\n");

    // 3. 创建 signalfd
    // -1 表示创建新的 signalfd
    // &mask 是要监听的信号集
    // SFD_CLOEXEC 在 exec 时关闭 fd,SFD_NONBLOCK 设置为非阻塞
    sfd = signalfd(-1, &mask, SFD_CLOEXEC | SFD_NONBLOCK);
    if (sfd == -1) {
        perror("signalfd");
        exit(EXIT_FAILURE);
    }
    printf("Created signalfd: %d\n", sfd);

    // 4. 设置 poll 的文件描述符数组
    // 监听 signalfd
    fds[0].fd = sfd;
    fds[0].events = POLLIN; // 等待可读事件
    // 监听标准输入 (stdin)
    fds[1].fd = STDIN_FILENO;
    fds[1].events = POLLIN; // 等待可读事件

    // 5. 主循环:使用 poll 等待事件
    while (1) {
        // poll 会阻塞,直到 fds 数组中的任何一个 fd 准备好
        // -1 表示无限期等待
        int poll_num = poll(fds, 2, -1);
        if (poll_num == -1) {
            if (errno == EINTR) {
                // poll 被信号中断 (不太可能发生,因为我们用的是 signalfd)
                continue;
            } else {
                perror("poll");
                break;
            }
        }

        if (poll_num > 0) {
            // 检查 signalfd 是否有数据可读
            if (fds[0].revents & POLLIN) {
                // 6. 从 signalfd 读取信号信息
                s = read(sfd, &si, sizeof(si));
                if (s != sizeof(si)) {
                    if (errno == EAGAIN || errno == EWOULDBLOCK) {
                        // 非阻塞模式下没有数据可读 (不太可能,因为 poll 说了有)
                        continue;
                    } else {
                        perror("read signalfd");
                        break;
                    }
                }

                // 7. 处理接收到的信号
                printf("\nReceived signal: %d", si.ssi_signo);
                switch(si.ssi_signo) {
                    case SIGUSR1:
                        printf(" (SIGUSR1)");
                        break;
                    case SIGUSR2:
                        printf(" (SIGUSR2)");
                        break;
                    case SIGINT:
                        printf(" (SIGINT - Ctrl+C)");
                        break;
                    case SIGQUIT:
                        printf(" (SIGQUIT - Ctrl+\\)");
                        break;
                }
                printf("\n  Sender PID: %d\n", si.ssi_pid);
                printf("  Sender UID: %d\n", si.ssi_uid);

                // 如果收到 SIGINT 或 SIGQUIT,则退出
                if (si.ssi_signo == SIGINT || si.ssi_signo == SIGQUIT) {
                    printf("Exiting due to signal.\n");
                    break;
                }
            }

            // 检查标准输入是否有数据可读
            if (fds[1].revents & POLLIN) {
                ssize_t nread = read(STDIN_FILENO, buf, sizeof(buf) - 1);
                if (nread > 0) {
                    buf[nread] = '\0';
                    printf("Read from stdin: %s", buf); // buf 可能已包含 \n
                    // 如果用户输入 'exit',则退出
                    if (strncmp(buf, "exit\n", 5) == 0) {
                        printf("Exiting due to 'exit' command.\n");
                        break;
                    }
                } else if (nread == 0) {
                    // EOF on stdin (e.g., Ctrl+D)
                    printf("EOF on stdin. Exiting.\n");
                    break;
                } else {
                    if (errno != EAGAIN && errno != EWOULDBLOCK) {
                        perror("read stdin");
                        break;
                    }
                }
            }

            // 检查是否有错误或挂起事件
            if (fds[0].revents & (POLLERR | POLLHUP | POLLNVAL)) {
                fprintf(stderr, "Error on signalfd.\n");
                break;
            }
            if (fds[1].revents & (POLLERR | POLLHUP | POLLNVAL)) {
                 fprintf(stderr, "Error on stdin.\n");
                 break;
            }
        }
    }

    // 8. 清理资源
    printf("Closing signalfd...\n");
    close(sfd);
    printf("Program finished.\n");

    return 0;
}

10. 编译和运行

# 假设代码保存在 signalfd_example.c 中
gcc -o signalfd_example signalfd_example.c

# 运行程序
./signalfd_example
# 程序会输出 PID,并提示你可以发送哪些信号
# 在另一个终端尝试发送信号,或在程序运行的终端按 Ctrl+C 等

11. 预期输出

--- Demonstrating signalfd ---
PID: 12345
Blocked signals: SIGUSR1, SIGUSR2, SIGINT, SIGQUIT
Created signalfd: 3
Try sending signals:
  kill -USR1 12345
  kill -USR2 12345
  Or press Ctrl+C (SIGINT) or Ctrl+\ (SIGQUIT)
  Type 'exit' and press Enter to quit.
  This program uses poll() to wait for signals or input.

Read from stdin: hello
Read from stdin: world

Received signal: 10 (SIGUSR1)
  Sender PID: 12346
  Sender UID: 1000

Received signal: 12 (SIGUSR2)
  Sender PID: 12346
  Sender UID: 1000

Received signal: 2 (SIGINT - Ctrl+C)
  Sender PID: 0
  Sender UID: 0
Exiting due to signal.
Closing signalfd...
Program finished.

12. 总结

signalfd 提供了一种现代化、同步化的方式来处理信号,特别适合于事件驱动的程序(如服务器)。通过将其与 pollselectepoll 等 I/O 多路复用技术结合,可以构建出高效且结构清晰的程序。记住两个关键点:

  1. 先阻塞,再创建:必须先用 sigprocmask 阻塞要监听的信号。
  2. 像文件一样读取:使用 read() 从 signalfd 中读取 struct signalfd_siginfo 结构体来获取信号信息。

这种方式避免了传统信号处理函数的复杂性和潜在风险,是编写健壮 Linux 应用程序的有力工具。好的,我们来深入学习 signalfd 和 signalfd4 系统调用,从 Linux 编程小白的角度出发。

此条目发表在linux文章分类目录,贴了标签。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注