好的,我们来深入学习 signalfd
和 signalfd4
系统调用
signalfd
1. 函数介绍
在传统的 Linux 信号处理中,我们使用 sigaction
来设置信号处理函数。当信号到达时,内核会中断程序的正常执行流程,转而去执行我们注册的处理函数。这是一种异步的处理方式。
但是,有时候我们希望用一种同步、基于文件描述符 (File Descriptor) 的方式来处理信号。这样做的好处是:
- 统一 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. 函数原型
#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()
系统调用从该文件描述符中读取信号信息。
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
系统调用中。
#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
提供了一种现代化、同步化的方式来处理信号,特别适合于事件驱动的程序(如服务器)。通过将其与 poll
, select
, epoll
等 I/O 多路复用技术结合,可以构建出高效且结构清晰的程序。记住两个关键点:
- 先阻塞,再创建:必须先用
sigprocmask
阻塞要监听的信号。 - 像文件一样读取:使用
read()
从signalfd
中读取struct signalfd_siginfo
结构体来获取信号信息。
这种方式避免了传统信号处理函数的复杂性和潜在风险,是编写健壮 Linux 应用程序的有力工具。好的,我们来深入学习 signalfd
和 signalfd4
系统调用,从 Linux 编程小白的角度出发。