signalfd4系统调用及示例

signalfd64

1. 函数介绍

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

data-ad-format="fluid" data-ad-layout-key="-7k+ex-4a-9w+4a">

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

https://www.calcguide.tech/2025/08/11/signalfd4系统调用及示例/

统一 I/O 模型:可以将信号处理集成到 select, poll, epoll 等 I/O 多路复用机制中。程序可以像等待文件描述符就绪一样等待信号。

避免信号处理函数的复杂性:信号处理函数有诸多限制(只能调用异步信号安全函数),并且容易引入竞态条件。使用 signalfd 可以在程序的主循环中处理信号,避免这些问题。

获取更详细的信号信息:可以从文件描述符中读取到完整的 signalfd_siginfo 结构体,包含信号的所有信息。

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

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

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

2. 函数原型

1
2
3
4
5
#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)

  • EINVAL: flags 参数包含无效标志,或者 fd 是一个已存在的 signalfd 但 mask 为 NULL。

  • EMFILE: 进程已打开的文件描述符数量达到上限。

  • ENFILE: 系统已打开的文件描述符数量达到上限。

  • ENOMEM: 内核内存不足。

  • EBADF: fd 参数不是 -1,也不是一个有效的 signalfd 文件描述符。

7. 读取信号信息

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

1
2
3
4
5
6
7
8
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 中读取信号信息。

  • poll, select, epoll_wait: I/O 多路复用函数,可以监听 signalfd 文件描述符的可读事件。

  • close: 关闭 signalfd 文件描述符。

9. 示例代码

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
#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&#91;2]; // 监听 signalfd 和 标准输入
struct signalfd_siginfo si;
ssize_t s;
char buf&#91;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&#91;0].fd = sfd;
fds&#91;0].events = POLLIN; // 等待可读事件
// 监听标准输入 (stdin)
fds&#91;1].fd = STDIN_FILENO;
fds&#91;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&#91;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&#91;1].revents & POLLIN) {
ssize_t nread = read(STDIN_FILENO, buf, sizeof(buf) - 1);
if (nread > 0) {
buf&#91;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&#91;0].revents & (POLLERR | POLLHUP | POLLNVAL)) {
fprintf(stderr, "Error on signalfd.\n");
break;
}
if (fds&#91;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. 编译和运行

1
2
3
4
5
6
7
8
# 假设代码保存在 signalfd_example.c 中
gcc -o signalfd_example signalfd_example.c

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

11. 预期输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
--- 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 提供了一种现代化、同步化的方式来处理信号,特别适合于事件驱动的程序(如服务器)。通过将其与 poll, select, epoll 等 I/O 多路复用技术结合,可以构建出高效且结构清晰的程序。记住两个关键点:

先阻塞,再创建:必须先用 sigprocmask 阻塞要监听的信号。

像文件一样读取:使用 read() 从 signalfd 中读取 struct signalfd_siginfo 结构体来获取信号信息。

这种方式避免了传统信号处理函数的复杂性和潜在风险,是编写健壮 Linux 应用程序的有力工具。

data-ad-format="auto" data-full-width-responsive="true">