Linux C语言信号处理的异步通知
信号的基本概念
信号是什么
在Linux系统中,信号(Signal)是一种软件中断,用于在进程间传递事件异步通知。它类似于硬件中断,但由软件产生,用于通知进程发生了某个特定事件。信号可以由内核、其他进程或进程自身产生。例如,当用户在终端按下 Ctrl + C
组合键时,系统会向当前前台进程发送一个 SIGINT
信号,通知进程用户希望终止它。
信号的种类
Linux系统定义了多种信号,每种信号都有一个唯一的编号和名称。常见的信号包括:
SIGINT
:中断信号,通常由用户在终端按下Ctrl + C
组合键产生,用于终止前台进程。SIGTERM
:终止信号,用于请求进程正常终止。与SIGKILL
不同,SIGTERM
允许进程捕获并执行清理操作。SIGKILL
:强制终止信号,该信号不能被捕获或忽略,用于立即终止进程。SIGALRM
:闹钟信号,由alarm
函数设置的定时器到期时产生,常用于实现超时机制。SIGCHLD
:子进程状态改变信号,当子进程终止、停止或继续运行时,父进程会收到该信号。
信号的处理方式
进程对信号有三种处理方式:
- 默认处理:每个信号都有默认的处理动作,例如
SIGINT
的默认处理是终止进程,SIGCHLD
的默认处理是忽略。 - 忽略信号:进程可以选择忽略某些信号,通过调用
signal
函数将信号的处理函数设置为SIG_IGN
来实现。但某些信号,如SIGKILL
和SIGSTOP
,不能被忽略。 - 捕获信号:进程可以定义自己的信号处理函数,当收到指定信号时,系统会暂停当前执行的代码,转而执行信号处理函数。处理完毕后,再返回原来的执行点继续执行。
信号处理函数
signal
函数
signal
函数用于设置信号的处理方式,其原型如下:
#include <signal.h>
void (*signal(int signum, void (*handler)(int)))(int);
signum
:要设置处理方式的信号编号。handler
:信号处理函数指针,或者是SIG_IGN
(忽略信号)、SIG_DFL
(恢复默认处理)。
例如,要捕获 SIGINT
信号并打印一条消息,可以这样写:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigint_handler(int signum) {
printf("Received SIGINT signal. Terminating...\n");
// 这里可以进行一些清理操作,如关闭文件、释放资源等
_exit(0); // 以正常状态退出进程
}
int main() {
// 设置SIGINT信号的处理函数
signal(SIGINT, sigint_handler);
printf("Press Ctrl + C to terminate the program.\n");
while (1) {
sleep(1);
}
return 0;
}
在上述代码中,通过 signal(SIGINT, sigint_handler)
将 SIGINT
信号的处理函数设置为 sigint_handler
。当用户按下 Ctrl + C
时,sigint_handler
函数会被调用,打印消息并终止进程。
sigaction
函数
sigaction
函数提供了更灵活和强大的信号处理设置方式,其原型如下:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
signum
:信号编号。act
:指向struct sigaction
结构体的指针,用于设置新的信号处理方式。oldact
:指向struct sigaction
结构体的指针,用于保存旧的信号处理方式。如果不需要保存旧的处理方式,可以设为NULL
。
struct sigaction
结构体定义如下:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
sa_handler
:信号处理函数指针,与signal
函数中的handler
类似。sa_sigaction
:另一种信号处理函数指针,用于在设置了SA_SIGINFO
标志时使用,提供更详细的信号信息。sa_mask
:信号掩码,在调用信号处理函数期间,会将该掩码中的信号阻塞,防止这些信号中断处理函数。sa_flags
:信号处理的标志,如SA_RESTART
(使被信号中断的系统调用自动重新启动)、SA_SIGINFO
(使用sa_sigaction
作为信号处理函数)等。sa_restorer
:此成员已过时,不应使用。
以下是使用 sigaction
捕获 SIGINT
信号的示例:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigint_handler(int signum) {
printf("Received SIGINT signal. Terminating...\n");
_exit(0);
}
int main() {
struct sigaction act;
act.sa_handler = sigint_handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGINT, &act, NULL);
printf("Press Ctrl + C to terminate the program.\n");
while (1) {
sleep(1);
}
return 0;
}
在这个示例中,先初始化 struct sigaction
结构体 act
,设置信号处理函数 sa_handler
,清空信号掩码 sa_mask
并设置标志 sa_flags
为 0。然后通过 sigaction(SIGINT, &act, NULL)
设置 SIGINT
信号的处理方式。
信号的阻塞与解除阻塞
信号掩码
每个进程都有一个信号掩码(Signal Mask),它是一个信号集,用于指定当前被阻塞的信号。被阻塞的信号不会被立即处理,而是被挂起,直到信号被解除阻塞。
信号集操作函数
sigemptyset
:初始化一个空的信号集。
#include <signal.h>
int sigemptyset(sigset_t *set);
sigfillset
:初始化一个信号集,使其包含所有信号。
#include <signal.h>
int sigfillset(sigset_t *set);
sigaddset
:将指定信号添加到信号集中。
#include <signal.h>
int sigaddset(sigset_t *set, int signum);
sigdelset
:将指定信号从信号集中删除。
#include <signal.h>
int sigdelset(sigset_t *set, int signum);
sigismember
:检查指定信号是否在信号集中。
#include <signal.h>
int sigismember(const sigset_t *set, int signum);
sigprocmask
函数
sigprocmask
函数用于设置和查询进程的信号掩码,其原型如下:
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how
:指定操作方式,有以下几种取值:SIG_BLOCK
:将set
中的信号添加到当前信号掩码中。SIG_UNBLOCK
:将set
中的信号从当前信号掩码中删除。SIG_SETMASK
:将当前信号掩码设置为set
。
set
:指向要操作的信号集的指针。如果how
为SIG_BLOCK
或SIG_SETMASK
,则set
不能为空。如果how
为SIG_UNBLOCK
,set
可以为NULL
。oldset
:指向一个信号集的指针,用于保存旧的信号掩码。如果不需要保存旧的信号掩码,可以设为NULL
。
以下是一个示例,展示如何阻塞和解除阻塞 SIGINT
信号:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigint_handler(int signum) {
printf("Received SIGINT signal. Terminating...\n");
_exit(0);
}
int main() {
struct sigaction act;
act.sa_handler = sigint_handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGINT, &act, NULL);
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
// 阻塞SIGINT信号
sigprocmask(SIG_BLOCK, &set, NULL);
printf("SIGINT signal is blocked. Press Enter to unblock.\n");
getchar();
// 解除阻塞SIGINT信号
sigprocmask(SIG_UNBLOCK, &set, NULL);
printf("SIGINT signal is unblocked. Press Ctrl + C to terminate.\n");
while (1) {
sleep(1);
}
return 0;
}
在上述代码中,先设置 SIGINT
信号的处理函数,然后创建一个信号集并将 SIGINT
信号添加到其中。通过 sigprocmask(SIG_BLOCK, &set, NULL)
阻塞 SIGINT
信号,用户按下回车键后,通过 sigprocmask(SIG_UNBLOCK, &set, NULL)
解除阻塞。
异步通知机制
什么是异步通知
异步通知是指当某个事件发生时,系统以信号的方式通知相关进程,进程无需不断轮询检查事件是否发生,从而提高系统的效率和响应性。例如,当文件描述符有数据可读时,系统可以通过信号通知进程,进程可以在信号处理函数中进行数据读取操作。
fcntl
函数与异步通知
fcntl
函数可以用于设置文件描述符的异步通知属性,其原型如下:
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
要设置文件描述符 fd
为异步通知模式,需要执行以下步骤:
- 使用
fcntl
函数设置文件描述符为非阻塞模式。 - 使用
fcntl
函数设置文件描述符的所有者进程ID(通常是当前进程ID)。 - 使用
fcntl
函数设置异步通知的信号。
以下是一个示例,展示如何对标准输入(文件描述符为 STDIN_FILENO
)设置异步通知:
#include <stdio.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
void sigio_handler(int signum) {
char buffer[1024];
ssize_t bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer) - 1);
if (bytes_read > 0) {
buffer[bytes_read] = '\0';
printf("Received data: %s", buffer);
}
}
int main() {
struct sigaction act;
act.sa_handler = sigio_handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGIO, &act, NULL);
int flags = fcntl(STDIN_FILENO, F_GETFL, 0);
fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK | O_ASYNC);
fcntl(STDIN_FILENO, F_SETOWN, getpid());
printf("Waiting for data on stdin...\n");
while (1) {
sleep(1);
}
return 0;
}
在上述代码中,先设置 SIGIO
信号的处理函数 sigio_handler
。然后通过 fcntl
函数获取标准输入的文件状态标志,并添加 O_NONBLOCK
和 O_ASYNC
标志使其成为非阻塞和异步通知模式。接着通过 fcntl(STDIN_FILENO, F_SETOWN, getpid())
设置文件描述符的所有者为当前进程。当有数据输入到标准输入时,系统会发送 SIGIO
信号,sigio_handler
函数会被调用并读取数据。
基于信号的异步I/O
除了标准输入,异步通知机制也可以应用于其他文件描述符,如管道、套接字等。例如,在网络编程中,可以对套接字设置异步通知,当有数据到达套接字时,通过信号通知进程进行数据处理,这样可以避免在等待数据时阻塞进程,提高程序的并发处理能力。
信号处理中的注意事项
异步信号安全函数
在信号处理函数中,只能调用异步信号安全(Async - Signal - Safe)函数。这些函数不会被信号中断,也不会引起未定义行为。常见的异步信号安全函数包括 read
、write
、_exit
等。而像 printf
这类函数不是异步信号安全的,在信号处理函数中调用可能会导致程序崩溃或其他未定义行为。
可重入性
信号处理函数应该是可重入的,即当信号处理函数正在执行时,如果再次收到相同的信号,函数应该能够安全地再次进入并执行。这要求信号处理函数不依赖于共享的静态或全局变量,除非在进入函数时对这些变量进行了适当的保护(如使用互斥锁)。
信号处理与系统调用
一些系统调用在收到信号时会被中断。例如,read
、write
、accept
等函数。默认情况下,这些被中断的系统调用会返回 -1
并设置 errno
为 EINTR
。如果希望被中断的系统调用自动重新启动,可以在设置信号处理函数时使用 SA_RESTART
标志。
综合示例
以下是一个综合示例,展示了信号处理、异步通知以及信号阻塞与解除阻塞的综合应用:
#include <stdio.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
void sigio_handler(int signum) {
char buffer[1024];
ssize_t bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer) - 1);
if (bytes_read > 0) {
buffer[bytes_read] = '\0';
printf("Received data: %s", buffer);
}
}
void sigint_handler(int signum) {
printf("Received SIGINT signal. Exiting...\n");
_exit(0);
}
int main() {
struct sigaction act;
// 设置SIGIO信号处理函数
act.sa_handler = sigio_handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGIO, &act, NULL);
// 设置SIGINT信号处理函数
act.sa_handler = sigint_handler;
sigaction(SIGINT, &act, NULL);
int flags = fcntl(STDIN_FILENO, F_GETFL, 0);
fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK | O_ASYNC);
fcntl(STDIN_FILENO, F_SETOWN, getpid());
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGIO);
// 阻塞SIGIO信号
sigprocmask(SIG_BLOCK, &set, NULL);
printf("SIGIO signal is blocked. Press Enter to unblock.\n");
getchar();
// 解除阻塞SIGIO信号
sigprocmask(SIG_UNBLOCK, &set, NULL);
printf("SIGIO signal is unblocked. Waiting for data on stdin...\n");
printf("Press Ctrl + C to exit.\n");
while (1) {
sleep(1);
}
return 0;
}
在这个示例中,设置了 SIGIO
和 SIGINT
信号的处理函数。对标准输入设置了异步通知模式,并阻塞和解除阻塞了 SIGIO
信号。程序会等待用户输入数据,当有数据输入时,通过 SIGIO
信号通知并读取数据,用户也可以通过按下 Ctrl + C
来终止程序。
通过以上内容,我们深入了解了Linux C语言中信号处理的异步通知机制,包括信号的基本概念、信号处理函数、信号的阻塞与解除阻塞、异步通知机制以及在信号处理中需要注意的事项,并通过多个代码示例进行了演示。掌握这些知识,能够让我们更好地编写在Linux环境下具有高响应性和稳定性的程序。