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

Linux C语言信号处理的异步通知

2024-09-205.1k 阅读

信号的基本概念

信号是什么

在Linux系统中,信号(Signal)是一种软件中断,用于在进程间传递事件异步通知。它类似于硬件中断,但由软件产生,用于通知进程发生了某个特定事件。信号可以由内核、其他进程或进程自身产生。例如,当用户在终端按下 Ctrl + C 组合键时,系统会向当前前台进程发送一个 SIGINT 信号,通知进程用户希望终止它。

信号的种类

Linux系统定义了多种信号,每种信号都有一个唯一的编号和名称。常见的信号包括:

  1. SIGINT:中断信号,通常由用户在终端按下 Ctrl + C 组合键产生,用于终止前台进程。
  2. SIGTERM:终止信号,用于请求进程正常终止。与 SIGKILL 不同,SIGTERM 允许进程捕获并执行清理操作。
  3. SIGKILL:强制终止信号,该信号不能被捕获或忽略,用于立即终止进程。
  4. SIGALRM:闹钟信号,由 alarm 函数设置的定时器到期时产生,常用于实现超时机制。
  5. SIGCHLD:子进程状态改变信号,当子进程终止、停止或继续运行时,父进程会收到该信号。

信号的处理方式

进程对信号有三种处理方式:

  1. 默认处理:每个信号都有默认的处理动作,例如 SIGINT 的默认处理是终止进程,SIGCHLD 的默认处理是忽略。
  2. 忽略信号:进程可以选择忽略某些信号,通过调用 signal 函数将信号的处理函数设置为 SIG_IGN 来实现。但某些信号,如 SIGKILLSIGSTOP,不能被忽略。
  3. 捕获信号:进程可以定义自己的信号处理函数,当收到指定信号时,系统会暂停当前执行的代码,转而执行信号处理函数。处理完毕后,再返回原来的执行点继续执行。

信号处理函数

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),它是一个信号集,用于指定当前被阻塞的信号。被阻塞的信号不会被立即处理,而是被挂起,直到信号被解除阻塞。

信号集操作函数

  1. sigemptyset:初始化一个空的信号集。
#include <signal.h>
int sigemptyset(sigset_t *set);
  1. sigfillset:初始化一个信号集,使其包含所有信号。
#include <signal.h>
int sigfillset(sigset_t *set);
  1. sigaddset:将指定信号添加到信号集中。
#include <signal.h>
int sigaddset(sigset_t *set, int signum);
  1. sigdelset:将指定信号从信号集中删除。
#include <signal.h>
int sigdelset(sigset_t *set, int signum);
  1. 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:指向要操作的信号集的指针。如果 howSIG_BLOCKSIG_SETMASK,则 set 不能为空。如果 howSIG_UNBLOCKset 可以为 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 为异步通知模式,需要执行以下步骤:

  1. 使用 fcntl 函数设置文件描述符为非阻塞模式。
  2. 使用 fcntl 函数设置文件描述符的所有者进程ID(通常是当前进程ID)。
  3. 使用 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_NONBLOCKO_ASYNC 标志使其成为非阻塞和异步通知模式。接着通过 fcntl(STDIN_FILENO, F_SETOWN, getpid()) 设置文件描述符的所有者为当前进程。当有数据输入到标准输入时,系统会发送 SIGIO 信号,sigio_handler 函数会被调用并读取数据。

基于信号的异步I/O

除了标准输入,异步通知机制也可以应用于其他文件描述符,如管道、套接字等。例如,在网络编程中,可以对套接字设置异步通知,当有数据到达套接字时,通过信号通知进程进行数据处理,这样可以避免在等待数据时阻塞进程,提高程序的并发处理能力。

信号处理中的注意事项

异步信号安全函数

在信号处理函数中,只能调用异步信号安全(Async - Signal - Safe)函数。这些函数不会被信号中断,也不会引起未定义行为。常见的异步信号安全函数包括 readwrite_exit 等。而像 printf 这类函数不是异步信号安全的,在信号处理函数中调用可能会导致程序崩溃或其他未定义行为。

可重入性

信号处理函数应该是可重入的,即当信号处理函数正在执行时,如果再次收到相同的信号,函数应该能够安全地再次进入并执行。这要求信号处理函数不依赖于共享的静态或全局变量,除非在进入函数时对这些变量进行了适当的保护(如使用互斥锁)。

信号处理与系统调用

一些系统调用在收到信号时会被中断。例如,readwriteaccept 等函数。默认情况下,这些被中断的系统调用会返回 -1 并设置 errnoEINTR。如果希望被中断的系统调用自动重新启动,可以在设置信号处理函数时使用 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;
}

在这个示例中,设置了 SIGIOSIGINT 信号的处理函数。对标准输入设置了异步通知模式,并阻塞和解除阻塞了 SIGIO 信号。程序会等待用户输入数据,当有数据输入时,通过 SIGIO 信号通知并读取数据,用户也可以通过按下 Ctrl + C 来终止程序。

通过以上内容,我们深入了解了Linux C语言中信号处理的异步通知机制,包括信号的基本概念、信号处理函数、信号的阻塞与解除阻塞、异步通知机制以及在信号处理中需要注意的事项,并通过多个代码示例进行了演示。掌握这些知识,能够让我们更好地编写在Linux环境下具有高响应性和稳定性的程序。