MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C 语言信号处理函数

2024-10-301.1k 阅读

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 语言还支持实时信号。实时信号从 SIGRTMINSIGRTMAX,它们具有以下特点:

  1. 排队机制:实时信号支持排队,这意味着如果多次发送同一个实时信号,信号处理函数会被多次调用,而标准信号通常不会排队,多次发送相同的标准信号可能只会导致信号处理函数被调用一次。
  2. 优先级:实时信号可以设置不同的优先级,较高优先级的实时信号会优先得到处理。

下面是一个简单的实时信号示例:

#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;
}

在这个示例中,我们首先创建了一个空的信号集,然后添加了 SIGINTSIGTERM 信号。接着,我们使用 sigismember 函数检查 SIGINT 是否在信号集中,并打印相应信息。之后,我们从信号集中删除 SIGTERM 信号,并再次使用 sigismember 函数检查 SIGTERM 是否在信号集中。

通过深入了解 C 语言信号处理函数,包括基本的信号注册与处理、常见信号的处理方式、信号处理的注意事项以及在多进程和多线程环境中的应用,还有高级信号处理技术等方面,开发者可以编写出更加健壮、可靠的程序,能够更好地应对各种异步事件的发生。无论是编写系统级程序、服务器应用还是其他类型的软件,掌握信号处理都是非常重要的。