Linux系统编程(八):信号(下)

发布时间:2024年01月03日

参考引用

1. 基本概念

  • 信号是事件发生时对进程的通知机制,也可以把它称为软件中断
    • 信号与硬件中断的相似之处在于能够打断程序当前执行的正常流程,其实是在软件层次上对中断机制的一种模拟
    • 大多数情况下无法预测信号到达的准确时间,所以信号提供了一种处理异步事件的方法

1.1 信号的目的是用来通信

  • 一个具有合适权限的进程能够向另一个进程发送信号,信号的这一用法可作为一种同步技术,甚至是进程间通信(IPC)的原始形式。信号可以由 “谁” 发出呢?
    • 硬件检测到错误条件并通知内核,随即再由内核发送相应的信号给相关进程
    • 用于在终端下输入了能够产生信号的特殊字符
      • 按下 CTRL + C 组合按键可以产生中断信号(SIGINT),可以终止在前台运行的进程
      • 按下 CTRL + Z 组合按键可以产生暂停信号(SIGCONT),可以暂停当前前台运行的进程
    • 进程调用 kill() 系统调用可将任意信号发送给另一个进程或进程组(使用限制如下)
      • 接收信号的进程和发送信号的进程的所有者必须相同
      • 发送信号的进程的所有者是 root 超级用户
    • 用户可以通过 kill 命令将信号发送给其它进程
      • 如:在终端下执行 "kill -9 xxx"来杀死 PID 为 xxx 的进程
      • kill 命令其内部的实现原理便是通过 kill() 系统调用来完成的
    • 发生了软件事件,即当检测到某种软件条件已经发生
      • 进程所设置的定时器已经超时、进程执行的 CPU 时间超限、进程的某个子进程退出等

    进程同样也可以向自身发送信号,但发送给进程的诸多信号中,大多数都是来自于内核

1.2 信号由谁处理、怎么处理

  • 信号通常是发送给对应的进程,当信号到达后,该进程会视具体信号执行以下操作之一
    • 忽略信号
      • 大多数信号都可以使用这种方式进行处理,但 SIGKILL 和 SIGSTOP 绝对不能被忽略
      • 如果忽略某些由硬件异常产生的信号,则进程的运行行为是未定义的
    • 捕获信号
      • 当信号到达进程后,执行预先绑定好的信号处理函数
      • Linux 系统提供系统调用 signal() 用于注册信号处理函数
    • 执行系统默认操作
      • 进程不对该信号事件作出处理,而是交由系统进行处理,每一种信号都会有其对应的系统默认的处理方式
      • 对大多数信号来说,系统默认的处理方式就是终止该进程

1.3 信号是异步的

  • 信号是异步事件的经典实例,产生信号的事件对进程而言是随机出现的,进程无法预测该事件产生的准确时间,进程不能通过简单地测试一个变量或使用系统调用来判断是否产生了一个信号,这就如同硬件中断事件,程序是无法得知中断事件产生的具体时间,只有当产生中断事件时才会告知程序,然后打断当前程序的正常执行流程、跳转去执行中断服务函数,这就是异步处理方式

1.4 信号本质上是 int 类型数字编号

  • 信号本质上是 int 类型的数字编号,这就好比硬件中断所对应的中断号。内核针对每个信号,都给其定义了一个唯一的整数编号,从数字 1 开始顺序展开。并且每一个信号都有其对应的名字(其实就是一个宏),信号名字与信号编号是一一对应关系,但是由于每个信号的实际编号随着系统的不同可能会不一样,所定义)
  • 这些信号在 <signum.h> 头文件中定义,每个信号都是以 SIGxxx 开头
    • 不存在编号为 0 的信号,信号编号是从 1 开始的,事实上 kill() 函数对信号编号 0 有着特殊的应用
    /* Signals. */
    #define SIGHUP 1 /* Hangup (POSIX). */
    #define SIGINT 2 /* Interrupt (ANSI). */
    #define SIGQUIT 3 /* Quit (POSIX). */
    #define SIGILL 4 /* Illegal instruction (ANSI). */
    #define SIGTRAP 5 /* Trace trap (POSIX). */
    #define SIGABRT 6 /* Abort (ANSI). */
    #define SIGIOT 6 /* IOT trap (4.2 BSD). */
    #define SIGBUS 7 /* BUS error (4.2 BSD). */
    #define SIGFPE 8 /* Floating-point exception (ANSI). */
    #define SIGKILL 9 /* Kill, unblockable (POSIX). */
    #define SIGUSR1 10 /* User-defined signal 1 (POSIX). */
    #define SIGSEGV 11 /* Segmentation violation (ANSI). */
    #define SIGUSR2 12 /* User-defined signal 2 (POSIX). */
    #define SIGPIPE 13 /* Broken pipe (POSIX). */
    #define SIGALRM 14 /* Alarm clock (POSIX). */
    #define SIGTERM 15 /* Termination (ANSI). */
    #define SIGSTKFLT 16 /* Stack fault. */
    #define SIGCLD SIGCHLD /* Same as SIGCHLD (System V). */
    #define SIGCHLD 17 /* Child status has changed (POSIX). */
    #define SIGCONT 18 /* Continue (POSIX). */
    #define SIGSTOP 19 /* Stop, unblockable (POSIX). */
    #define SIGTSTP 20 /* Keyboard stop (POSIX). */
    #define SIGTTIN 21 /* Background read from tty (POSIX). */
    #define SIGTTOU 22 /* Background write to tty (POSIX). */
    #define SIGURG 23 /* Urgent condition on socket (4.2 BSD). */
    #define SIGXCPU 24 /* CPU limit exceeded (4.2 BSD). */
    #define SIGXFSZ 25 /* File size limit exceeded (4.2 BSD). */
    #define SIGVTALRM 26 /* Virtual alarm clock (4.2 BSD). */
    #define SIGPROF 27 /* Profiling alarm clock (4.2 BSD). */
    #define SIGWINCH 28 /* Window size change (4.3 BSD, Sun). */
    #define SIGPOLL SIGIO /* Pollable event occurred (System V). */
    #define SIGIO 29 /* I/O now possible (4.2 BSD). */
    #define SIGPWR 30 /* Power failure restart (System V). */
    #define SIGSYS 31 /* Bad system call. */
    #define SIGUNUSED 31
    

2. 信号的分类

2.1 可靠信号与不可靠信号

  • Linux 信号机制基本上是从 UNIX 系统中继承过来的,早期 UNIX 系统中的信号机制存在问题

    • 进程每次处理信号后,就将对信号的响应设置为系统默认操作
    • 早期 UNIX 下的不可靠信号主要是指
      • 进程可能对信号做出错误的反应
      • 信号可能丢失(处理信号时又来了新的信号,则导致信号丢失)
  • Linux 支持不可靠信号,但是对不可靠信号机制做了改进

    • 在调用完信号处理函数后,不必重新调用 signal(),Linux 下的不可靠信号问题主要指的是信号可能丢失
    • Linux 系统下,信号值小于 SIGRTMIN(34)的信号都是不可靠信号
  • 新增加了一些信号 (SIGRTMIN(34)~ SIGRTMAX(64)),并把它们定义为可靠信号

    • 编号 1~31 所对应的是不可靠信号,编号 34~64 对应的是可靠信号
    • 可靠信号支持排队,不会丢失,可靠信号并没有一个具体对应的名字
    $ kill -l
     1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
     6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
    11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
    16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
    21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
    26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
    31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
    38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
    43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
    48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
    53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
    58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
    63) SIGRTMAX-1	64) SIGRTMAX
    

2.2 实时信号与非实时信号

  • 实时信号与非实时信号其实是从时间关系上进行的分类,与可靠信号与不可靠信号相对应
    • 非实时信号都不支持排队,都是不可靠信号
    • 实时信号都支持排队,都是可靠信号
    • 实时信号保证了发送的多个信号都能被接收,实时信号是 POSIX 标准的一部分,可用于应用进程
    • 一般也把非实时信号(不可靠信号)称为标准信号

3. 常见信号与默认行为

  • 系统默认操作解析
    • term 表示终止进程
    • core 表示生成核心转储文件,核心转储文件可用于调试
    • ignore 表示忽略信号
    • cont 表示继续运行进程
    • stop 表示停止进程(停止不等于终止,而是暂停)
      在这里插入图片描述

4. 进程对信号的处理

  • 当进程接收到内核或用户发送过来的信号之后,根据具体信号可以采取不同的处理方式:忽略信号、捕获信号或者执行系统默认操作。Linux 系统提供了系统调用 signal() 和 sigaction() 两个函数用于设置信号的处理方式

4.1 signal() 函数

  • 系统调用 signal() 函数可将信号的处理方式设置为捕获信号、忽略信号以及系统默认操作

    #include <signal.h>
    
    typedef void (*sig_t)(int);
    sig_t signal(int signum, sig_t handler);
    
    • signum
      • 此参数指定需要进行设置的信号,可使用信号名(宏)或信号的数字编号,建议使用信号名
    • handler
      • sig_t 类型的函数指针,指向信号对应的信号处理函数,当进程接收到信号后会自动执行该处理函数
      • 参数 handler 既可以设置为用户自定义的函数,也就是捕获信号时需要执行的处理函数,也可以设置为 SIG_IGN 或 SIG_DFL:SIG_IGN 表示此进程需要忽略该信号,SIG_DFL 则表示设置为系统默认操作
      /* Fake signal functions */
      #define SIG_ERR ((sig_t) -1) /* Error return. */
      #define SIG_DFL ((sig_t) 0) /* Default action. */
      #define SIG_IGN ((sig_t) 1) /* Ignore signal. */
      
    • sig_t 函数指针 int 参数
      • 当前触发该函数的信号,可将多个信号绑定到同一个信号处理函数上,此时就可通过此参数来判断当前触发的是哪个信号
    • 返回值
      • 此函数的返回值也是一个 sig_t 类型的函数指针,成功情况下的返回值则是指向在此之前的信号处理函数;如果出错则返回 SIG_ERR,并会设置 errno
  • signal() 函数使用示例

    #include <stdio.h>
    #include <stdlib.h>
    #include <signal.h>
    
    static void sig_handler(int sig) {
        // 于测试程序中捕获了该信号,而对应的处理方式仅仅只是打印一条语句,而并不终止进程
        printf("Received signal: %d\n", sig);
    }   
    
    int main(int argc, char* argv[]) {
        sig_t ret = NULL;
    
        ret = signal(SIGINT, (sig_t)sig_handler);
        if (SIG_ERR == ret) {
            perror("signal error");
            exit(-1);
        }
        
        for (;;) {}
        
        exit(0);
    }
    
    $ gcc signal.c -o signal
    $ ./signal 
    ^CReceived signal: 2
    ^CReceived signal: 2
    ^CReceived signal: 2
    # 需另开一个终端 kill -9 xxxx(pid)
    Killed
    
    # 2617 为 ./signal 对应的 pid 号
    # SIGKILL(编号为 9)
    $ kill -9 2617
    

4.2 sigaction() 函数

  • 除了 signal() 之外,sigaction() 系统调用是设置信号处理方式的另一选择,推荐使用 sigaction() 函数。虽然 signal() 函数简单好用,而 sigaction() 更复杂,但 sigaction() 更具灵活性以及移植性

    • sigaction() 允许单独获取信号的处理函数而不是设置
    • 还可以设置各种属性对调用信号处理函数时的行为施以更加精准的控制
    #include <signal.h>
    
    // signum:需要设置的信号,除了 SIGKILL 信号和 SIGSTOP 信号之外的任何信号
    // act:一个 struct sigaction 类型指针,指向一个 struct sigaction 数据结构,该数据结构描述了信号的处理方式
        // 如果参数 act 不为 NULL,则表示需要为信号设置新的处理方式
        // 如果参数 act 为 NULL,则表示无需改变信号当前的处理方式
    // oldact:一个 struct sigaction 类型指针,指向一个 struct sigaction 数据结构
        // 如果参数 oldact 不为 NULL,则会将信号之前的处理方式等信息通过参数 oldact 返回出来
        // 如果无意获取此类信息,那么可将该参数设置为 NULL
    // 返回值:成功返回 0;失败将返回-1,并设置 errno
    int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
    
  • struct sigaction 结构体

    struct sigaction {
        void     (*sa_handler)(int);  // sa_handler:指定信号处理函数,与 signal() 函数的 handler 参数相同
        void     (*sa_sigaction)(int, siginfo_t *, void *);
        sigset_t sa_mask;
        int      sa_flags;
        void     (*sa_restorer)(void);  // sa_restorer:该成员已过时,不要再使用
    };
    
    • sa_sigaction
      • 也用于指定信号处理函数,是一个替代的信号处理函数,可提供更多的参数,通过该函数获取更多信息,这些信号通过 siginfo_t(man sigaction 查看) 参数获取
      • sa_handler 和 sa_sigaction 是互斥的,不能同时设置,对于标准信号使用 sa_handler,可通过 SA_SIGINFO 标志进行选择
    • sa_mask
      • 定义了一组信号,当进程在执行由 sa_handler 所定义的信号处理函数之前,会先将这组信号添加到进程的信号掩码字段中,当进程执行完处理函数之后再恢复信号掩码,将这组信号从信号掩码字段中删除
      • 信号掩码可以避免一些信号之间的竞争状态
    • sa_flags
      • 参数 sa_flags 指定了一组标志,这些标志用于控制信号的处理过程,可设置为如下这些标志(多个标志使用位或" | "组合)
      • SA_NOCLDSTOP:子进程停止时(即当它们接收到 SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU 中的一种时)或恢复(即它们接收到 SIGCONT)时不会收到 SIGCHLD 信号
      • SA_NOCLDWAIT:子进程终止时不要将其转变为僵尸进程
      • SA_NODEFER:默认情况下,期望进程在处理一个信号时阻塞同种信号,否则引起一些竞态条件;如果设置了 SA_NODEFER 标志,则表示不对它进行阻塞
      • SA_RESETHAND:执行完信号处理函数之后,将信号的处理方式设置为系统默认操作
      • SA_RESTART:被信号中断的系统调用,在信号处理完成之后将自动重新发起
      • SA_SIGINFO:表示使用 sigaction() 作为信号处理函数而不是 handler()
  • 示例:sigaction() 函数使用

    #include <stdio.h>
    #include <stdlib.h>
    #include <signal.h>
    
    static void sig_handler(int sig) {
        printf("Received signal: %d\n", sig);
    }
    
    int main(int argc, char *argv[]) {
        struct sigaction sig = {0};
        int ret;
    
        sig.sa_handler = sig_handler;
        sig.sa_flags = 0;
        ret = sigaction(SIGINT, &sig, NULL);
        if (-1 == ret) {
            perror("sigaction error");
            exit(-1);
        }
    
        /* 死循环 */
        for (;;) {}
    
        exit(0);
    }
    

5. 向进程发送信号

  • Linux 系统提供了 kill() 系统调用,一个进程可通过 kill()向另一个进程发送信号;除了 kill() 系统调用之外,还提供了系统调用 killpg() 以及库函数 raise(),也可用于实现发送信号的功能

5.1 kill() 函数

  • kill() 系统调用可将信号发送给指定的进程或进程组中的每一个进程

    #include <sys/types.h>
    #include <signal.h>
    
    int kill(pid_t pid, int sig);
    
    • pid 不同取值含义
      • 如果 pid 为正,则信号 sig 将发送到 pid 指定的进程
      • 如果 pid 等于 0,则将 sig 发送到当前进程的进程组中的每个进程
      • 如果 pid 等于-1,则将 sig 发送到当前进程有权发送信号的每个进程,但进程 1(init)除外
      • 如果 pid 小于-1,则将 sig 发送到 ID 为 -pid 的进程组中的每个进程
    • sig:参数 sig 指定需要发送的信号,也可设置为 0,如果参数 sig 设置为 0 则表示不发送信号,但任执行错误检查,这通常可用于检查参数 pid 指定的进程是否存在
    • 返回值:成功返回 0;失败将返回-1,并设置 errno
  • 示例:使用 kill() 函数向一个指定的进程发送信号

    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/types.h>
    #include <signal.h>
    
    int main(int argc, char* argv[]) {
        int pid;
    
        /* 判断传参个数 */
        if (argc < 2) {
            exit(-1);
        }
        
        /* 将传入的字符串转为整形数字 */
        pid = atoi(argv[1]);
        printf("pid: %d\n", pid);
    
        /* 向 pid 指定的进程发送信号 */
        if (kill(pid, SIGINT) == -1) {
            perror("kill error");
            exit(-1);
        }
        
        exit(0);
    }
    
    # 将上一小节代码运行于后台
    $ ./signal &
    [1] 2869
    $ Received signal: 2
    
    # 另开一个终端
    $ gcc kill.c -o kill
    $ ./kill 2869
    pid: 2869
    

5.2 raise() 函数

  • 有时进程需要向自身发送信号,raise()函数可用于实现这一要求
    • raise() 其实等价于 kill(getpid(), sig);
    #include <signal.h>
    
    // sig:需要发送的信号。
    // 返回值:成功返回 0;失败将返回非零值
    int raise(int sig);
    

6. alarm() 和 pause() 函数

6.1 alarm() 函数

  • 使用 alarm() 函数可以设置一个定时器(闹钟),当定时器时间到,内核会向进程发送 SIGALRM 信号

    • 参数 seconds 的值是产生 SIGALRM 信号需要经过的时钟秒数
    • 虽然 SIGALRM 信号的系统默认操作是终止进程,但如果程序当中设置了 alarm 闹钟,大多数使用闹钟的进程都会捕获此信号
    • alarm 闹钟并不能循环触发,只能触发一次,若想要实现循环触发,可以在 SIGALRM 信号处理函数中再次调用 alarm() 函数设置定时器
    #include <unistd.h>
    
    // seconds:设置定时时间,以秒为单位;如果为 0,则表示取消之前设置的 alarm 闹钟
    // 返回值:如果在调用 alarm() 时,之前已经为该进程设置了 alarm 闹钟还没有超时
        // 则该闹钟的剩余值作为本次 alarm() 函数调用的返回值,之前设置的闹钟则被新的替代;否则返回 0
    unsigned int alarm(unsigned int seconds);
    
  • 示例:alarm() 函数使用

    #include <stdio.h>
    #include <stdlib.h>
    #include <signal.h>
    #include <unistd.h>
    
    static void sig_handler(int sig) {
        puts("Alarm timeout");
        exit(0);
    }   
    
    int main(int argc, char *argv[]) {
        struct sigaction sig = {0};
        int second;
    
        /* 检验传参个数 */
        if (argc < 2) {
            exit(-1);
        }
        
        /* 为 SIGALRM 信号绑定处理函数 */
        sig.sa_handler = sig_handler;
        sig.sa_flags = 0;
        if (sigaction(SIGALRM, &sig, NULL) == -1) {
            perror("sigaction error");
            exit(-1);
        }
        
        /* 启动 alarm 定时器 */
        second = atoi(argv[1]);
        printf("定时时长: %d 秒\n", second);
        alarm(second);
    
        for (;;) {
            sleep(1);
        }
    
        exit(0);
    }
    
    $ gcc alarm.c -o alarm
    $ ./alarm 3
    定时时长: 3 秒
    Alarm timeout
    

6.2 pause() 函数

  • pause() 系统调用可以使得进程暂停运行、进入休眠状态,直到进程捕获到一个信号为止,只有执行了信号处理函数并从其返回时,pause() 才返回,在这种情况下,pause() 返回 -1,并且将 errno 设置为 EINTR

    #include <unistd.h>
    
    int pause(void);
    
  • 示例:alarm() 和 pause() 模拟 sleep

    #include <stdio.h>
    #include <stdlib.h>
    #include <signal.h>
    #include <unistd.h>
    
    static void sig_handler(int sig) {
        puts("Alarm timeout");
    }   
    
    int main(int argc, char *argv[]) {
        struct sigaction sig = {0};
        int second;
    
        /* 检验传参个数 */
        if (argc < 2) {
            exit(-1);
        }
        
        /* 为 SIGALRM 信号绑定处理函数 */
        sig.sa_handler = sig_handler;
        sig.sa_flags = 0;
        if (sigaction(SIGALRM, &sig, NULL) == -1) {
            perror("sigaction error");
            exit(-1);
        }
        
        /* 启动 alarm 定时器 */
        second = atoi(argv[1]);
        printf("定时时长: %d 秒\n", second);
        alarm(second);
    
        /* 进入休眠状态 */
        pause();
        puts("休眠结束");
        
        exit(0);
    }
    

7. 信号集

  • 信号集(signalset)用于表示多个信号(一组信号)的数据类型
  • 信号集其实就是 sigset_t 类型数据结构

7.1 初始化信号集

  • sigemptyset() 和 sigfillset() 用于初始化信号集

    • sigemptyset() 初始化信号集,使其不包含任何信号
    • sigfillset() 初始化信号集,使其包含所有信号(包括所有实时信号)
    #include <signal.h>
    
    // set:指向需要进行初始化的信号集变量
    // 返回值:成功返回 0;失败将返回 -1,并设置 errno
    int sigemptyset(sigset_t *set);
    int sigfillset(sigset_t *set);
    
  • 使用示例

    // 初始化为空信号集
    sigset_t sig_set;
    sigemptyset(&sig_set);
    
    // 初始化信号集,使其包含所有信号
    sigset_t sig_set;
    sigfillset(&sig_set);
    

7.2 向信号集中添加/删除信号

  • 分别使用 sigaddset() 和 sigdelset() 函数向信号集中添加或移除一个信号

    #include <signal.h>
    
    // set:指向信号集
    // signum:需要添加/删除的信号
    // 返回值:成功返回 0;失败将返回 -1,并设置 errno
    int sigaddset(sigset_t *set, int signum);
    int sigdelset(sigset_t *set, int signum);
    
  • 示例

    // 向信号集中添加信号
    sigset_t sig_set;
    
    sigemptyset(&sig_set);
    sigaddset(&sig_set, SIGINT);
    
    // 从信号集中移除信号
    sigset_t sig_set;
    
    sigfillset(&sig_set);
    sigdelset(&sig_set, SIGINT);
    

7.3 测试信号是否在信号集中

  • 使用 sigismember() 函数可以测试某一个信号是否在指定的信号集中

    #include <signal.h>
    
    // set:指定信号集
    // signum:需要进行测试的信号
    // 返回值:若 signum 在信号集 set 中,则返回 1;如果不在则返回 0;失败则返回 -1,并设置 errno
    int sigismember(const sigset_t *set, int signum);
    
  • 示例:判断 SIGINT 信号是否在 sig_set 信号集中

    sigset_t sig_set;
    ......
    if (sigismember(&sig_set, SIGINT) == 1) {
        puts("信号集中包含 SIGINT 信号");
    } else if (!sigismember(&sig_set, SIGINT)) {
        puts("信号集中不包含 SIGINT 信号");
    }
    

8. 获取信号的描述信息

  • 在 Linux 下,每个信号都有一串与之相对应的字符串描述信息,用于对该信号进行相应的描述

    • 这些字符串位于 sys_siglist 数组中,sys_siglist 数组是一个 char *类型的数组,数组中的每一个元素存放的是一个字符串指针,指向一个信号描述信息
  • 示例:从 sys_siglist 数组获取信号描述信息

    #include <signal.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void) {
        printf("SIGINT 描述信息: %s\n", sys_siglist[SIGINT]);
        printf("SIGQUIT 描述信息: %s\n", sys_siglist[SIGQUIT]);
        printf("SIGBUS 描述信息: %s\n", sys_siglist[SIGBUS]);
        exit(0);
    }
    
    $ gcc test.c -o test
    $ ./test
    SIGINT 描述信息: Interrupt
    SIGQUIT 描述信息: Quit
    SIGBUS 描述信息: Bus error
    

8.1 strsignal() 函数

  • 除了直接使用 sys_siglist 数组获取描述信息之外,还可以使用 strsignal() 库函数(推荐

    #include <string.h>
    
    char *strsignal(int sig);
    
  • 示例

    ...
    printf("SIGINT 描述信息: %s\n", strsignal(SIGINT));
    printf("SIGQUIT 描述信息: %s\n", strsignal(SIGQUIT));
    printf("SIGBUS 描述信息: %s\n", strsignal(SIGBUS));
    printf("编号为 1000 的描述信息: %s\n", strsignal(1000));
    ...
    

8.2 psignal() 函数

  • psignal() 可以在标准错误(stderr)上输出信号描述信息

    #include <signal.h>
    
    void psignal(int sig, const char *s);
    
  • 示例

    #include <signal.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main(void) {
        psignal(SIGINT, "SIGINT 信号描述信息");
        psignal(SIGQUIT, "SIGQUIT 信号描述信息");
        psignal(SIGBUS, "SIGBUS 信号描述信息");
        exit(0);
    }
    

9. 信号掩码 (阻塞信号传递)

  • 内核为每一个进程维护了一个信号掩码(其实就是一个信号集),即一组信号

    • 当进程接收到一个属于信号掩码中定义的信号时,该信号将会被阻塞,无法传递给进程进行处理
    • 直到该信号从信号掩码中移除,内核才会把该信号传递给进程处理
  • 向信号掩码中添加一个信号的方式

    • 当调用 signal() 或 sigaction() 为某一个信号设置处理方式时,进程会自动将该信号添加到信号掩码中
    • 使用 sigaction() 函数为信号设置处理方式时,可以额外指定一组信号,当调用信号处理函数时将该组信号自动添加到信号掩码中,当信号处理函数结束返回后,再将这组信号从信号掩码中移除
    • 使用 sigprocmask() 系统调用,随时可以显式地向信号掩码中添加/移除信号
    #include <signal.h>
    
    // how:指定了调用函数时的一些行为
        // SIG_BLOCK:将参数 set 所指向的信号集内的所有信号添加到进程的信号掩码中
        // SIG_UNBLOCK:将参数 set 指向的信号集内的所有信号从进程信号掩码中移除
        // SIG_SETMASK:进程信号掩码直接设置为参数 set 指向的信号集
    
    // set:将参数 set 指向的信号集内的所有信号添加到信号掩码中或者从信号掩码中移除
    // 如果参数 set 为 NULL,则表示无需对当前信号掩码作出改动
    
    // oldset:如果不为 NULL,在向信号掩码中添加新的信号之前,获取到进程当前的信号掩码,存放在 oldset 所指定的信号集中
    // 如果为 NULL 则表示不获取当前的信号掩码
    
    // 返回值:成功返回 0;失败将返回 -1,并设置 errno
    int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
    
  • 示例 1:将信号 SIGINT 添加到进程的信号掩码中

    int ret;
    
    /* 定义信号集 */
    sigset_t sig_set;
    
    /* 将信号集初始化为空 */
    sigemptyset(&sig_set);
    
    /* 向信号集中添加 SIGINT 信号 */
    sigaddset(&sig_set, SIGINT);
    
    /* 向进程的信号掩码中添加信号 */
    ret = sigprocmask(SIG_BLOCK, &sig_set, NULL);
    if (-1 == ret) {
        perror("sigprocmask error");
        exit(-1);
    }
    
  • 示例 2:从信号掩码中移除 SIGINT 信号

    ...
    /* 从信号掩码中移除信号 */
    ret = sigprocmask(SIG_UNBLOCK, &sig_set, NULL);
    if (-1 == ret) {
        perror("sigprocmask error");
        exit(-1);
    }
    
  • 测试信号掩码的作用

    #include <stdio.h>
    #include <stdlib.h>
    #include <signal.h>
    #include <unistd.h>
    
    static void sig_handler(int sig) {
        printf("执行信号处理函数...\n");
    }
    
    int main(void) {
        struct sigaction sig = {0};
        sigset_t sig_set;
    
        /* 注册信号处理函数 */
        sig.sa_handler = sig_handler;
        sig.sa_flags = 0;
        if (sigaction(SIGINT, &sig, NULL) == -1) {
            exit(-1);
        }
    
        /* 信号集初始化 */
        sigemptyset(&sig_set);
        sigaddset(&sig_set, SIGINT);  // 向信号集中添加 SIGINT 信号
    
        /* 向信号掩码中添加 SIGINT 信号 */
        if (sigprocmask(SIG_BLOCK, &sig_set, NULL) == -1) {
            exit(-1);
        }
    
        /* 向自己发送 SIGINT 信号 */
        // 如果信号掩码没有生效,应该会立马执行 sig_handler 函数
        raise(SIGINT);
    
        /* 休眠 2 秒 */
        // 2 秒后执行信号处理函数
        sleep(2);
        printf("休眠结束\n");
    
        /* 从信号掩码中移除添加的信号 */
        // 在移除之前接收到该信号会将其阻塞
        if (sigprocmask(SIG_UNBLOCK, &sig_set, NULL) == -1) {
            exit(-1);
        }
    
        exit(0);
    }
    
    $ gcc mask.c -o mask
    $ ./mask
    休眠结束
    执行信号处理函数...
    

10. 阻塞等待信号 sigsuspend()

  • 更改进程的信号掩码可以阻塞所选择的信号,或解除对它们的阻塞,可以保护不希望由信号中断的关键代码段。如果希望对一个信号解除阻塞后,然后调用 pause() 以等待之前被阻塞的信号的传递,如何实现?

    • 使用 sigsuspend() 系统调用将恢复信号掩码和 pause() 挂起进程这两个动作封装成一个原子操作
    #include <signal.h>
    
    // mask:参数 mask 指向一个信号集
    // 返回值:始终返回 -1,并设置 errno 来指示错误(通常为 EINTR),表示被信号所中断,如果调用失败,将 errno 设置为 EFAULT
    int sigsuspend(const sigset_t *mask);
    
    • sigsuspend() 会将进程的信号掩码设置为参数 mask 所指向的信号集,然后挂起进程,直到捕获到信号被唤醒(如果捕获的信号是 mask 信号集中的成员,将不会唤醒、继续挂起),并从信号处理函数返回,一旦从信号处理函数返回,sigsuspend() 会将进程的信号掩码恢复成调用前的值
  • sigsuspend() 函数使用示例

    • 希望执行受保护代码段时不被 SIGINT 中断信号打断,所以在执行保护代码段之前将 SIGINT 信号添加到进程的信号掩码中,执行完受保护的代码段之后,调用 sigsuspend() 挂起进程,等待被信号唤醒,被唤醒之后再解除 SIGINT 信号的阻塞状态
    #include <stdio.h>
    #include <stdlib.h>
    #include <signal.h>
    #include <unistd.h>
    
    static void sig_handler(int sig) {
        printf("执行信号处理函数...\n");
    }
    
    int main(void) {
        struct sigaction sig = {0};
        sigset_t new_mask, old_mask, wait_mask;
    
        /* 信号集初始化 */
        sigemptyset(&new_mask);
        sigaddset(&new_mask, SIGINT);
        sigemptyset(&wait_mask);
    
        /* 注册 SIGINT 信号处理函数 */
        sig.sa_handler = sig_handler;
        sig.sa_flags = 0;
        if (sigaction(SIGINT, &sig, NULL) == -1) {
            exit(-1);
        }
    
        /* 向信号掩码中添加 SIGINT 信号 */
        if (sigprocmask(SIG_BLOCK, &new_mask, &old_mask) == -1) {
            exit(-1);
        }
    
        /* 执行保护代码段 */
        puts("执行保护代码段");
        /******************/
    
        /* 挂起、等待信号唤醒 */
        if (sigsuspend(&wait_mask) != -1) {
            exit(-1);
        }
    
        /* 恢复信号掩码 */
        if (sigprocmask(SIG_SETMASK, &old_mask, NULL) == -1) {
            exit(-1);
        }    
    
        exit(0);
    }
    
    $ gcc susp.c -o susp
    $ ./susp
    执行保护代码段
    ^C执行信号处理函数...
    

11. 实时信号

  • 如果进程当前正在执行信号处理函数,在处理信号期间接收到了新的信号,如果该信号是信号掩码中的成员,那么内核会将其阻塞,将该信号添加到进程的等待信号集(等待被处理的信号)中,为确定进程中处于等待状态的是哪些信号,可以使用 sigpending() 函数获取

11.1 sigpending() 函数

#include <signal.h>

// set:处于等待状态的信号会存放在参数 set 所指向的信号集中
// 返回值:成功返回 0;失败将返回-1,并设置 errno
int sigpending(sigset_t *set);
  • 判断 SIGINT 信号当前是否处于等待状态
    /* 定义信号集 */
    sigset_t sig_set;
    
    /* 将信号集初始化为空 */
    sigemptyset(&sig_set);
    
    /* 获取当前处于等待状态的信号 */
    sigpending(&sig_set);
    
    /* 判断 SIGINT 信号是否处于等待状态 */
    if (sigismember(&sig_set, SIGINT) == 1) {
        puts("SIGINT 信号处于等待状态");
    } else if (!sigismember(&sig_set, SIGINT)) {
        puts("SIGINT 信号未处于等待状态");
    }
    

11.2 发送实时信号

  • 应用程序当中使用实时信号,需要有以下的两点要求

    • 发送进程使用 sigqueue() 系统调用向另一个进程发送实时信号以及伴随数据
    • 接收实时信号的进程要为该信号建立一个信号处理函数,使用 sigaction 为信号建立处理函数,并加入 SA_SIGINFO,这样信号处理函数才能接收到实时信号以及伴随数据,即使用 sa_sigaction 指针指向的处理函数,而不是 sa_handler,当然允许应用程序使用 sa_handler,但这样就不能获取到实时信号的伴随数据了
    #include <signal.h>
    
    // pid:指定接收信号的进程对应 pid,将信号发送给该进程
    // sig:指定需要发送的信号。与 kill() 函数一样,也可将参数 sig 设置为 0,用于检查参数 pid 所指定的进程是否存在
    // value:指定信号的伴随数据,union sigval 数据类型
    // 返回值:成功将返回 0;失败将返回-1,并设置 errno
    int sigqueue(pid_t pid, int sig, const union sigval value);
    
    // 携带的伴随数据,既可以指定一个整形的数据,也可以指定一个指针
    typedef union sigval {
        int sival_int;
        void *sival_ptr;
    } sigval_t;
    
  • 示例 1:使用 sigqueue() 函数发送信号

    #include <stdio.h>
    #include <stdlib.h>
    #include <signal.h>
    
    int main(int argc, char *argv[]) {
        union sigval sig_val;
        int pid;
        int sig;
    
        /* 判断传参个数 */
        if (argc < 3) {
            exit(-1);
        }
    
        /* 获取用户传递的参数 */
        pid = atoi(argv[1]);
        sig = atoi(argv[2]);
        printf("pid: %d\nsignal: %d\n", pid, sig);
    
        /* 发送信号 */
        sig_val.sival_int = 10; //伴随数据
        if (sigqueue(pid, sig, sig_val) == -1) {
            perror("sigqueue error");
            exit(-1);
        }
        puts("信号发送成功!");
        exit(0);
    }
    
  • 示例 2:使用 sigaction() 函数为实时信号绑定处理函数

    #include <stdio.h>
    #include <stdlib.h>
    #include <signal.h>
    #include <unistd.h>
    
    static void sig_handler(int sig, siginfo_t *info, void *context) {
        sigval_t sig_val = info->si_value;
        printf("接收到实时信号: %d\n", sig);
        printf("伴随数据为: %d\n", sig_val.sival_int);
    }
    
    int main(int argc, char *argv[]) {
        struct sigaction sig = {0};
        int num;
    
        /* 判断传参个数 */
        if (argc < 2) {
            exit(-1);
        }
    
        /* 获取用户传递的参数 */
        num = atoi(argv[1]);
    
        /* 为实时信号绑定处理函数 */
        sig.sa_sigaction = sig_handler;
        sig.sa_flags = SA_SIGINFO;
        if (sigaction(num, &sig, NULL) == -1) {
            perror("sigaction error");
            exit(-1);
        }
    
        /* 死循环 */
        for (;;) {
            sleep(1);
        }
    
        exit(0);
    }
    

12. 异常退出 abort() 函数

  • 使用 exit()、_exit() 或 _Exit() 这些函数来终止进程,通常用于正常退出应用程序,而对于异常退出程序,则一般使用 abort() 库函数,使用 abort() 终止进程运行,会生成核心转储文件,用于判断程序调用 abort() 时的程序状态

    • 函数 abort() 通常产生 SIGABRT 信号来终止调用该函数的进程,SIGABRT 信号的系统默认操作是终止进程运行并生成核心转储文件;当调用 abort() 函数之后,内核会向进程发送 SIGABRT 信号
    #include <stdlib.h>
    
    void abort(void);
    
  • 示例:abort() 终止进程

    • 无论阻塞或忽略 SIGABRT 信号,abort() 调用均不受影响,总会成功终止进程
    #include <stdio.h>
    #include <stdlib.h>
    #include <signal.h>
    #include <unistd.h>
    
    static void sig_handler(int sig) {
        printf("接收到信号: %d\n", sig);
    }
    
    int main(int argc, char *argv[]) {
        struct sigaction sig = {0};
        sig.sa_handler = sig_handler;
        sig.sa_flags = 0;
        if (-1 == sigaction(SIGABRT, &sig, NULL)) {
            perror("sigaction error");
            exit(-1);
        }
    
        sleep(2);
        abort(); // 调用 abort
        for (;;) {
            sleep(1);
        }
        
        exit(0);
    }
    
    $ gcc abort.c -o abort
    $ ./abort 
    接收到信号: 6
    Aborted (core dumped)
    
文章来源:https://blog.csdn.net/qq_42994487/article/details/135359573
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。