C 语言信号处理函数
C 语言信号处理函数概述
在 C 语言编程中,信号(signal)是一种异步通知机制,用于向进程报告特定事件的发生。信号可以由操作系统、其他进程或者进程自身产生。信号处理函数则是用于处理这些信号的函数,它们允许程序员对特定信号做出自定义的响应。
信号的概念
信号是一种软件中断,类似于硬件中断,但它是由软件事件触发的。每个信号都有一个唯一的整数值标识,在 C 语言中,这些值定义在 <signal.h>
头文件中。例如,常见的信号有 SIGINT
(通常由用户按下 Ctrl+C 产生)、SIGTERM
(用于请求进程终止)和 SIGSEGV
(表示进程发生了段错误,例如访问非法内存地址)等。
信号处理的基本原理
当一个信号被发送到进程时,进程会暂停当前的执行流程,转而执行与该信号关联的处理函数。如果没有为该信号设置自定义的处理函数,进程会采用默认的处理方式,这通常包括终止进程、忽略信号或者产生核心转储文件(core dump)等。
信号处理函数的注册与使用
在 C 语言中,我们使用 signal
函数来注册信号处理函数。
signal
函数的原型
signal
函数的原型如下:
#include <signal.h>
void (*signal(int signum, void (*handler)(int)))(int);
这个原型看起来比较复杂,我们可以将其简化理解为:
void *signal(int signum, void (*handler)(int));
其中,signum
是要处理的信号编号,handler
是指向处理函数的指针。处理函数的原型必须是 void handler(int signum)
,其中 signum
参数表示接收到的信号编号。
简单的信号处理示例
下面是一个简单的示例,演示如何捕获 SIGINT
信号(用户按下 Ctrl+C 时产生)并进行自定义处理:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void signal_handler(int signum) {
printf("Received SIGINT. Stopping...\n");
// 这里可以进行一些清理工作,比如关闭文件等
}
int main() {
// 注册信号处理函数
signal(SIGINT, signal_handler);
printf("Press Ctrl+C to stop the program.\n");
while (1) {
sleep(1);
printf("Running...\n");
}
return 0;
}
在这个示例中,我们定义了一个 signal_handler
函数来处理 SIGINT
信号。在 main
函数中,我们使用 signal
函数将 SIGINT
信号与 signal_handler
函数关联起来。然后程序进入一个无限循环,每秒打印一次 "Running..."。当用户按下 Ctrl+C 时,SIGINT
信号被发送到进程,进程会暂停当前循环,转而执行 signal_handler
函数,打印 "Received SIGINT. Stopping..."。
信号处理函数的返回值
signal
函数返回上一次为 signum
信号设置的处理函数的指针。如果出错,返回 SIG_ERR
。例如:
#include <stdio.h>
#include <signal.h>
void old_handler(int signum) {
printf("Old handler for SIGINT\n");
}
int main() {
void (*old_handler_ptr)(int);
old_handler_ptr = signal(SIGINT, old_handler);
if (old_handler_ptr == SIG_ERR) {
perror("signal");
return 1;
}
printf("Old handler pointer: %p\n", old_handler_ptr);
return 0;
}
在这个例子中,我们先获取了 SIGINT
信号之前的处理函数指针,并打印出来。如果 signal
函数调用失败,perror
函数会打印错误信息。
常见信号及其处理
SIGINT
信号
如前面示例所述,SIGINT
信号通常由用户在终端按下 Ctrl+C 产生。默认情况下,进程接收到 SIGINT
信号会终止。通过注册自定义的信号处理函数,我们可以实现更优雅的程序终止,例如进行一些清理工作。
SIGTERM
信号
SIGTERM
信号是一种通用的终止信号,通常由系统管理员或其他进程发送,用于请求进程正常终止。与 SIGKILL
不同,SIGTERM
允许进程捕获并进行清理操作后再终止。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigterm_handler(int signum) {
printf("Received SIGTERM. Cleaning up...\n");
// 进行清理工作,如关闭文件、释放资源等
// 这里简单打印一条信息模拟清理
_exit(0); // 清理完成后终止进程
}
int main() {
signal(SIGTERM, sigterm_handler);
printf("This process can be terminated with SIGTERM.\n");
while (1) {
sleep(1);
printf("Running...\n");
}
return 0;
}
在这个示例中,当进程接收到 SIGTERM
信号时,会执行 sigterm_handler
函数,打印清理信息并调用 _exit
函数终止进程。
SIGSEGV
信号
SIGSEGV
信号表示进程发生了段错误,通常是由于访问了非法的内存地址,如空指针解引用、数组越界访问等。处理 SIGSEGV
信号可以帮助我们在程序崩溃前进行一些诊断和日志记录工作。
#include <stdio.h>
#include <signal.h>
void segv_handler(int signum) {
printf("Received SIGSEGV. Performing diagnostic...\n");
// 这里可以添加更复杂的诊断代码,比如打印当前栈信息等
}
int main() {
signal(SIGSEGV, segv_handler);
int *ptr = NULL;
*ptr = 10; // 这会导致段错误,触发 SIGSEGV 信号
return 0;
}
在这个例子中,我们故意进行了空指针解引用操作,这会触发 SIGSEGV
信号。进程会执行 segv_handler
函数,打印诊断信息。虽然程序最终还是会崩溃,但通过捕获信号,我们可以在崩溃前获取一些有用的信息。
信号处理的注意事项
异步信号安全
在编写信号处理函数时,必须确保其是异步信号安全的。这意味着信号处理函数只能调用异步信号安全的函数,并且不能访问可能在信号处理期间被修改的共享资源。例如,标准 I/O 函数(如 printf
)在信号处理函数中使用时要特别小心,因为它们不是异步信号安全的。更安全的选择是使用 write
函数进行输出。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void signal_handler(int signum) {
const char *msg = "Received signal\n";
write(STDOUT_FILENO, msg, sizeof(msg) - 1);
}
int main() {
signal(SIGINT, signal_handler);
printf("Press Ctrl+C to generate a signal.\n");
while (1) {
sleep(1);
}
return 0;
}
在这个示例中,我们使用 write
函数在信号处理函数中输出信息,以确保异步信号安全。
可重入性
信号处理函数应该是可重入的。可重入函数是指可以被中断,然后在中断恢复后继续正确执行的函数。如果一个函数使用了静态变量或者全局变量,并且在函数执行期间这些变量可能被信号处理函数修改,那么这个函数就不是可重入的。例如:
#include <stdio.h>
#include <signal.h>
int global_var = 0;
void non_reentrant_handler(int signum) {
global_var++;
printf("global_var in handler: %d\n", global_var);
}
int main() {
signal(SIGINT, non_reentrant_handler);
while (1) {
global_var++;
printf("global_var in main: %d\n", global_var);
sleep(1);
}
return 0;
}
在这个例子中,non_reentrant_handler
函数不是可重入的,因为它修改了 global_var
这个全局变量,并且 main
函数也在修改这个变量。在多线程或者信号处理的情况下,这种操作可能会导致未定义行为。
信号屏蔽与未决信号
信号屏蔽是指阻止进程接收某些信号,直到这些信号被解除屏蔽。未决信号是指已经发送到进程,但由于信号屏蔽等原因尚未被处理的信号。在 C 语言中,我们可以使用 sigprocmask
函数来设置信号屏蔽字,使用 sigpending
函数来检查未决信号。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void signal_handler(int signum) {
printf("Received SIGINT\n");
}
int main() {
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
// 设置信号屏蔽,屏蔽 SIGINT 信号
if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) {
perror("sigprocmask");
return 1;
}
signal(SIGINT, signal_handler);
printf("SIGINT is blocked. Generating SIGINT...\n");
// 模拟发送 SIGINT 信号,此时信号会成为未决信号
raise(SIGINT);
sigset_t pending_set;
sigemptyset(&pending_set);
// 检查未决信号
if (sigpending(&pending_set) == -1) {
perror("sigpending");
return 1;
}
if (sigismember(&pending_set, SIGINT)) {
printf("SIGINT is pending.\n");
}
// 解除信号屏蔽
if (sigprocmask(SIG_UNBLOCK, &set, NULL) == -1) {
perror("sigprocmask");
return 1;
}
printf("SIGINT is unblocked. Now the signal will be handled.\n");
sleep(2); // 等待信号被处理
return 0;
}
在这个示例中,我们首先屏蔽了 SIGINT
信号,然后模拟发送 SIGINT
信号,此时信号成为未决信号。我们使用 sigpending
函数检查 SIGINT
是否为未决信号。最后,我们解除信号屏蔽,让信号可以被处理。
信号处理函数在多进程和多线程环境中的应用
多进程环境中的信号处理
在多进程编程中,信号处理需要特别注意。当一个进程 fork 出子进程时,子进程会继承父进程的信号处理设置。然而,在一些情况下,我们可能需要子进程有不同的信号处理方式。例如,在一个服务器程序中,父进程可能负责监听端口,而子进程负责处理客户端请求。我们可能希望子进程捕获 SIGCHLD
信号,以处理子进程的终止。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
void sigchld_handler(int signum) {
pid_t pid;
while ((pid = waitpid(-1, NULL, WNOHANG)) > 0) {
printf("Child process %d terminated.\n", pid);
}
}
int main() {
signal(SIGCHLD, sigchld_handler);
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
} else if (pid == 0) {
// 子进程
printf("Child process is running.\n");
sleep(2);
printf("Child process is exiting.\n");
_exit(0);
} else {
// 父进程
printf("Parent process is waiting...\n");
while (1) {
sleep(1);
}
}
return 0;
}
在这个示例中,父进程注册了 SIGCHLD
信号处理函数 sigchld_handler
。当子进程终止时,会发送 SIGCHLD
信号给父进程,父进程的 sigchld_handler
函数会调用 waitpid
函数来获取终止子进程的 PID,并打印相应信息。
多线程环境中的信号处理
在多线程编程中,信号处理更为复杂。默认情况下,信号会发送到进程中的任意一个线程。然而,我们可以使用 pthread_sigmask
函数来设置线程的信号屏蔽字,从而控制哪些线程接收信号。另外,我们可以使用 pthread_kill
函数向特定线程发送信号。
#include <stdio.h>
#include <pthread.h>
#include <signal.h>
#include <unistd.h>
void *thread_function(void *arg) {
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
// 线程屏蔽 SIGINT 信号
if (pthread_sigmask(SIG_BLOCK, &set, NULL) != 0) {
perror("pthread_sigmask");
return NULL;
}
while (1) {
printf("Thread is running...\n");
sleep(1);
}
return NULL;
}
int main() {
pthread_t thread;
if (pthread_create(&thread, NULL, thread_function, NULL) != 0) {
perror("pthread_create");
return 1;
}
sleep(2);
// 向线程发送 SIGINT 信号
if (pthread_kill(thread, SIGINT) != 0) {
perror("pthread_kill");
return 1;
}
// 主线程等待线程结束
if (pthread_join(thread, NULL) != 0) {
perror("pthread_join");
return 1;
}
return 0;
}
在这个示例中,我们创建了一个线程,并在该线程中屏蔽了 SIGINT
信号。主线程等待 2 秒后,使用 pthread_kill
函数向线程发送 SIGINT
信号。由于线程屏蔽了该信号,线程不会立即处理 SIGINT
信号,而是继续运行。如果我们想要在线程中处理 SIGINT
信号,可以在线程中注册信号处理函数,并在合适的时候解除信号屏蔽。
高级信号处理技术
实时信号
除了标准信号外,C 语言还支持实时信号。实时信号从 SIGRTMIN
到 SIGRTMAX
,它们具有以下特点:
- 排队机制:实时信号支持排队,这意味着如果多次发送同一个实时信号,信号处理函数会被多次调用,而标准信号通常不会排队,多次发送相同的标准信号可能只会导致信号处理函数被调用一次。
- 优先级:实时信号可以设置不同的优先级,较高优先级的实时信号会优先得到处理。
下面是一个简单的实时信号示例:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void rt_signal_handler(int signum) {
static int count = 0;
printf("Received real - time signal %d. Count: %d\n", signum, ++count);
}
int main() {
struct sigaction sa;
sa.sa_handler = rt_signal_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_SIGINFO;
if (sigaction(SIGRTMIN, &sa, NULL) == -1) {
perror("sigaction");
return 1;
}
printf("Sending real - time signals...\n");
for (int i = 0; i < 5; i++) {
if (raise(SIGRTMIN) == -1) {
perror("raise");
return 1;
}
sleep(1);
}
return 0;
}
在这个示例中,我们使用 sigaction
函数注册了 SIGRTMIN
实时信号的处理函数 rt_signal_handler
。然后在 main
函数中,通过 raise
函数多次发送 SIGRTMIN
信号。每次发送信号后,处理函数会打印接收到信号的次数。
信号集操作
信号集是一个数据结构,用于表示一组信号。在 C 语言中,我们使用 sigset_t
类型来表示信号集,并使用一系列函数来操作信号集,如 sigemptyset
(清空信号集)、sigaddset
(向信号集中添加信号)、sigdelset
(从信号集中删除信号)和 sigismember
(检查信号是否在信号集中)等。
#include <stdio.h>
#include <signal.h>
int main() {
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigaddset(&set, SIGTERM);
if (sigismember(&set, SIGINT)) {
printf("SIGINT is in the set.\n");
}
sigdelset(&set, SIGTERM);
if (!sigismember(&set, SIGTERM)) {
printf("SIGTERM is removed from the set.\n");
}
return 0;
}
在这个示例中,我们首先创建了一个空的信号集,然后添加了 SIGINT
和 SIGTERM
信号。接着,我们使用 sigismember
函数检查 SIGINT
是否在信号集中,并打印相应信息。之后,我们从信号集中删除 SIGTERM
信号,并再次使用 sigismember
函数检查 SIGTERM
是否在信号集中。
通过深入了解 C 语言信号处理函数,包括基本的信号注册与处理、常见信号的处理方式、信号处理的注意事项以及在多进程和多线程环境中的应用,还有高级信号处理技术等方面,开发者可以编写出更加健壮、可靠的程序,能够更好地应对各种异步事件的发生。无论是编写系统级程序、服务器应用还是其他类型的软件,掌握信号处理都是非常重要的。