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

Linux C语言信号类型的捕获与忽略

2023-03-114.0k 阅读

信号的基本概念

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

信号有多种类型,每种类型都有特定的含义和默认处理动作。常见的信号类型包括:

  • SIGINT:中断信号,通常由用户在终端按下 Ctrl+C 组合键产生,默认处理动作是终止进程。
  • SIGTERM:终止信号,通常由系统管理员或其他进程发送,用于请求目标进程正常终止,默认处理动作也是终止进程。
  • SIGKILL:强制终止信号,不能被捕获或忽略,一旦发送,目标进程将立即被终止。
  • SIGSEGV:段错误信号,当进程访问非法内存地址时产生,默认处理动作是终止进程并产生核心转储文件(如果启用了核心转储功能)。

信号的处理方式

进程对信号的处理方式主要有以下三种:

  1. 默认处理:每个信号都有系统预设的默认处理动作,如前面提到的 SIGINTSIGTERM 的默认处理动作是终止进程,SIGSEGV 的默认处理动作是终止进程并产生核心转储文件。
  2. 忽略信号:进程可以选择忽略某些信号,即对该信号不做任何处理。例如,对于一些非关键的信号,进程可能选择忽略以避免不必要的干扰。
  3. 捕获信号:进程可以注册一个信号处理函数,当接收到特定信号时,系统会暂停当前进程的正常执行流程,转而执行信号处理函数。处理函数执行完毕后,进程恢复正常执行。

信号捕获

在C语言中,我们可以使用 signal 函数或 sigaction 函数来捕获信号。下面分别介绍这两个函数的使用方法。

signal函数

signal 函数是一个较老的用于处理信号的函数,其原型如下:

#include <signal.h>
void (*signal(int signum, void (*handler)(int)))(int);
  • signum:要捕获的信号类型,如 SIGINTSIGTERM 等。
  • handler:指向信号处理函数的指针。如果 handlerSIG_IGN,表示忽略该信号;如果 handlerSIG_DFL,表示采用默认处理方式;否则,handler 指向一个自定义的信号处理函数。

下面是一个使用 signal 函数捕获 SIGINT 信号的示例代码:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

// 信号处理函数
void sigint_handler(int signum) {
    printf("Received SIGINT signal. Exiting gracefully...\n");
    // 在这里可以进行一些清理工作,如关闭文件、释放资源等
    _exit(0);
}

int main() {
    // 注册信号处理函数
    signal(SIGINT, sigint_handler);

    printf("Press Ctrl+C to send SIGINT signal.\n");
    while (1) {
        // 模拟进程的正常工作
        sleep(1);
        printf("Process is running...\n");
    }

    return 0;
}

在上述代码中,我们定义了一个 sigint_handler 函数作为 SIGINT 信号的处理函数。在 main 函数中,使用 signal 函数将 SIGINT 信号与 sigint_handler 函数关联起来。当用户在终端按下 Ctrl+C 时,进程会接收到 SIGINT 信号,然后执行 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:指向一个更复杂的信号处理函数的指针,用于处理需要更多信息的信号(如 SIGCHLD 信号)。
  • sa_mask:一个信号集,用于指定在信号处理函数执行期间需要屏蔽的其他信号。
  • sa_flags:一些标志位,用于指定信号处理的行为,如 SA_RESTART 表示系统调用被信号中断后自动重新启动。

下面是一个使用 sigaction 函数捕获 SIGINT 信号的示例代码:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

// 信号处理函数
void sigint_handler(int signum) {
    printf("Received SIGINT signal. Exiting gracefully...\n");
    // 在这里可以进行一些清理工作,如关闭文件、释放资源等
    _exit(0);
}

int main() {
    struct sigaction act;

    // 初始化sigaction结构体
    act.sa_handler = sigint_handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;

    // 注册信号处理函数
    sigaction(SIGINT, &act, NULL);

    printf("Press Ctrl+C to send SIGINT signal.\n");
    while (1) {
        // 模拟进程的正常工作
        sleep(1);
        printf("Process is running...\n");
    }

    return 0;
}

在上述代码中,我们首先定义了一个 sigint_handler 信号处理函数。然后,在 main 函数中,初始化了一个 struct sigaction 结构体 act,将 act.sa_handler 设置为 sigint_handler,清空 act.sa_mask,并设置 act.sa_flags 为 0。最后,使用 sigaction 函数将 SIGINT 信号与 act 结构体关联起来,从而实现信号的捕获。

信号忽略

在C语言中,忽略信号非常简单,只需将 signal 函数或 sigaction 函数中的 handler 参数设置为 SIG_IGN 即可。下面是使用 signal 函数忽略 SIGINT 信号的示例代码:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int main() {
    // 忽略SIGINT信号
    signal(SIGINT, SIG_IGN);

    printf("Press Ctrl+C, nothing will happen.\n");
    while (1) {
        // 模拟进程的正常工作
        sleep(1);
        printf("Process is running...\n");
    }

    return 0;
}

在上述代码中,我们使用 signal(SIGINT, SIG_IGN) 语句将 SIGINT 信号设置为忽略状态。这样,当用户在终端按下 Ctrl+C 时,进程将不会对 SIGINT 信号做出任何响应,继续正常运行。

如果使用 sigaction 函数忽略信号,可以按照以下方式编写代码:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int main() {
    struct sigaction act;

    // 初始化sigaction结构体
    act.sa_handler = SIG_IGN;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;

    // 忽略SIGINT信号
    sigaction(SIGINT, &act, NULL);

    printf("Press Ctrl+C, nothing will happen.\n");
    while (1) {
        // 模拟进程的正常工作
        sleep(1);
        printf("Process is running...\n");
    }

    return 0;
}

在这个示例中,我们同样初始化了一个 struct sigaction 结构体 act,将 act.sa_handler 设置为 SIG_IGN,然后使用 sigaction 函数将 SIGINT 信号与 act 结构体关联起来,从而实现对 SIGINT 信号的忽略。

信号屏蔽与解除屏蔽

在信号处理过程中,有时我们需要临时屏蔽某些信号,以防止在执行关键代码段时被信号打断。在C语言中,可以使用 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:指向一个信号集的指针,用于指定要操作的信号。
  • oldset:指向一个信号集的指针,用于保存旧的信号屏蔽字。如果不需要保存旧设置,可以将其设置为 NULL

下面是一个示例代码,展示了如何屏蔽和解除屏蔽 SIGINT 信号:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int main() {
    sigset_t set, oldset;

    // 初始化信号集
    sigemptyset(&set);
    sigaddset(&set, SIGINT);

    // 屏蔽SIGINT信号
    sigprocmask(SIG_BLOCK, &set, &oldset);

    printf("SIGINT signal is blocked. Press Ctrl+C, nothing will happen.\n");
    sleep(5);

    // 解除屏蔽SIGINT信号
    sigprocmask(SIG_UNBLOCK, &set, NULL);

    printf("SIGINT signal is unblocked. Press Ctrl+C to send SIGINT signal.\n");
    while (1) {
        // 模拟进程的正常工作
        sleep(1);
        printf("Process is running...\n");
    }

    return 0;
}

在上述代码中,我们首先初始化了一个信号集 set,并将 SIGINT 信号添加到该信号集中。然后,使用 sigprocmask 函数将 SIGINT 信号屏蔽。在屏蔽期间,即使用户按下 Ctrl+C,进程也不会接收到 SIGINT 信号。5 秒后,我们使用 sigprocmask 函数解除对 SIGINT 信号的屏蔽,此时用户按下 Ctrl+C 就可以正常向进程发送 SIGINT 信号了。

可重入函数与信号处理

在编写信号处理函数时,需要注意可重入性问题。可重入函数是指可以被多个线程或信号处理函数安全调用的函数,即使在调用过程中被中断并再次调用,也不会出现数据损坏或其他错误。

一些常见的不可重入函数包括:

  • 标准I/O函数(如 printffopen 等):这些函数通常使用静态数据结构来管理缓冲区,在信号处理函数中调用可能会导致数据损坏。
  • 动态内存分配函数(如 mallocfree 等):这些函数也可能使用静态数据结构,在信号处理函数中调用可能会引发问题。

为了确保信号处理函数的可重入性,应尽量使用可重入函数。如果确实需要在信号处理函数中执行一些不可重入的操作,可以考虑使用信号屏蔽和解除屏蔽的方法,在执行关键代码段时屏蔽相关信号,避免信号处理函数的干扰。

例如,下面是一个使用可重入函数 write 来替代不可重入函数 printf 的信号处理函数示例:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>

// 信号处理函数
void sigint_handler(int signum) {
    const char *msg = "Received SIGINT signal. Exiting gracefully...\n";
    write(STDOUT_FILENO, msg, sizeof(msg) - 1);
    // 在这里可以进行一些清理工作,如关闭文件、释放资源等
    _exit(0);
}

int main() {
    // 注册信号处理函数
    signal(SIGINT, sigint_handler);

    printf("Press Ctrl+C to send SIGINT signal.\n");
    while (1) {
        // 模拟进程的正常工作
        sleep(1);
        printf("Process is running...\n");
    }

    return 0;
}

在上述代码中,sigint_handler 信号处理函数使用 write 函数输出提示信息,从而确保了函数的可重入性。

信号与系统调用

当进程正在执行一个系统调用时,如果接收到一个信号,系统调用可能会被中断。例如,当进程正在执行 read 系统调用从文件中读取数据时,如果接收到一个 SIGINT 信号,read 系统调用可能会被中断,返回 -1 并设置 errnoEINTR

在早期的UNIX系统中,被信号中断的系统调用默认不会自动重新启动,这就要求程序员在编写代码时要检查系统调用的返回值,并在 errnoEINTR 时手动重新调用系统调用。例如:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>

#define BUFFER_SIZE 1024

// 信号处理函数
void sigint_handler(int signum) {
    printf("Received SIGINT signal.\n");
}

int main() {
    int fd, n;
    char buffer[BUFFER_SIZE];

    // 注册信号处理函数
    signal(SIGINT, sigint_handler);

    // 打开文件
    fd = open("test.txt", O_RDONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    while (1) {
        // 从文件中读取数据
        n = read(fd, buffer, BUFFER_SIZE);
        if (n == -1) {
            if (errno == EINTR) {
                // 系统调用被信号中断,重新调用read
                continue;
            } else {
                perror("read");
                close(fd);
                return 1;
            }
        } else if (n == 0) {
            // 读取到文件末尾,退出循环
            break;
        } else {
            // 处理读取到的数据
            write(STDOUT_FILENO, buffer, n);
        }
    }

    close(fd);
    return 0;
}

在上述代码中,当 read 系统调用被 SIGINT 信号中断时(errnoEINTR),我们通过 continue 语句重新调用 read 系统调用,以确保数据读取的完整性。

在现代的UNIX系统(包括Linux)中,一些系统调用默认是可重启的,即被信号中断后会自动重新启动。例如,readwriteaccept 等系统调用在默认情况下是可重启的。如果希望禁用系统调用的自动重启功能,可以在 sigaction 函数的 sa_flags 字段中设置 SA_RESTART 标志。例如:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>

#define BUFFER_SIZE 1024

// 信号处理函数
void sigint_handler(int signum) {
    printf("Received SIGINT signal.\n");
}

int main() {
    int fd, n;
    char buffer[BUFFER_SIZE];
    struct sigaction act;

    // 初始化sigaction结构体
    act.sa_handler = sigint_handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = SA_RESTART; // 禁用系统调用的自动重启

    // 注册信号处理函数
    sigaction(SIGINT, &act, NULL);

    // 打开文件
    fd = open("test.txt", O_RDONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    while (1) {
        // 从文件中读取数据
        n = read(fd, buffer, BUFFER_SIZE);
        if (n == -1) {
            if (errno == EINTR) {
                // 系统调用被信号中断,手动重新调用read
                continue;
            } else {
                perror("read");
                close(fd);
                return 1;
            }
        } else if (n == 0) {
            // 读取到文件末尾,退出循环
            break;
        } else {
            // 处理读取到的数据
            write(STDOUT_FILENO, buffer, n);
        }
    }

    close(fd);
    return 0;
}

在上述代码中,我们通过设置 act.sa_flags = SA_RESTART 禁用了系统调用的自动重启功能,这样当 read 系统调用被 SIGINT 信号中断时,就需要我们手动重新调用 read 系统调用。

实时信号

到目前为止,我们讨论的信号大多是非实时信号,如 SIGINTSIGTERM 等。非实时信号有一些局限性,例如它们可能会丢失(如果在短时间内多次发送同一个非实时信号,进程可能只会接收到一次),并且它们的优先级是固定的。

为了克服这些局限性,Linux系统引入了实时信号。实时信号的编号从 SIGRTMINSIGRTMAX,不同的系统可能有不同的 SIGRTMINSIGRTMAX 值,但通常 SIGRTMIN 为 34,SIGRTMAX 为 64。

实时信号具有以下特点:

  • 可靠性:实时信号不会丢失,即使在短时间内多次发送同一个实时信号,进程也会接收到相应次数的信号。
  • 优先级:实时信号具有不同的优先级,信号编号越大,优先级越高。

在使用实时信号时,通常需要使用 sigaction 函数来注册信号处理函数,并设置 sa_sigaction 字段为一个更复杂的信号处理函数,该函数可以获取更多关于信号的信息。例如:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

// 实时信号处理函数
void rt_sig_handler(int signum, siginfo_t *info, void *context) {
    printf("Received real - time signal %d.\n", signum);
    printf("Sender's PID: %d\n", info->si_pid);
}

int main() {
    struct sigaction act;

    // 初始化sigaction结构体
    act.sa_sigaction = rt_sig_handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = SA_SIGINFO;

    // 注册实时信号处理函数
    sigaction(SIGRTMIN, &act, NULL);

    printf("Sending real - time signal SIGRTMIN...\n");
    // 发送实时信号
    kill(getpid(), SIGRTMIN);

    sleep(2);
    return 0;
}

在上述代码中,我们定义了一个 rt_sig_handler 函数作为实时信号 SIGRTMIN 的处理函数。该函数通过 siginfo_t 结构体获取了发送信号的进程的PID。在 main 函数中,我们使用 sigaction 函数注册了 SIGRTMIN 信号的处理函数,并设置 sa_flagsSA_SIGINFO,以启用获取信号详细信息的功能。然后,使用 kill 函数向自身发送了一个 SIGRTMIN 信号。

信号与多线程

在多线程程序中,信号的处理变得更加复杂。默认情况下,信号会发送到进程中的所有线程,并且只有一个线程可以处理信号。

为了在多线程程序中更好地处理信号,可以使用以下方法:

  • 主线程捕获信号:可以在主线程中注册信号处理函数,然后通过线程间通信机制(如管道、共享内存等)将信号信息传递给其他线程进行处理。
  • 线程特定信号处理:使用 pthread_sigmask 函数为每个线程设置不同的信号屏蔽字,使特定线程可以捕获特定信号。pthread_sigmask 函数的原型如下:
#include <pthread.h>
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);

其参数与 sigprocmask 函数类似,但作用于线程级别的信号屏蔽字。

下面是一个简单的示例,展示了如何在多线程程序中使用 pthread_sigmask 函数让特定线程捕获 SIGINT 信号:

#include <stdio.h>
#include <signal.h>
#include <pthread.h>
#include <unistd.h>

// 线程函数
void* thread_func(void* arg) {
    sigset_t set;
    int signum;

    // 初始化信号集
    sigemptyset(&set);
    sigaddset(&set, SIGINT);

    // 设置线程的信号屏蔽字,阻塞SIGINT信号
    pthread_sigmask(SIG_BLOCK, &set, NULL);

    // 等待SIGINT信号
    sigwait(&set, &signum);

    printf("Thread received SIGINT signal. Exiting...\n");
    pthread_exit(NULL);
}

int main() {
    pthread_t tid;

    // 创建线程
    if (pthread_create(&tid, NULL, thread_func, NULL) != 0) {
        perror("pthread_create");
        return 1;
    }

    printf("Press Ctrl+C to send SIGINT signal to the thread.\n");
    sleep(10);

    // 取消线程
    pthread_cancel(tid);
    pthread_join(tid, NULL);

    return 0;
}

在上述代码中,我们创建了一个新线程 thread_func。在 thread_func 中,首先使用 pthread_sigmask 函数阻塞了 SIGINT 信号,然后使用 sigwait 函数等待 SIGINT 信号。当主线程运行一段时间后,通过 pthread_cancel 函数取消线程。如果在这期间用户按下 Ctrl+CSIGINT 信号将被 thread_func 线程捕获并处理。

通过合理地使用信号处理机制,在多线程程序中可以实现更灵活、高效的异步事件处理。但需要注意的是,在多线程环境下处理信号时,要充分考虑线程安全问题,避免出现数据竞争和其他并发错误。

总结信号相关要点

  • 信号概念:信号是Linux系统中的异步通知机制,用于进程间传递事件信息,每种信号有特定含义和默认处理动作。
  • 处理方式:进程对信号可采用默认处理、忽略或捕获三种方式。捕获信号常用 signalsigaction 函数,sigaction 更灵活强大。
  • 信号忽略:通过设置 handlerSIG_IGN 可忽略信号,使用 signalsigaction 函数均可实现。
  • 信号屏蔽与解除屏蔽:利用 sigprocmask 函数可临时屏蔽或解除屏蔽信号,防止关键代码段被打断。
  • 可重入函数:编写信号处理函数应使用可重入函数,避免使用不可重入函数导致数据损坏,如用 write 替代 printf
  • 信号与系统调用:系统调用可能被信号中断,现代系统部分系统调用默认可重启,也可通过设置禁用自动重启,需处理 errnoEINTR 的情况。
  • 实时信号:具有可靠性和不同优先级,编号从 SIGRTMINSIGRTMAX,使用 sigaction 注册处理函数并设置 sa_sigaction 获取更多信号信息。
  • 信号与多线程:多线程中信号处理更复杂,可主线程捕获信号再通知其他线程,或用 pthread_sigmask 为特定线程设置信号屏蔽字捕获信号。