Linux信号机制

Linux常见信号

这些信号是Unix和类Unix操作系统中用于通知进程发生了某些事件的机制。信号可以由操作系统内核、用户或其他进程发送给目标进程。以下是这些信号的作用和效果的简要说明:

  1. SIGHUP (1): 当终端挂断(如用户注销)时发送,通常用于通知进程重新读取配置文件。
  2. SIGINT (2): 当用户按下Ctrl+C时发送,通常用于中断正在运行的程序。
  3. SIGQUIT (3): 当用户按下Ctrl+\时发送,通常导致进程终止并生成核心转储文件。
  4. SIGILL (4): 当程序尝试执行非法指令时发送,如非法操作码。
  5. SIGTRAP (5): 由调试器或trap系统调用生成,用于调试目的。
  6. SIGABRT (6): 当程序调用abort()函数时发送,通常导致进程异常终止并生成核心转储文件。
  7. SIGBUS (7): 当程序试图访问未分配的内存或执行其他非法的内存访问操作时发送。
  8. SIGFPE (8): 当程序执行算术错误(如除以零)时发送。
  9. SIGKILL (9): 用于立即终止进程,进程无法忽略或捕获此信号。
  10. SIGUSR1 (10): 用户定义的信号,通常用于进程间通信。
  11. SIGSEGV (11): 当程序试图访问无效的内存地址时发送。
  12. SIGUSR2 (12): 用户定义的信号,同样用于进程间通信。
  13. SIGPIPE (13): 当进程尝试向已关闭的管道或套接字写入数据时发送。
  14. SIGALRM (14): 当定时器到期时发送,用于定时操作。
  15. SIGTERM (15): 用于请求进程正常终止,进程可以捕获并执行清理操作。
  16. SIGSTKFLT (16): 当栈溢出时发送。
  17. SIGCHLD (17): 当子进程状态改变(如终止或停止)时发送给父进程。
  18. SIGCONT (18): 当进程从停止状态恢复执行时发送。
  19. SIGSTOP (19): 用于暂停进程的执行,进程无法捕获此信号。
  20. SIGTSTP (20): 当用户按下Ctrl+Z时发送,用于暂停进程。
  21. SIGTTIN (21): 当后台进程试图从终端读取输入时发送。
  22. SIGTTOU (22): 当后台进程试图向终端写入输出时发送。
  23. SIGURG (23): 当套接字上有带外数据时发送。
  24. SIGXCPU (24): 当进程超出CPU时间限制时发送。
  25. SIGXFSZ (25): 当进程尝试创建一个超过系统限制大小的文件时发送。
  26. SIGVTALRM (26): 当虚拟定时器到期时发送,用于定时操作。
  27. SIGPROF (27): 当CPU时间限制到期时发送,用于性能分析。
  28. SIGWINCH (28): 当终端窗口大小改变时发送。
  29. SIGIO (29): 当有I/O事件发生时发送,如文件描述符准备好读取或写入。
  30. SIGPWR (30): 当电源状态改变时发送,如电池电量低。
  31. SIGSYS (31): 当程序调用不支持的系统调用时发送。
  32. SIGRTMIN (34) 到 64: 这些是实时信号,用于进程间通信,通常由sigqueue()系统调用发送。

可靠信号和不可靠信号

不可靠信号和可靠信号的区别主要体现在信号处理的可靠性和信号的排队机制上。以下是它们的主要区别:

  1. 信号处理的可靠性

    • 不可靠信号:在早期的Unix系统中,信号处理后,信号的默认行为会被恢复,这意味着如果进程在处理信号后没有重新设置信号处理,那么后续的相同信号可能会被忽略。此外,不可靠信号在处理时可能会丢失,即如果信号发生的速度超过了进程处理的速度,那么只有最后一个信号会被处理,其他的信号可能会丢失。在Linux中,信号值小于SIGRTMIN(在Red Hat 7.2中,SIGRTMIN=32)的信号被认为是不可靠的。

    • 可靠信号:为了解决不可靠信号的问题,后来引入了可靠信号。可靠信号在处理后不会自动恢复默认行为,而且支持信号排队。这意味着即使信号发生的速度超过了进程处理的速度,所有信号都会被排队,进程可以在处理完当前信号后依次处理排队中的信号,从而避免了信号丢失。在Linux中,信号值位于SIGRTMINSIGRTMAX(在Red Hat 7.2中,SIGRTMAX=63)之间的信号被认为是可靠的。

  2. 信号的排队机制

    • 不可靠信号:不支持信号排队,即如果多个相同的不可靠信号连续发生,只有最后一个信号会被处理。

    • 可靠信号:支持信号排队,所有发生的信号都会被记录下来,进程可以按顺序处理这些信号。

在实际应用中,可靠信号通常用于需要确保所有信号都被正确处理的场景,而不可靠信号则可能因为其简单性而被使用在不需要严格顺序处理信号的场合。在Linux系统中,可以通过sigaction()系统调用来设置信号处理,这个调用支持可靠信号的排队机制。

信号安装

信号的安装(也称为信号处理程序的安装或注册)是指为特定的信号指定一个处理函数,以便当进程接收到该信号时,能够执行相应的动作。在Unix和类Unix操作系统中,信号是一种异步通知机制,用于通知进程发生了某些事件,如用户输入(如Ctrl+C)、系统错误、硬件中断等。

安装信号处理程序通常涉及以下几个步骤:

  1. 定义信号处理函数:首先,你需要定义一个函数,这个函数将在信号发生时被调用。这个函数通常接受一个整数参数,表示接收到的信号编号。

  2. 使用信号处理函数:在C语言中,你可以使用signal函数(在较新的系统中推荐使用sigaction函数)来安装信号处理程序。你需要指定信号编号和指向你的处理函数的指针。

  3. 处理信号:当信号发生时,操作系统会暂停当前进程的执行,调用你安装的处理函数。处理函数执行完毕后,进程会恢复到信号发生前的状态,除非处理函数中有明确的退出或继续执行的逻辑。

  4. 信号的默认行为:如果进程没有安装任何信号处理程序,或者安装的处理程序返回,那么信号的默认行为(如终止进程)将被执行。

  5. 信号的排队和处理:在某些情况下,如果信号发生得非常频繁,操作系统可能会将多个相同信号排队,等待处理程序处理。处理程序需要能够处理这种情况,确保不会遗漏任何信号。

信号的安装是进程间通信和错误处理的重要机制,它允许程序在运行时对各种异步事件做出响应。正确地安装和处理信号对于编写健壮的程序至关重要。

signal函数练习

  • 使用signal函数安装SIGINT信号处理方式,分别安装默认处理方式,忽略处理方式和自定义处理方式,并验证是否安装成功

    默认处理方式(SIG_DFL):默认处理方式通常会导致进程终止。在Unix系统中,SIGINT信号的默认行为是终止进程。

    //
    // Created by ubuntu on 24-2-12.
    //
    #include <stdlib.h>
    #include <stdio.h>
    #include <signal.h>
    #include <unistd.h>
    //默认处理方式通常会导致进程终止。在Unix系统中,SIGINT信号的默认行为是终止进程。
    
    void signal_handler(int signum) {
        printf("Received SIGINT, default behavior will terminate the program.\n");
    }
    
    int main() {
        if(signal(SIGINT,SIG_DFL) == SIG_ERR) {
            perror("signal");
            return 1;
        }
        printf("Press ctrl + c to trigger SIGINT and see the default behavior.\n");
        while(1) {
            sleep(1);
        }
    }
    

    image-20240212211501373

忽略处理方式(SIG_IGN): 忽略处理方式会导致进程对SIGINT信号不做任何响应。

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

int main() {
    // 设置忽略处理方式
    if (signal(SIGINT, SIG_IGN) == SIG_ERR) {
        perror("signal");
        return 1;
    }

    printf("Press Ctrl+C to see that the program ignores SIGINT.\n");
    while (1) {
        sleep(1); // 休眠1秒
    }

    return 0;
}

image-20240212211722188

自定义处理方式: 自定义处理方式允许你定义一个函数来处理SIGINT信号。

#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void signal_handler(int signum) {
    printf("Received SIGINT, custom handler is called.\n");
    // 在这里添加你的自定义逻辑
}

int main() {
    // 设置自定义处理方式
    if (signal(SIGINT, signal_handler) == SIG_ERR) {
        perror("signal");
        return 1;
    }

    printf("Press Ctrl+C to trigger SIGINT and see the custom handler.\n");
    while (1) {
        sleep(1); // 休眠1秒
    }

    return 0;
}

sigaction函数

struct sigaction

struct sigaction 是一个在 POSIX 标准中定义的结构体,用于描述信号处理的行为。这个结构体包含了信号处理函数、信号屏蔽集、信号处理属性等信息。以下是 struct sigaction 的主要成员:

struct sigaction {
    void (*sa_handler)(int);       // 信号处理函数
    void (*sa_sigaction)(int, siginfo_t *, void *); // 可选的信号处理函数,提供更多信息
    sigset_t sa_mask;            // 信号屏蔽集,指定在处理信号时需要屏蔽的信号
    int sa_flags;               // 信号处理属性,如SA_RESTART等
    void (*sa_restorer)(void);     // 已废弃,不应使用
};
  • sa_handler:这是一个函数指针,指向一个信号处理函数。当信号发生时,这个函数会被调用。这个函数通常接受一个整数参数,表示信号的编号。

  • sa_sigaction:这是一个可选的函数指针,指向一个更复杂的信号处理函数。这个函数接受三个参数:信号编号、一个指向 siginfo_t 结构的指针(包含信号的详细信息),以及一个用户定义的上下文指针。如果设置了 SA_SIGINFO 标志,那么 sa_sigaction 将被使用,而不是 sa_handler

  • sa_mask:这是一个信号集,用于指定在信号处理函数执行期间需要被屏蔽的信号。这是为了防止在处理信号时,同一信号或其他信号再次发生,导致信号处理函数被重复调用。

  • sa_flags:这是一个整数,用于设置信号处理的属性。可以设置的属性包括:

    • SA_RESTART:如果信号中断了进程中的系统调用,系统调用将自动重试。
    • SA_NOCLDSTOP:父进程在子进程暂停或继续时不会收到 SIGCHLD 信号。
    • SA_NOCLDWAIT:父进程在子进程退出时不会收到 SIGCHLD 信号,子进程不会成为僵尸进程。
    • SA_NODEFER:在信号处理函数执行期间,不会阻塞该信号。
    • SA_RESETHAND:信号处理后,信号处理函数重置为默认行为。
    • SA_SIGINFO:使用 sa_sigaction 而不是 sa_handler 作为信号处理函数。
  • sa_restorer:这个成员在现代系统中已经废弃,不应使用。在早期的实现中,它用于指定在信号处理结束后恢复的函数,但这个功能现在已经被移除。

struct sigaction 结构体通过 sigaction 函数与信号关联,允许开发者对信号处理进行更精细的控制。

sigaction函数

sigaction函数是POSIX标准中用于设置信号处理行为的函数。它提供了比传统的signal函数更灵活的信号处理方式,允许你指定更复杂的信号处理逻辑,包括信号处理函数、信号屏蔽集和信号处理的属性。

函数原型:

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  • signum:指定要设置处理方式的信号编号。
  • act:指向struct sigaction结构的指针,包含新的信号处理行为。
  • oldact:指向struct sigaction结构的指针,用于存储旧的信号处理行为,如果不需要,可以设置为NULL

结构体sigaction:

struct sigaction {
    void (*sa_handler)(int);       // 信号处理函数
    void (*sa_sigaction)(int, siginfo_t *, void *); // 可选的信号处理函数,提供更多信息
    sigset_t sa_mask;            // 信号屏蔽集,指定在处理信号时需要屏蔽的信号
    int sa_flags;               // 信号处理属性,如SA_RESTART等
    void (*sa_restorer)(void);     // 已废弃,不应使用
};
  • sa_handler:是信号处理函数,当信号发生时会被调用。
  • sa_sigaction:是可选的信号处理函数,它提供了额外的参数,允许处理更复杂的信号场景。
  • sa_mask:在信号处理期间,这个信号集内的信号将被屏蔽,防止它们在处理过程中再次发生。
  • sa_flags:可以设置多种标志,例如SA_RESTART(系统调用在信号处理后自动重试)。

简单使用例子:

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

// 信号处理函数
void signal_handler(int signum) {
    printf("Signal %d caught\n", signum);
}

int main() {
    struct sigaction sa;

    // 设置信号处理函数
    sa.sa_handler = signal_handler;
    sigemptyset(&sa.sa_mask); // 初始化信号屏蔽集
    sa.sa_flags = 0; // 不设置任何特殊标志

    // 使用sigaction设置信号处理
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction");
        return 1;
    }

    printf("Press Ctrl+C to generate SIGINT\n");
    while (1) {
        sleep(1); // 主循环,等待信号
    }

    return 0;
}

在这个例子中,我们定义了一个简单的信号处理函数signal_handler,它在接收到SIGINT信号时打印一条消息。然后,我们使用sigaction函数来设置这个处理函数,并在主循环中等待信号。当用户按下Ctrl+C时,程序会捕获SIGINT信号并调用signal_handler函数。