Linux C应用——信号

信号的概念

  • 信号是发生事件时对进程的一种通知机制
    • 通知机制:说明信号的目的是用来通信的
    • 对进程:说明进程是信号的接收者
    • 发生事件时:说明在发生某些事件时会产生信号

信号发送者

一个具有合适权限的进程能够向另一个进程发送信号,信号的这一用法可作为一种同步技术,甚至是进程间通信(IPC)的原始形式。信号的发送者可以是进程或者内核,以下情况均可以产生信号:

进程处理信号的方式

  • 忽略信号。也就是说,当信号到达进程后,该进程并不会去理会它、直接忽略,就好像是没有出该信号,信号对该进程不会产生任何影响。事实上,大多数信号都可以使用这种方式进行处理,但有两种信号却决不能被忽略,它们是 SIGKILLSIGSTOP,这两种信号不能被忽略的原因是:它们向内核和超级用户提供了使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号,则进程的运行行为是未定义的。
  • 捕获信号。当信号到达进程后,执行预先绑定好的信号处理函数。为了做到这一点,要通知内核在某种信号发生时,执行用户自定义的处理函数,该处理函数中将会对该信号事件作出相应的处理,Linux 系统提供了 **signal()**系统调用可用于注册信号的处理函数
  • 执行系统默认操作。进程不对该信号事件作出处理,而是交由系统进行处理,每一种信号都会有其对应的系统默认的处理方式。需要注意的是,对大多数信号来说,系统默认的处理方式就是终止该进程。

信号是异步的

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

信号本质是 int 型数字编号

  • kill -l 可以打印出所有的信号

信号的分类

早期信号存在的问题

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

    1. 进程每次处理信号后,就对信号的响应设置为系统默认操作,如果用户如果不希望这样的操作,那么就要在信号处理函数结尾再一次调用 signal(),重新为该信号绑定相应的处理函数
    2. 信号可能会丢失。在处理信号时(信号处理函数中)如果来新的信号,要么打断当前信号处理函数,要么阻塞等待,假设按照阻塞等待(有些信号不能被阻塞),新来的信号会被放入等待信号集,由于这个信号集是一个集合 set,不能有重复的数值,因此如果新的信号有重复的,只会保存一个,这样就会造成信号丢失
  • Linux 系统解决了问题1

信号分类

  • 信号可分为可靠信号与不可靠信号

    • 不可靠信号(1-31):Linux 下信号不可靠主要是指信号可能会丢失,不可靠信号有具体的名字,对应具体的功能
    • 可靠信号(34-64):可靠信号支持排队,不会丢失,可靠信号并没有一个具体对应的名字,而是使用了 SIGRTMIN+N 或 SIGRTMAX-N 的方式来表示
  • 实时信号与非实时信号其实是从时间关系上进行的分类,与可靠信号与不可靠信号是相互对应的,非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。实时信号保证了发送的多个信号都能被接收,实时信号是 POSIX 标准的一部分,可用于应用进程。 一般我们也把非实时信号(不可靠信号)称为标准信号

常见信号介绍(标准信号)

SIGINT

  • 当用户在终端按下中断字符(通常是 CTRL + C)时,内核将发送 SIGINT 信号给前台进程组中的每一个进程(占用当前终端的进程)。该信号的系统默认操作是终止进程的运行。所以通常我们都会使用 CTRL + C 来终止一个占用前台的进程,原因在于大部分的进程会将该信号交给系统去处理,从而执行该信号的系统默认操作
  • 使用 kill -2 PID 也可以终止某个进程(2 可以使用 kill -l 查看信号的编号)

SIGQUIT

  • 当用户在终端按下退出字符(通常是 CTRL + **)时,内核将发送 SIGQUIT 信号给前台进程组中的每一个进程。该信号的系统默认操作是终止进程的运行**、并生成可用于调试的核心转储文件进程如果陷入无限循环或不再响应时,使用 SIGQUIT 信号就很合适。所以对于一个前台进程,既可以在终端按下中断字符 CTRL + C、也可以按下退出字符 CTRL + \来终止,当然前提条件是,此进程会将 SIGINT 信号或 SIGQUIT 信号交给系统处理(也就是没有将信号忽略或捕获),进入执行该信号所对应的系统默认操作

SIGILL

  • 如果进程试图执行非法(即格式不正确)的机器语言指令,系统将向进程发送该信号。该信号的系统默认操作是终止进程的运行

SIGABRT

  • 当进程调用 abort()系统调用时(进程异常终止),系统会向该进程发送 SIGABRT 信号。该信号的系统默认操作是终止进程、并生成核心转储文件

SIGBUS

  • 产生该信号(总线错误,bus error)表示发生了某种内存访问错误。该信号的系统默认操作是终止进程

SIGFPE

  • 该信号因特定类型的算术错误而产生,譬如除以 0。该信号的系统默认操作是终止进程

SIGKILL

  • 此信号为“必杀(sure kill)”信号,用于杀死进程的终极办法,此信号无法被进程阻塞、忽略或者捕获,故而“一击必杀”,总能终止进程。使用 SIGINT 信号和 SIGQUIT 信号虽然能终止进程,但是前提条件是该进程并没有忽略或捕获这些信号,如果使用 SIGINT 或 SIGQUIT 无法终止进程,那就使用“必杀信号”SIGKILL 吧(这种方式不友好,只是作为最后的手段)

SIGUSR1/SIGUSR2

  • SIGUSR1/SIGUSR2 信号供程序员自定义使用,内核绝不会为进程产生这些信号,在我们的程序中,可以使用这些信号来互通通知事件的发生,或是进程彼此同步操作。该信号的系统默认操作是终止进程

SIGSEGV

  • 这一信号非常常见,当应用程序对内存的引用无效时,操作系统就会向该应用程序发送该信号。引起对内存无效引用的原因很多,C 语言中引发这些事件往往是解引用的指针里包含了错误地址(比如未初始化的指针),或者传递了一个无效参数供函数调用等。该信号的系统默认操作是终止进程

SIGPIPE

  • 涉及到管道和 socket,当进程向已经关闭的管道、FIFO 或套接字写入信息时,那么系统将发送该信号给进程。该信号的系统默认操作是终止进程

SIGALRM

  • 与系统调用 alarm()或 setitimer()有关,应用程序中可以调用 alarm()或 setitimer()函数来设置一个定时器,当定时器定时时间到,那么内核将会发送 SIGALRM 信号给该应用程序。该信号的系统默认操作是终止进程

SIGTERM

  • 这是用于终止进程的标准信号,也是 kill 命令所发送的默认信号(kill xxx,xxx 表示进程 pid),有时我们会直接使用"kill -9 xxx"显式向进程发送 SIGKILL 信号来终止进程,然而这一做法通常是错误的,精心设计的应用程序应该会捕获** SIGTERM 信号、并为其绑定一个处理函数**,当该进程收到 SIGTERM 信号时,会在处理函数中清除临时文件以及释放其它资源,再退出程序。如果直接使用 SIGKILL 信号终止进程, 从而跳过了 SIGTERM 信号的处理函数,通常 SIGKILL 终止进程是不友好的方式、是暴力的方式,这种方式应该作为最后手段,应首先尝试使用 SIGTERM,实在不行再使用最后手段 SIGKILL

SIGCHLD

  • 当父进程的某一个子进程终止时,内核会向父进程发送该信号。当父进程的某一个子进程因收到信号而停止或恢复时,内核也可能向父进程发送该信号。注意这里说的停止并不是终止,你可以理解为暂停。该信号的系统默认操作是忽略此信号,如果父进程希望被告知其子进程的这种状态改变,则应捕获此信号

SIGCLD

  • 与 SIGCHLD 信号同义

SIGCONT

  • 将该信号发送给已停止的进程,进程将会恢复运行。当进程接收到此信号时并不处于停止状态,系统默认操作是忽略该信号,但如果进程处于停止状态,则系统默认操作是使该进程继续运行。

SIGSTOP

  • 这是一个“必停”信号,用于停止进程(注意停止不是终止,停止只是暂停运行、进程并没有终止),应用程序无法将该信号忽略或者捕获,故而总能停止进程。

SIGTSTP

  • 这也是一个停止信号,当用户在终端按下停止字符(通常是 CTRL + Z),那么系统会将 SIGTSTP 信号发送给前台进程组中的每一个进程,使其停止运行(可以被应用程序忽略或者捕获)

SIGXCPU

  • 当进程的 CPU 时间超出对应的资源限制时,内核将发送此信号给该进程

SIGPOLL/SIGIO

  • 这两个信号同义。这两个信号用于提示一个异步 IO 事件的发生,譬如应用程序打开的文件描述符发生了 I/O 事件时,内核会向应用程序发送 SIGIO 信号

SIGSYS

  • 如果进程发起的系统调用有误,那么内核将发送该信号给对应的进程

在这里插入图片描述

进程对信号的处理

signal 函数

  • signal 函数可将信号的处理方式设置为捕获信号、忽略信号以及系统默认操作
#include <signal.h>

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
  • 参数含义:

    • signum:此参数指定需要进行设置的信号(除了 SIGKILL 信号和 SIGSTOP 信号之外的任何信号),可使用信号名(宏)或信号的数字编号,建议使用信号名

    SIGKILL 和 SIGSTOP 信号都不能被忽略或者捕获

    • handler:sighandler_t 类型的函数指针,指向信号对应的信号处理函数,当进程接收到信号后会自动执行该处理函数;参数 handler 既可以设置为用户自定义的函数,也就是捕获信号时需要执行的处理函数,也可以设置为 SIG_IGN(1) 或 SIG_DFL(0),SIG_IGN 表示此进程需要忽略该信号,SIG_DFL 则表示设置为系统默认操作。 sighandler_t 函数指针的 int 类型参数指的是,当前触发该函数的信号,可将多个信号绑定到同一个信号处理函数上,此时就可通过此参数来判断当前触发的是哪个信号(handler 函数的参数为绑定的信号)
    • 返回值:此函数的返回值也是一个 sighandler_t 类型的函数指针,成功情况下的返回值则是指向在此之前的信号处理函数;如果出错则返回 SIG_ERR(-1),并会设置 errno
  • signal 函数使用:

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

static void sig_handler(int sig){
    printf("Receive signal: %d\n", sig);
}
typedef void (*sighandler_t)(int);
int main(int argc, char* argv[]){
    sighandler_t ret;
    ret = signal(SIGINT, (sighandler_t)sig_handler);
    if(ret == SIG_ERR){
        perror("signal error");
        return -1;
    }
    while(1){
        sleep(1);
    }
    return 0;
}

sigaction 函数

  • sigaction()允许单独获取信号的处理函数而不是设置,并且还可以设置各种属性对调用信号处理函数时的行为施以更加精准的控制(推荐使用这个函数)
#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  • 参数含义:

    • signum:需要设置的信号,除了 SIGKILL 信号和 SIGSTOP 信号之外的任何信号。
    • act:act 参数是一个 struct sigaction 类型指针,指向一个 struct sigaction 数据结构,该数据结构描述了信号的处理方式;如果参数 act 不为 NULL,则表示需要为信号设置新的处理方式;如果参数 act 为 NULL,则表示无需改变信号当前的处理方式。
    • oldact:oldact 参数也是一个 struct sigaction 类型指针,指向一个 struct sigaction 数据结构。如果参数 oldact 不为 NULL,则会将信号之前的处理方式等信息通过参数 oldact 返回出来;如果无意获取此类信息,那么可将该参数设置为 NULL。
    • 返回值:成功返回 0;失败将返回-1,并设置 errno。
  • 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:也用于指定信号处理函数,这是一个替代的信号处理函数,他提供了更多的参数,可以通过该函数获取到更多信息,这些信号通过 siginfo_t 参数获取;sa_handler 和 sa_sigaction 是互斥的,不能同时设置,对于标准信号来说,使用 sa_handler 就可以了,可通过 SA_SIGINFO 标志进行选择
      • sa_sigaction 第一个参数为指定的信号,第二个参数为信号的信息,第三个参数为进程上下文
    • sa_mask:参数 sa_mask 定义了一组信号,当进程在执行由 sa_handler 所定义的信号处理函数之前,会先将这组信号添加到进程的信号掩码字段中,当进程执行完处理函数之后再恢复信号掩码,将这组信号从信号掩码字段中删除。当进程在执行信号处理函数期间,可能又收到了同样的信号或其它信号,从而打断当前信号处理函数的执行,这就好点像中断嵌套;通常我们在执行信号处理函数期间不希望被另一个信号所打断,那么怎么做呢?那么就是通过信号掩码来实现(阻塞等待),如果进程接收到了信号掩码中的这些信号,那么这个信号将会被阻塞暂时不能得到处理,直到这些信号从进程的信号掩码中移除。在信号处理函数调用时,进程会自动将当前处理的信号添加到信号掩码字段中,这样保证了在处理一个给定的信号时,如果此信号再次发生,那么它将会被阻塞。如果用户还需要在阻塞其它的信号,则可以通过设置参数 sa_mask 来完成(此参数是 sigset_t 类型变量,表示信号集),信号掩码可以避免一些信号之间的竞争状态(也称为竞态)。
  • sa_flags:参数 sa_flags 指定了一组标志,这些标志用于控制信号的处理过程

    • SA_NOCLDSTOP:如果 signum 为 SIGCHLD,则子进程停止时(即当它们接收到 SIGSTOP、SIGTSTP、SIGTTIN 或 SIGTTOU 中的一种时)或恢复(即它们接收到 SIGCONT)时不会收到 SIGCHLD 信号
    • SA_NOCLDWAIT:如果 signum 是 SIGCHLD,则在子进程终止时不要将其转变为僵尸进程
    • SA_NODEFER:不要阻塞从某个信号自身的信号处理函数中接收此信号。也就是说当进程此时正在执行某个信号的处理函数,默认情况下,进程会自动将该信号添加到进程的信号掩码字段中,从而在执行信号处理函数期间阻塞该信号,默认情况下,我们期望进程在处理一个信号时阻塞同种信号,否则引起一些竞态条件;如果设置了 SA_NODEFER 标志,则表示不对它进行阻塞
    • SA_RESETHAND:执行完信号处理函数之后,将信号的处理方式设置为系统默认操作
    • SA_RESTART:被信号中断的系统调用,在信号处理完成之后将自动重新发起
    • SA_SIGINFO:如果设置了该标志,则表示使用 sa_sigaction 作为信号处理函数、而不是 sa_handler
  • sigaction 函数使用:

struct sigaction sig = {0};  
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
ret = sigaction(SIGINT, &sig, NULL);
  • siginfo_t 结构体:
siginfo_t {
    int      si_signo;     /* Signal number */   // 表示当前触发信号处理函数的信号是哪个
    int      si_errno;     /* An errno value */  // 错误码,当它为非零值时,表示信号发生了错误
    int      si_code;      /* Signal code */     // 用来描述触发信号的原因(用户主动发送、内核发送等)(SI_USER、                                                           SI_KERNEL)
    int      si_trapno;    /* Trap number that caused        
                              hardware-generated signal
                              (unused on most architectures) */
    pid_t    si_pid;       /* Sending process ID */    // 发送信号的进程
    uid_t    si_uid;       /* Real user ID of sending process */  
    int      si_status;    /* Exit value or signal */  // 只对SIGCHLD有效,表示子进程退出时的状态
    clock_t  si_utime;     /* User time consumed */    // 只对SIGCHLD有效,表示子进程终止时所消耗的用户CPU时间
    clock_t  si_stime;     /* System time consumed */  // 只对SIGCHLD有效,表示子进程终止时所消耗的系统CPU时间
    union sigval si_value; /* Signal value */          // 发送信号时所携带的伴随数据
    int      si_int;       /* POSIX.1b signal */       // 伴随数据中的int类型变量  info->sig_value.sival_int
    void    *si_ptr;       /* POSIX.1b signal */       // 伴随数据中的指针变量
    int      si_overrun;   /* Timer overrun count;
                              POSIX.1b timers */
    int      si_timerid;   /* Timer ID; POSIX.1b timers */
    void    *si_addr;      /* Memory location which caused fault */
    long     si_band;      /* Band event (was int in       // 只对SIGIO信号有效
                              glibc 2.3.2 and earlier) */
    int      si_fd;        /* File descriptor */          // 只对SIGIO信号有效
    short    si_addr_lsb;  /* Least significant bit of address
                              (since Linux 2.6.32) */
    void    *si_lower;     /* Lower bound when address violation
                              occurred (since Linux 3.19) */
    void    *si_upper;     /* Upper bound when address violation
                              occurred (since Linux 3.19) */
    int      si_pkey;      /* Protection key on PTE that caused
                              fault (since Linux 4.6) */
    void    *si_call_addr; /* Address of system call instruction
                              (since Linux 3.5) */
    int      si_syscall;   /* Number of attempted system call
                              (since Linux 3.5) */
    unsigned int si_arch;  /* Architecture of attempted system call
                              (since Linux 3.5) */
};

一般而言,将信号处理函数设计越简单越好,这就好比中断处理函数,越快越好,不要在处理函数中做大量消耗 CPU 时间的事情,这一个重要的原因在于,设计的越简单这将降低引发信号竞争条件的风险

向进程发送信号

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 指定的**进程是否存在(**如果向一个不存在的进程发送信号,kill()将会返回-1,errno 将被设置为 ESRCH,表示进程不存在)
    • **返回值:**成功返回 0;失败将返回-1,并设置 errno

进程中将信号发送给另一个进程是需要权限的,并不是可以随便给任何一个进程发送信号,超级用户 root进程可以将信号发送给任何进程,但对于非超级用户(普通用户)进程来说,其基本规则是发送者进程的实际用户 ID 或有效用户 ID 必须等于接收者进程的实际用户 ID 或有效用户 ID

raise 函数

  • raise 用于进程向自身发送信号
#include <signal.h>
 int raise(int sig);   // 需要发送的信号,成功返回0,失败返回非零值
  • raise 实际上等价于:kill(getpid(), sig)

信号相关函数

alarm

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

unsigned int alarm(unsigned int seconds);
  • 参数含义:

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

pause

  • pause()系统调用可以使得进程暂停运行、进入休眠状态直到进程捕获到一个信号为止只有执行了信号处理函数并从其返回时,pause()才返回,在这种情况下,pause()返回-1,并且将 errno 设置为 EINTR
#include <unistd.h>
int pause(void);
  • alarm 和 pause 函数使用(两者组合相当于 sleep 的效果)
#include <signal.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>

static void sig_handler(int sig){
    printf("Alarm error\n");
    // exit(0);   // 这里如果加上退出函数,会使得进程在接收到指定信号后直接退出
}

int main(int argc, char* argv[]){
    int ret;
    int second;
    struct sigaction sig = {0};
    sig.sa_handler = sig_handler;
    sig.sa_flags = 0;
    // 这里只是绑定了信号处理函数,继续往下执行代码,直到产生信号才进入信号处理函数中
    ret = sigaction(SIGALRM, &sig, NULL);  
    if(ret == -1){
        perror("sigaction error");
        return -1;
    }
    second = atoi(argv[1]);
    printf("time:%d\n", second);
    // alarm和pause共同组成了sleep的效果
    alarm(second);
    pause();  // 只有当进程接收到信号(SIGALRM)且执行完信号处理函数后,pause才结束  
    puts("进入休眠");   
    return 0;
}

信号集

  • 信号集 sigset_t:表示一组信号的数据类型
#define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
  unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;
typedef __sigset_t sigset_t;

初始化信号集

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


// 使用示例
sigset_t sig_set;
sigemptyset(&sig_set);
sigfillset(&sig_set);

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

#include <signal.h>
int sigaddset(sigset_t *set, int signum);  // 向信号集中添加一个信号,sigaddset(&sig_set, SIGINT)
int sigdelset(sigset_t *set, int signum);  // 向信号集中删除一个信号,sigdelset(&sig_set, SIGINT)
// 成功返回 0;失败将返回-1,并设置 errno

测试信号是否在信号集中

  • 使用 sigismember 可以测试某个信号是否在指定的信号集中
#include <signal.h>
int sigismember(const sigset_t *set, int signum);  // 如果信号 signum 在信号集 set 中,则返回 1;如果不在信号集 set 中,则返回 0;失败则返回-1,并设置 errno

信号掩码

  • 内核为每一个进程维护了一个信号掩码(其实就是一个信号集),即一组信号。当进程接收到一个属于信号掩码中定义的信号时,该信号将会被阻塞无法传递给进程进行处理,那么内核会将其阻塞,直到该信号从信号掩码中移除,内核才会把该信号传递给进程从而得到处理

向信号掩码中添加信号的方式

  1. 当应用程序调用 signal()或 sigaction()函数为某一个信号设置处理方式时,进程会自动将该信号添加到信号掩码中(处理完信号后才会从信号掩码中释放),这样保证了在处理一个给定的信号时,如果此信号再次发生,那么它将会被阻塞;当然对于 sigaction()而言,是否会如此,需要根据 sigaction()函数是否设置了 SA_NODEFER 标志而定;当信号处理函数结束返回后,会自动将该信号从信号掩码中移除。
  2. 使用 sigaction()函数为信号设置处理方式时,可以额外指定一组信号,当调用信号处理函数时将该组信号自动添加到信号掩码中,当信号处理函数结束返回后,再将这组信号从信号掩码中移除,通过 sa_mask 参数进行设置
  3. 还可以使用 **sigprocmask()**系统调用,随时可以显式地向信号掩码中添加/移除信号
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  • 参数含义:

    • how:参数 how 指定了调用函数时的一些行为
      • SIG_BLOCK:将参数 set 所指向的信号集内的所有信号添加到进程的信号掩码中。换言之,将信号掩码设置为当前值与 set 的并集
      • SIG_UNBLOCK:将参数 set 指向的信号集内的所有信号从进程信号掩码中移除
      • SIG_SETMASK:进程信号掩码直接设置为参数 set 指向的信号集
    • set:将参数 set 指向的信号集内的所有信号添加到信号掩码中或者从信号掩码中移除;如果参数 set 为 NULL,则表示无需对当前信号掩码作出改动
    • oldset:如果参数 oldset 不为 NULL,在向信号掩码中添加新的信号之前,获取到进程当前的信号掩码,存放在 oldset 所指定的信号集中;如果为 NULL 则表示不获取当前的信号掩码
    • 返回值:成功返回 0;失败将返回-1,并设置 errno
  • 测试代码:

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

static void sig_handler(int sig){
    printf("Receive signal: %d\n", sig);
}

int main(int argc, char* argv[]){
    int ret;
    sigset_t sig_set;
    struct sigaction sig = {0};
    sig.sa_handler = sig_handler;
    sig.sa_flags = 0;

    ret = sigaction(SIGINT, &sig, NULL);       
    if(ret == -1){
        perror("sigaction error");
        return -1;
    }
    sigemptyset(&sig_set);                         // 信号集初始化
    sigaddset(&sig_set, SIGINT);                   // 将SIGINT信号添加到信号集中
    ret = sigprocmask(SIG_BLOCK, &sig_set, NULL);  // 在信号掩码中添加SIGINT信号
    if(ret == -1){
        perror("sigprocmask error");
        return -1;
    }
    sleep(5);
    ret = sigprocmask(SIG_UNBLOCK, &sig_set, NULL);   // 将SIGINT从信号掩码中移除,之前阻塞的信号会被执行
    if(ret == -1){
        perror("sigprocmask error");
        return -1;
    } 
    return 0;
}

实时信号(可靠信号)

sigpending 函数

  • 如果进程当前正在执行信号处理函数,在处理信号期间接收到了新的信号,如果该信号是信号掩码中的成员,那么内核会将其阻塞,将该信号添加到进程的等待信号集(等待被处理,处于等待状态的信号)中,为了确定进程中处于等待状态的是哪些信号,可以使用 sigpending()函数获取
#include <signal.h>
int sigpending(sigset_t *set);   // 处于等待状态的信号会存放在参数 set 所指向的信号集中
// 成功返回 0;失败将返回-1,并设置 errno
  • 可以配合使用 sigismember 函数,判断某信号是否处于 set 中

实时信号的编号

  • 实时信号(34-64)和非实时信号(1-31)都有 31 个,实时信号由 SIGRTMIN+N 或 SIGRTMAX-N 的方式来表示,SIGRTMIN 表示编号最小的实时信号(34),SIGRTMAX 表示编号最大的实时信号(64)

实时信号比标准信号的优势

  • 实时信号的信号范围有所扩大,可应用于应用程序自定义的目的,而标准信号仅提供了两个信号可用于应用程序自定义使用:SIGUSR1 和 SIGUSR2
  • 内核对于实时信号所采取的是队列化管理。如果将某一实时信号多次发送给另一个进程,那么将会多次传递此信号。相反,对于某一标准信号正在等待某一进程,而此时即使再次向该进程发送此信号,信号也只会传递一次
  • 当发送一个实时信号时,可为信号指定伴随数据(一整形数据或者指针值),供接收信号的进程在它的信号处理函数中获取
  • 不同实时信号的传递顺序得到保障。如果有多个不同的实时信号处于等待状态,那么将率先传递具有最小编号的信号。换言之,信号的编号越小,其优先级越高,如果是同一类型的多个信号在排队,那么信号(以及伴随数据)的传递顺序与信号发送来时的顺序保持一致

实时信号的使用

  • 应用程序当中使用实时信号,需要有以下的两点要求:
    • 发送进程使用** sigqueue()**系统调用向另一个进程发送实时信号以及伴随数据
    • 接收实时信号的进程要为该信号建立一个信号处理函数,使用 sigaction 函数为信号建立处理函数,并加入 SA_SIGINFO,这样信号处理函数才能够接收到实时信号以及伴随数据,也就是要使用 sa_sigaction 指针指向的处理函数,而不是 sa_handler,当然允许应用程序使用 sa_handler,但这样就不能获取到实时信号的伴随数据了

sigqueue 函数

  • 可以使用 sigqueue 发送一个实时信号(也可发送标准信号)
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
  • 参数含义:
    • pid:指定接收信号的进程对应的 pid,将信号发送给该进程
    • sig:指定需要发送的信号。与 kill()函数一样,也可将参数 sig 设置为 0,用于检查参数 pid 所指定的进程是否存在
    • value:参数 value 指定了信号的伴随数据,union sigval 数据类型
    • 返回值:成功将返回 0;失败将返回-1,并设置 errno
typedef union sigval {
    int sival_int;
    void *sival_ptr;
} sigval_t;   // 既可以指定一个整型数据,又可以指定一个指针
  • 测试
// 发送方
#include <signal.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
static void sig_handler(int sig){
    printf("Receive signal: %d\n", sig);
}

int main(int argc, char* argv[]){
    union sigval sig_val;
    int ret;
    int pid = atoi(argv[1]);
    int sig = atoi(argv[2]);
    printf("pid: %d\nsignal: %d\n", pid, sig);
    sig_val.sival_int = 10;
    ret = sigqueue(pid, sig, sig_val);
    if(ret == -1){
        perror("sigqueue error");
        return -1;
    }
    return 0;
}

// 接收方
#include <signal.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
static void sig_handler(int sig, siginfo_t *info, void *context){
    printf("接收到实时信号: %d\n", sig);
    printf("伴随数据为: %d\n", info->si_value.sival_int);
    printf("PID:%d\n", info->si_pid);
    printf("UID:%d\n", info->si_uid);
}

int main(int argc, char* argv[]){
    int ret;
    struct sigaction sig = {0};
    sig.sa_sigaction = sig_handler;
    sig.sa_flags = SA_SIGINFO;

    ret = sigaction(atoi(argv[1]), &sig, NULL);
    if(ret == -1){
        perror("sigaction error");
        return -1;
    }
    while(1){
        sleep(1);
    }
    return 0;
}

如果使用 kill 函数发送标准信号,那么接收方收到的伴随数据等于 0,因为 kill 函数无法发送伴随数据

abort 异常终止进程

  • abort 函数要点:

    • 解除对 SIGABRT 信号的阻塞(从信号掩码中移除)
    • 向本进程发送 SIGABRT 信号(使用 raise 函数),导致进程异常终止
    • 如果进程忽略/捕获 SIGABRT 信号:abort 会将 SIGABRT 信号的处理方式重置为系统默认操作,并再次向本进程发送 SIGABRT 信号
  • 调用 abort 函数一定会终止进程

#include <stdlib.h>
void abort(void);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值