• 小试牛刀Linux信号


    前言

    生活中处处是信号。
    当早晨的闹钟响起
    当赛道上的信号枪响起
    当路口的红绿灯绿灯亮起
    ……

    当信号产生时,我们就知道接下来该做什么了。
    系统中的信号也如此

    信号的概念

    信号:信号是发给进程的,是进程之间事件异步通知的一种方式,属于软中断。

    信号产生时为什么我们会知道接下来该做什么,本质是因为被提前灌输了“遇到这个信号时,该做什么”。同样,在早期技术人员写相关源码时就已经提前写入可能产生的信号以及处理方法,这代表着进程识别和处理信号的能力远远早于信号的产生

    进程发送信号的本质
    OS将信号数据写入进程的PCB中。

    当进程收到信号时,进程并不是立即处理的,有可能当前进程做着更加重要的事情,所以收到信号后,会将信号保存起来,在“合适的时机”处理。

    根据上述可得出一条逻辑线路
    在这里插入图片描述

    查看信号列表

    Linux查看信号列表的命令kill -l

    [YDY@VM-0-2-centos ~]$ 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	
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    1~31号信号为普通信号,34 ~ 64为实时信号。以下的学习均只涉及普通信号。

    初识信号捕捉

    进程收到信号后,有三种处理方案:

    • 默认动作。执行原本的处理方案,例如终止,暂停等。
    • 忽略动作。也是信号的一种处理方式,只是什么都不做。
    • 自定义动作(信号捕捉)。我们可以定义信号的处理做法,信号捕捉后,将不会执行默认动作,而是执行自定义动作。

    信号捕捉所用到的system call

    #include 
    
    typedef void (*sighandler_t)(int);
    
    sighandler_t signal(int signum, sighandler_t handler);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    例如,捕捉2号信号。

     1 #include <stdio.h>
     2 #include <unistd.h>
     3 #include <signal.h>
     4 
     5 void handler(int sig);
     6 
     7 int main(void)
     8 {
     9   //捕捉2号信号                                                              
    10   signal(2, handler);
    11   sleep(20);
    12   return 0;
    13 }
    14 
    15 void handler(int sig)
    16 {
    17   printf("get a sig :%d\n", sig);
    18 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在这里插入图片描述


    注意:9号信号(SIGKILL)不可被捕捉.

    信号产生的方式

    1.由终端按键产生

     1 #include <stdio.h>
      2 #include <unistd.h>
      3 #include <signal.h>
      4 
      5 //测试各种终端按键发送的普通信号
      6 
      7 void handler(int sig);
      8 
      9 int main(void)
     10 {
     11   //信号捕捉
     12   int sign = 1;
     13   for(sign = 1; sign < 32; sign++)
     14   {
     15     signal(sign, handler);
     16   }
     17   while(1);
     18    //var sleep(50);
     19   return 0;
     20 }
     21 void handler(int sig)
     22 {
     23   printf("get a sinal : %d\n", sig);                                         
     24 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    经过测试,在Linux上可发送信号的几个按键

    按键信号
    ctrl + z20
    ctrl + c2
    ctrl + 43
    ctrl + \3
    …………

    2.进程异常产生

    当进程异常的时候,会产生信号,导致进程退出。

        1 #include <stdio.h>
        2 #include <unistd.h>
        3 #include <signal.h>
        4 #include <sys/types.h>
        5 #include <sys/wait.h>
        6 //测试进程异常产生的信号
        7 int main(void)
        8 {
        9   if(fork() == 0)
       10   {
       11     int a = 10;
       12     printf("%d\n", a / 0);
       13   }
       14 
       15   int status = 0;
       16   pid_t ret = waitpid(-1, &status, 0);
       17   if(ret > 0)
       18   {
       19     //wait success
       20     printf("child get a sinal : %d\n", status & 0x7F);                     
       21   }
       22   return 0;
       23 }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    在这里插入图片描述

    子进程,收到了8号信号(SIGFPE),导致进程崩溃并终止。

    当进程异常退出后,我们最希望的就是知道哪里出错,所以需要去检查刚才的数据等信息。但是进程都终止了,数据不在内存,还如何事后检查呢?

    当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许
    产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。

    例如,在Linux上,默认不产生core文件。如果需要使用,可以用ulimit -c filesize命令更改core文件的大小,这样就可以生成core文件,filesize的大小最大不超过1024K。

    父进程使用waitpid等待子进程时,其第二个参数(输出型参数status)反映了子进程的退出信息。这个参数只使用低16位,次低7位是一个core dump标志位
    在这里插入图片描述
    如果产生了core文件,那么core dump标志位上为1;没有产生则标志位上为0。

      //测试进程异常产生的信号
     int main(void)
     {
       if(fork() == 0)
       {
         int a = 10;
         printf("%d\n", a / 0);
       } 
       int status = 0;
       pid_t ret = waitpid(-1, &status, 0);
       if(ret > 0)
       {
         //wait success
         printf("child get a sinal : %d, core dump: %d\n", status & 0x7F, (statu      s >> 7) & 0x7F);                                                           
     
       }
       return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在这里插入图片描述
    说明刚才的进程没有创建core文件。

    进程异常收到信号导致终止的本质:
    CPU计算过程中,导致硬件出现了问题。然后被其他的硬件检测到并通知给内核,内核随后向该进程发送合适的信号。

    3.系统调用函数产生

    作用:发送一个信号给进程
    #include 
    #include 
    
    int kill(pid_t pid, int sig);
    //pid:发送给进程的PID
    //sig:要发送的信号
    //成功返回0,失败返回-1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    测试系统调用kill
      8 int main(void)
      9 {
     10   pid_t id = fork();
     11   if(id == 0)
     12   {
     13     //child
     14     while(1);
     15   }
     16   else
     17   {
     18     kill(id, 2);
     19     int status = 0;
     20     pid_t ret = waitpid(-1, &status, 0);
     21     if(ret > 0)
     22     {
     23        //wait success
     24        printf("child get a sinal : %d\n", status & 0x7F);
     25     }                                                                                                                                                              
     26   }
     27   return 0;
     28 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    运行结果:
    在这里插入图片描述

    在Linux上有一个kill命令也可以发送信号,它本质上也是调用了系统调用kill.
    kill命令的使用方法
    kill -signal pid
    signal:几号信号
    pid:发送给进程,这个进程pid

    例如:
    在这里插入图片描述

    .

    4.软件条件产生

    通过某种软件,来触发信号的产生和发送。
    SIGPIPE、SIGALRM信号是由软件条件产生的信号。

    这里主要学习SIGALRM信号。

    涉及函数

    作用:通知内核在seconds秒后发送SIGALRM信号给当前进程
    #include 
    
    unsigned int alarm(unsigned int seconds);
    //返回值为0,或者为之前设定的闹钟时间还剩的秒数
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如果设定的秒数为0,表示取消以前设定的闹钟,返回值是以前设定的闹钟还剩的秒数。

      1 #include <stdio.h>
      2 #include <unistd.h>                                                          
      3 #include <signal.h>
      4 #include <sys/types.h>
      5 #include <sys/wait.h>
      6 
      7 void handler(int sig);
      8 
      9 int main(void)
     10 {
     11   signal(14, handler); //捕捉SIGALRM信号
     12   alarm(10);
     13   int cnt = 0;
     14   while(1)
     15   {
     16     cnt++;
     17     printf("cnt : %d\n", cnt);
     18     sleep(1);
     19   }
     20   return 0;
     21 }
     22 void handler(int sig)
     23 {
     24   printf("get a signal : %d\n", sig);
     25 }  
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    运行结果:10秒后收到了14号(SIGALRM)信号。
    在这里插入图片描述

    其他常见信号概念

    • 信号递达(delivery):实际执行信号的处理动作。(默认动作、忽略动作、自定义动作)。
    • 信号未决(pending):信号从产生到递达之间的状态。
    • 进程可以选择阻塞(block)某个信号。这个信号依然是未决的,直到解除了阻塞,才可以递达。

    信号在内核中的记录

    在收到信号后,进程并不是立即处理,它会在“合适的时机”进行处理,所以进程需要保存收到的信号。

    以Linux为例。

    Linux的进程PCB–>task_struct中存在着三张关于信号的表。

    在这里插入图片描述
    block表和pending表是位图结构。

    block表:比特位的位置代表信号编号,比特位内容标识是否被阻塞。阻塞位图又称为信号屏蔽字
    pending表:比特位的位置代表信号编号,比特位内容标识是否收到该信号,是否处于未决状态。
    handler表:是一个函数指针数组,第一个条目对应编号为1的信号,第二个条目对应编号为2的信号……,每个条目指向对应信号的处理方法。

    以上图为例,该进程收到了2号信号,但是2号信号被阻塞。

    信号集操作

    阻塞标志(block)和未决标志(pending)可以使用同一个类型存储,这个类型为sigset_t,称为信号集。可以表示信号的“有效”和“无效”状态。这个类型中如何存储bit依赖于系统实现,用户只需调用库函数来操作sigset_t变量。

    #include 
    
    int sigemptyset(sigset_t *set);
    int sigfillset(sigset_t *set);
    int sigaddset(sigset_t *set, int signum);
    int sigdelset(sigset_t *set, int signum);
    int sigismember(const sigset_t *set, int signum);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • sigemptyset函数将信号集初始化为空,也就是将所有的bit变为一个标志位,表示当前没有收到信号状态。
    • sigfillset函数将信号集初始化为收到所有信号的状态。
    • sigaddsetsigdelset函数,添加/删除有效信号进set的信号集。
    • 以上四个函数的返回值,都是成功返回0,失败返回-1
    • sigismember函数,判断一个信号集的有效信号中是否包含该信号。如果包含,返回1,不包含返回0,出错返回-1
    • 使用sigset_t类型的变量前,一定要调用sigemptyset/sigfillset函数初始化,让信号集处于确定状态。

    以上函数在“信号屏蔽字操作”做测试


    信号屏蔽字操作

    #include 
    
    int sigprocmask(int how, const sigset_t *set, sigset_t *oldset)
    
    • 1
    • 2
    • 3
    • 作用:检查和修改阻塞信号集(信号屏蔽字)。
    • 返回值:成功返回0,失败返回-1

    假设当前的信号屏蔽字为mask ,参数how的可以选项

    含义
    SIG_BLOCK现在的阻塞信号集,是当前阻塞信号集mask和信号集set的并集。信号集set包含了我们想要添加进当前信号集的信号
    SIG_UNBLOCK从当前阻塞信号集mask中删除信号集set中的信号,推导的公式:mask = mask &(~set)
    SIG_SETMASK使现在的阻塞信号集由mask变为set的阻塞信号集
    • oset是一个输出型参数,若oset非空,则当前信号屏蔽字由oset截取
    • set是一个输入型参数,若set非空,其内容参见参数how的可选项

    注意:如果使用system call接口sigprocmask解除了当前多个未决信号的阻塞,那么在sigprocmask返回之前,至少有一个信号已经被递达。

    测试代码(阻塞2号信号):

      1 #include <stdio.h>
      2 #include <signal.h>
      3 #include <unistd.h>
      4 #include <sys/types.h>
      5 
      6 int main(void)
      7 {
      8   //解除对2号信号的阻塞
      9   sigset_t set, oset;
     10 
     11   sigemptyset(&set);
     12   sigemptyset(&oset);
     13 
     14   //添加2号信号进信号集set,用于修改当前进程的信号屏蔽字
     15   sigaddset(&set, 2);
     16 
     17   //从当前信号屏蔽字中添加信号集set中包含的信号
     18   sigprocmask(SIG_BLOCK, &set, &oset);                                       
     19 
     20   while(1)
     21   {
     22     printf("i am process : %d\n", getpid());
     23     sleep(1);
     24   }
     25 
     26   return 0;
     27 }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    运行现象:给这个进程发送2号信号,并没有看到进程对这个信号做了处理,这个信号一直处于未决状态,最后进程被我使用终端按键ctrl + \ 给进程发送3号信号终止。
    在这里插入图片描述

    pending位图操作

    #include 
    
    int sigpending(sigset_t *set);
    
    • 1
    • 2
    • 3

    作用:读取当前进程的未决信号集,通过输出型参数set截取
    返回值,成功返回0,失败返回-1

    测试代码

      1 #include <stdio.h>                                                                                                                                    
      2 #include <signal.h>
      3 #include <unistd.h>
      4 #include <sys/types.h>
      5 
      6 void show_pending(sigset_t *p);
      7 int main(void)
      8 {
      9   //解除对2号信号的阻塞
     10   sigset_t set, oset;
     11 
     12   sigemptyset(&set);
     13   sigemptyset(&oset);
     14 
     15   //添加2号信号进信号阻塞集set,用于修改当前进程的信号屏蔽字
     16   sigaddset(&set, 2); 
     17 
     18   //从当前信号屏蔽字中删除阻塞信号集set中包含的信号
     19   sigprocmask(SIG_BLOCK, &set, &oset);
     20 
     21   sigset_t pending;
     22   while(1)
     23   {
     24 
     25     sigemptyset(&pending);
     26     
     27     //将当前进程的未决信号集通过pending传出(存储在pending变量中国)
     28     sigpending(&pending);
     29 
     30     //打印当前进程的未决信号集
     31     show_pending(&pending);
     32                       
     33     printf("i am process : %d\n", getpid());
     34     sleep(1);                  
     35   }           
     36  
     37   return 0;            
     38 }                    
     39 void show_pending(sigset_t *p)
     40 {                   
     41   printf("the pending of current process is ");
     42   int i = 1;
     43   for(i = 1; i < 32; i++)                                   
     44   {                   
     45     //如果信号存在那么bit为1
     46     if(sigismember(p, i))                          
     47     {                                 
     48       printf("1");
     49     }              
     50     else  
     51     {
     52       printf("0");
     53     }                     
     54     
     55   }                                        
     56   printf("\n");          
     57 }                                                         
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    运行现象:进程在未收到信号前,其pending位图的所有比特位均为0,而后向进程发送2号信号后,pending位图的第2位比特位变成1,其余均不变,2号信号处于未决状态。
    在这里插入图片描述

    handler表操作

    #include 
    
    int sigaction(int signo, const struct signaction *act, struct sigaction *oldact);
    
    • 1
    • 2
    • 3

    作用:是一个系统调用函数,用于修改信号的处理动作。
    返回值:调用成功返回0,失败返回-1。

    • 参数signo是信号编号
    • 若参数act非空,则根据act修改信号signo的处理动作
    • 若参数oldact非空,则由oldact截取当前的信号处理动作

    act和oldact都指向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);
        };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    结构体内的sa_handler是一个函数指针,指向的是信号的处理方法,有三种情况:

    • 被赋值SIG_DEF,表示信号的处理动作为默认动作
    • 被赋值SIG_IGN,表示信号的处理动作为忽略动作
    • 被赋值函数指针,表示信号的处理动作为自定义动作(执行函数指针指向的方法)。

    结构体内还有一个信号集mask,希望哪些信号在执行信号的处理方法时被阻塞,就把这些信号放入mask中。

    测试代码:

      1 #include <stdio.h>                                                     
      2 #include <signal.h>
      3 #include <unistd.h>
      4 #include <string.h>
      5 
      6 void handler(int signo)
      7 {
      8   while(1)
      9   {
     10     printf("executing  %d signal\n", signo);
     11     sleep(1);
     12   }
     13 }
     14 
     15 int main(void)
     16 {
     17   struct sigaction act;
     18   memset(&act, 0, sizeof(act));
     19   act.sa_handler = handler; //注册了一个处理方法
     20 
     21   //将希望处理信号时屏蔽的信号添加进sa_mask中
     22   sigemptyset(&act.sa_mask);
     23   sigaddset(&act.sa_mask, 3);
     24 
     25   //更改2号信号的处理方法
     26   sigaction(2, &act, NULL);
     27 
     28   printf("i am process :%d\n", getpid());
     29   sleep(20);
     30 
     31   return 0;                              
     32 }              
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    运行现象:在打印了i am process : 16931过后,在20秒之内给该进程发送2号信号,进程开始死循环打印"executing 2 signal", 发送3号信号没有反应。
    在这里插入图片描述

    信号捕捉在内核中

    信号的处理时机:当进程从内核态返回用户态时。
    在这里插入图片描述
    上图可抽象为一个“无穷大”。
    在这里插入图片描述

  • 相关阅读:
    Ehcache(二次封装,每个元素可自定义过期时间)
    解析/区分MOS管的三个引脚G、S、D(NMOS管和PMOS管)
    HadSky+内网穿透打造个人专属社区论坛并远程访问
    python数据分析(numpy)
    【毕业设计】基于javaEE+SSH+mysql的码头船只出行及配套货柜码放管理系统设计与实现(毕业论文+程序源码)——码头船只出行及配套货柜码放管理系统
    1.图形学-矩形中的线段的裁剪Cohen-Sutherland算法(附带源码)
    java毕业设计房屋租赁网站Mybatis+系统+数据库+调试部署
    python练习(4)
    深度学习之 8 深度模型优化与正则化
    ffmpeg 解包流程
  • 原文地址:https://blog.csdn.net/qq_56870066/article/details/126031379