• 【Linux】信号 —— 信号的产生 | 信号的保存 | 信号的处理 | volalite关键字 | SIGCHLD



    people change

    前排声明:以后更新文章会慢一些吧,如上文所言,people change… (哈哈哈距离我刚开始写下这句话时候已过去一个月了,果然,人生总是在不经意的想法中出现转折,曾经认为理所当然的事儿… 所以顺其自然…

    “而且我们都是学计算机的,我们知道如果把宇宙所有的原子结构保存,配上超强的算法,一切都是必然的,我们决定不了的”

    “你一上升到这个高度我就讨论不了了”

    “我只是想说顺其自然”

    a zing never lies @小边小边别发愁

    生活中有许多关于信号的场景:比如红绿灯,你daddy的脸色。。。

    这些场景触发时,我立马就知道我接下来该做什么,这来自于天生or后天习得,并不是它们真的发生时我知道该怎么做。同理,进程具有识别并处理信号的能力是远远早于信号产生的,这是由那些编写操作系统的工程师写好的。

    在生活中,我们收到信号时,不一定会立即处理,因为当前我可能做着更重要的事儿。同理,信号随时都可能产生(异步),进程收到某种信号时,不一定是立即处理的,而是在合适的时候处理

    比如有一小伙儿跟你表白了,你虽然没有立即回复(此时你正向闺蜜群组求助哈哈哈),但你也记着要给个答复。既然有时信号不能被立即处理,进程就需要先将信号保存起来,以供合适时机处理。那应该保存在哪里呢?struct task_struct,信号的发送即向进程控制块写入信号数据。task_struct是一个内核数据结构,内核不相信任何人,只相信自己,无论以何种方式发送信号,本质都是通过OS发送的,那一定是操作系统向task_struct写入信号数据。

    kill -l 查看系统支持的信号列表
    
    • 1

    前31个(1 ~ 31)是普通信号,后31个(34 ~ 64)是实时信号 ——

    image-20220830193845696

    一般进程收到信号的处理方案有三种情况 ——

    1. 默认动作:终止自己、暂停等

    2. 忽略动作:是一种信号处理的方式,只不过就是什么也不干

    3. 自定义动作(信号的捕捉):比如如下的signal方法,修改处理信号的动作,由默认动作变为自定义动作

    1. 信号的产生 - 信号发送前

    信号产生的方式有哪些呢?

    1.1 键盘产生

    写一段死循环函数,可以ctrl+c终止程序。我们键盘ctrl+C时,本质是向指定进程发送2号信号SIGINT。那你怎么证明?可以通过signal函数为信号指定处理函数:

    signal - ANSI C signal handling
    
    #include 
    
    typedef void (*sighandler_t)(int); //函数指针 - 返回值为void,参数为int
    
    sighandler_t signal(int signum, sighandler_t handler);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    修改进程对信号的默认处理动作 ——

    • signal:实际上这些大写的信号都是#define 整数定义出来的
    • handler:注册函数。注册时不是调用这个函数,只有信号到来时,函数才会被调用。

    代码如下 ——

    #include    
    #include    
    #include    
    #include
    
    void handler(int signo)    
    {    
      printf("get a signal: %d, pid: %d\n", signo, getpid());    
      exit(1);
    }    
        
    int main()    
    { 
      int sig;
      for(sig = 1; sig<=31; sig++)
      {
        //通过signal注册对1~31号信号的处理动作,改成我们自定义的动作    
        signal(sig, handler);                                                           
      }
      
      while(1)    
      {    
        printf("people change...pid: %d\n", getpid());    
        sleep(1);    
      }    
      return 0;    
    } 
    
    • 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

    发送信号时,执行自定义方法 ——

    image-20220830191947376

    信号的产生方式的其中一种是通过键盘产生,键盘产生的信号只能终止前台进程,杀掉后台进程&得用kill命令 ——

    kill -9 [pid]
    
    • 1

    我们还可以设置不同信号的处理方式 ——

    #include    
    #include    
    #include    
    #include    
        
    void handler(int signo)    
    {    
      switch(signo)    
      {    
        case 2:                                                                     
          printf("Ayayaya~\n");    
          break;    
        case 3:    
          printf("people change a lot\n");    
          break;    
        case 9:    
          printf("I am tired\n");    
          break;    
        default:
          printf("signo: %d\n", signo);
          break;    
      }    
      //exit(1);
    }    
        
    int main()    
    {    
      int sig;    
      for(sig = 1; sig<=31; sig++)    
      { 
        //通过signal注册对1~31号信号的处理动作,改成我们自定义的动作    
        signal(sig, handler);    
      }    
      while(1)    
      {    
        printf("people change...pid: %d\n", getpid());
        sleep(1);
      }
      return 0;
    } 
    
    • 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

    发现9号信号不可被捕捉(自定义) ——

    072801

    为什么呢?we will talk about it later…

    1.2 异常

    信号产生,也可能由于程序中存在异常问题,使进程收到信号并退出。

    写一段野指针解引用代码 ——

      int* p = NULL;    
      *p = 100;
    
    • 1
    • 2

    进程为什么会崩溃呢?本质是因为进程收到了11号信号SIGSEGV。而且如果不exit会刷屏,说明不断收到11号信号。

    image-20220830192208739

    除0 ——

      int a = 10;
      a /= 0;
    
    • 1
    • 2

    除0收到的是8号信号SIGFPE,浮点数异常 ——

    image-20220830192225461

    综上,在win或Linux环境下,进程崩溃的本质是进程收到了对应的信号,并执行默认处理动作(杀死进程)。那为什么会收到信号呢?

    软件上的错误通常会体现在硬件或其它软件上,而操作系统是硬件管理者,就应该对硬件的健康负责,找到有问题的进程并发送信号反馈,终止进程。我们在C++中的捕捉异常,实际上就是在处理信号。

    进程崩溃时,你最关心的是崩溃的原因,这是通过waitpid()的status参数的低7位来获取退出信号。(忘了的宝子去复习进程控制吧~)

    image-20220830192240158

    从前从前我们就知道,在Linux中,进程正常退出时,它的退出码和退出信号都会被设置;进程异常退出时,它的退出信号会被设置,表明进程退出的原因。另外我还想知道是在哪一行崩溃的喂?

    从前从前我们挖了这样一个坑:如有必要,OS会设置退出信息中的core dump标志位,并将进程在内存中的数据转储到磁盘中,方便我们后期调试。

    在云服务器上,core dump被关掉了,我们把它打开~

    ulimit -a	查看系统资源
    ulimit -c 10240
    
    • 1
    • 2
    image-20220830192326805

    这样就允许你core dump了,(但也不是所有的信号都会core dumped),此时再运行,还生成了core文件 ——

    image-20220830192533926image-20220830192548240

    我们打开生成的core.10225,发现是人类看不懂的乱码,因为是把磁盘内容直接拷下来的,我们也不关心啦~

    我们需要小小修改一下Makefile文件,编译时带上-g选项,允许用gdb调试。用core-file命令,得知错误原因及错误行数 ——

    image-20220830192533926image-20220830192548240

    这种方案叫做事后调试。曾经我们程序崩溃,我们只能注释掉定位或者一行一行的单步调试。

    若进程出现异常时被core dump,core dump会被设置为1,我们来通过位操作获取退出信息 ——

    #include    
    #include                                                            
    #include    
    #include    
    
    int main()    
    {    
        if(fork() == 0)    
        {    
          while(1)    
          {    
            printf("I am child...\n");    
            int a = 10;    
            a /= 0;    
          }    
        }    
    
        int status = 0;    
        waitpid(-1, &status, 0);    
        printf("exit code: %d, exit sig:%d, core dump flag: %d\n", (status>>8)&0xFF, status&0x7F, (status>>7)&1); return 0;    
    }   
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    image-20220830192638108

    1.3 系统调用

    通过系统调用产生信号 ——

    #include 
    #include 
    
    int kill(pid_t pid, int sig); //给任意进程发送任意信号
    int raise(int sig); //给自己发任意信号
    
    • 1
    • 2
    • 3
    • 4
    • 5
    #include 
    
    void abort(void); //给自己发送SIGABRT信号
    
    • 1
    • 2
    • 3

    kill命令就是调用kill函数实现的 ——

    #include    
    #include    
    #include    
    #include    
        
    static void Usage(const char* proc)    
    {    
      printf("Usage:\n\t%s signo who", proc);    
    }    
     
    // ./mytest signo who
    int main(int argc,char* argv[])    
    {    
      if(argc != 3)    
      {    
        Usage(argv[0]);    
        return 1;    
      }    
          
      int signo = atoi(argv[1]);    
      int who = atoi(argv[2]);    
          
      kill(who, signo);    
          
      return 0;                                                                                     
    }  
    
    • 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

    image-20220830192655262

    1.4 软件条件

    通过某种软件(OS),来触发信号的发送,在系统层面设置定时器,或某种操作导致条件不就绪等这样的场景下,触发的信号。

    在进程间通信匿名管道的4种场景中,写端疯狂写,若此时读端关闭了读fd,则写端会**收到13号信号SIGPIPE**退出,这就是一种软件条件触发的信号发送。今天我们再介绍 ——

    #include 
    
    unsigned int alarm(unsigned int seconds);
    
    • 1
    • 2
    • 3

    设置一个计时器/闹钟,延迟n秒后发送14号信号SIGALRM ——

    • 返回值:可以认为alarm被正常触发时,返回值就是0;或者是距离设定时间余下的秒数。
    #include    
    #include    
    #include    
    #include    
    #include    
        
    void handler(int signo)    
    {    
      printf("well, I recieved %d signal\n", signo);    
    }    
        
    int main()    
    {    
      int sig;    
      for(sig = 1; sig <= 31;sig++)    
      {    
        signal(sig, handler);    
      }    
        
      int ret = alarm(20);    
        
      sleep(5);    
      ret = alarm(0); //取消闹钟                                                                                                                                        
      printf("autually, it still left %d seconds\n", ret);    
        
      return 0;    
    }
    
    • 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

    image-20220830192713126image-20220830192731743

    我们不断打印count,统计1s中server对count可以递增到三万多次;然后屏蔽掉printf,而只是在捕捉信号处打印count,发现count达到了5亿次 ——

    #include    
    #include    
    #include    
    #include    
    #include    
    
    int count = 0;    
    
    //没有设置alarm信号(14)的捕捉/自定义动作, 执行默认动作:终止进程
    //void handler(int signo)    
    //{    
    //	printf("after 1s... count: %d\n", count);                                                               //  exit(1);    
    //}    
    
    int main()    
    {    
        signal(SIGALRM, handler);    
        alarm(1); /    
        while(1)    
        {    
          //printf("people change...%d\n", count);    
          count++;    
        }    
        return 0;    
    }
    
    • 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
    image-20220830192731743

    因为IO是非常慢的。。

    1.5 理解发送信号

    信号产生方式的种类非常多,但就算信号产生的方式千差万别,最终一定是通过OS向目标进程发送信号。

    如何理解OS给进程发送信号?朴素的理解就是OS发送信号数据给task_struct,我们要进一步来谈 ——

    我们发现信号的编号是有规律的**[1, 31],进程内部一定要有对应的数据变量,来保存记录是否收到了对应的信号。那用什么数据类型,来标识进程是否收到信号**呢?唔唔唔!位图

    struct task_struct{
        //进程的各种属性
        uint32_t sigs; 
    };
    
    • 1
    • 2
    • 3
    • 4

    比特位的位置(第几个),代表的是哪一个信号;比特位的内容(0/1),代表是否收到信号 ——

    //比如这个,表示收到3号信号
    00000000 00000000 00000000 00000100 
    
    • 1
    • 2

    综上,“信号的发送”更准确的说是信号的写入,即OS向指定进程的task_struct的信号位图(pending位图)比特位 置为1。

    2. 信号的保存 - 信号发送中

    信号随时都可能产生(异步),进程收到某种信号时不一定是立即处理的,因为当前我可能做着更重要的事儿,而是把它暂存起来在合适的时候处理。怎么保存信号?合适又是什么时候?

    2.1 相关概念 & 内核结构

    信号有三张表:pendingblockhandler,我们姑且不关心它们底层的数据结构,其中pending表就是用来写入接收到的信号的位图 ——

    • 实际执行信号的处理动作称为信号递达(Delivery) —— 自定义捕捉、默认、忽略
    • 信号从产生到递达之间的状态,称为信号未决(Pending) —— 本质是这个信号被暂存在task_struct的信号位图pending中。
    • 进程可以选择阻塞信号(Block) —— 本质是OS允许进程暂时屏蔽指定信号,表示该信号仍然是未决的,该信号不会被递达,直到解除阻塞方可执行递达的动作

    注意:阻塞和递达中的忽略有区别吗?yes,忽略是递达的一种方式,而阻塞是递达前的独立状态。

    grep -ER 'SIG_DFL | SIG_IGN' /usr/include	递归查找一下这两个宏
        
    #define SIG_DFL	((__sighandler_t)0)	/* Default action */
    #define SIG_IGN	((__sighandler_t)1)	/* ignore signal */
    
    • 1
    • 2
    • 3
    • 4
    • block表:信号屏蔽字,阻塞位图,表示是否阻塞,不影响接收信号但影响递达信号。是一种状态位图,同样的也是uint32_t block;无符号整数类型:比特位的位置,代表信号的编号;比特位的内容,代表信号是否被阻塞(屏蔽)。这些信号不会被递达直到解除阻塞。
    • pending表:未决位图,保存的是已经收到但没有被递达的信号。比特位的位置(第几个),代表的是哪一个信号;比特位的内容(0/1),代表是否收到信号。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。所以你看,发送信号时都要给pid和信号数。
    • handler表:函数指针数组,每个信号的编号就是该数组的下标。实际上,我们执行signal方法,对特定的信号注册自定义方法的本质就是,把handler函数地址填入到信号编号对应下标的 handler表中,这样执行的就是自定义方法。

    内核中对信号会做类似如下检测 ——

    int ishandled(int signo)
    {
        if(block & signo) { //该信号被block
            //根本就不看是否收到该信号
    	}
        else if(pending & signo) { //该信号没被block,且已经收到了
            handler_array[signo](signo); /*回调函数*/
            return 0;
    	}
        return 1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    综上,这张表应该横着看,进程内置了识别信号的方式,[三元组] 是否被屏蔽→是否被递达(→handler),即你是谁?你现在能否处理?怎么处理?

    2.2 sigset_t 及一系列系统调用函数

    从上图来看,每个信号只有一个1bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,这种类型无法直接对其进行位操作,因为这是极不安全的,就必须通过系统调用接口。因为不同OS对于sigset_t位图结构的实现是不一样的,因此不能让用户直接修改。它定义的变量和我们之前的int、double没有任何区别,都是在用户栈上。

    💛 ​下面是信号集的各种操作。

    #include 
    
    int sigemptyset(sigset_t *set);
    int sigfillset(sigset_t *set);
    int sigaddset (sigset_t *set, int signo);
    int sigdelset(sigset_t *set, int signo);
    int sigismember(const sigset_t *set, int signo);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    但这只是对用户层定义的set进行的操作,要想设置进对应进程,还需要如下函数。

    💛 ​sigpromask

    读取或更改进程的信号屏蔽字(阻塞信号集),本质是修改block表

    #include 
    int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
    返回值:若成功则为0,若出错则为-1
    
    • 1
    • 2
    • 3
    • set:输入型参数。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改;
    • oset:输出型参数。如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出;如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。
    • how ——
      • SIG_BLOCK:把set中的信号屏蔽字添加到进程的信号屏蔽字中,mask = mask|set
      • SIG_UNBLOCK:把set中的信号屏蔽字在进程信号屏蔽字的那些抹掉,mask = mask&~set,和权限掩码类似
      • SIG_SETMASK:设置当前进程的信号屏蔽字为set,mask = set

    来~ 用起来 ——

    #include    
    #include    
    #include    
        
    int main()    
    {    
      sigset_t set, oset;    
      sigemptyset(&set);    
      sigemptyset(&oset);    
        
      //屏蔽2号信号    
      sigaddset(&set, 2);    
        
      // 设置当前进程屏蔽字,获取当前进程老的屏蔽字    
      sigprocmask(SIG_SETMASK, &set, &oset);    
        
      while(1)    
      {    
        printf("people change..\n");                                                                        
        sleep(1);    
      }    
      return 0;    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    发现ctrl+c不能再终止进程 ——

    image-20220830192839061

    但是就算设置屏蔽9号信号,也没法把它屏蔽,因为9号信号被称为管理员信号,不能被屏蔽,也不能被捕捉,只能执行默认行为。

    💛 sigpending

    不对pending位图做修改,因为修改一定是OS做的,这儿只是单纯的获取进程的pending表

    #include 
    
    int sigpending(sigset_t *set);
    
    • 1
    • 2
    • 3
    • sigprocmask:输出型参数

    还有signal()是用来修改handler表的~

    来用一下吧 ——

    ① 我们不断的获取并打印当前进程的pending位图00000000 00000000 00000000 00000000;如果我的进程预先屏蔽掉2号信号,因为2号信号不会被递达,所以预期打印的pending位图是01000000 00000000 00000000 00000000 (注意打印位图不能直接通过位操作,必须通过信号集操作函数)。

    ② 当然也可以恢复2号信号的接收,把老的屏蔽字再传回去就行了;由于2号信号的默认动作是终止进程,我们看不到现象,所以我们signal()指定处理程序。

    #include                                                       
    #include
    #include                                 
                                                
    void show_pending(sigset_t* pending)
    {                   
      printf("current process pending:");
      int i;      
      for(i = 1; i<=31; i++)
      {                           
        if(sigismember(pending, i))
        {                   
          printf("1");                                       
        }                                            
        else               
        {                                              
          printf("0");
        }                      
      }                                                    
      printf("\n");                                 
    }                                             
                                                                           
    void handler(int signo)                                              
    {                                                             
      printf("signal %d has been recieved and handled...\n", signo);
    }                                                         
                                                      
    int main()             
    {                                                   
      signal(2, handler);     
      sigset_t set, oset;   
      sigemptyset(&set);    
      sigemptyset(&oset);
     
      //屏蔽2号信号
      sigaddset(&set, 2);
                                                                            
      // 设置当前进程屏蔽字,获取当前进程老的屏蔽字
      sigprocmask(SIG_SETMASK, &set, &oset);
      
      int count = 0;
      sigset_t pending;
      while(1)
      {
        sigemptyset(&pending);
        sigpending(&pending);
        //打印pending表
        show_pending(&pending);
        sleep(1);
        
        count++;
        if(count == 8)
        {
          //10s后解除对2号信号的屏蔽,处理2号信号(1->0)
          sigprocmask(SIG_SETMASK, &oset, NULL);
          printf("SIGINT can be delivered\n"); 
        }
      }
      
      return 0;
    }
    
    • 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
    • 59
    • 60
    • 61
    image-20220830192857646

    3. 信号的处理 - 信号发送后

    3.1 内核如何实现信号捕捉

    因为信号的产生是异步的,当前进程可能正在做更重要的事儿,需要暂存并在合适的时对信号延时处理 (取决于OS和进程)。

    **什么是合适,信号何时被处理?**信号是保存在进程的PCB的pending位图中,要经历处理(检测->递达[默认、忽略、自定义]),当进程从内核态 返回到 用户态时,会进行如上的检测并处理。

    执行和访问用户代码和数据时所处的状态叫做用户态,我们自己写的代码全都是在用户态执行的;执行OS的代码和数据时,计算机所处的状态是内核态。它们的主要区别在于权限

    用户调用system call函数,除了进入函数,身份也会发生切换,从用户身份变为内核身份。


    用户的身份是以进程为代表的。用户的数据和代码一定要被加载到内存;OS的数据和代码也是要加载到内存的。OS的代码是怎样被执行到的呢?是通过系统级页表映射到物理内存的代码和数据,用户级页表一个进程一个;而内核页表由所有进程共享。

    这样以来,一个进程就可以通过地址空间看到用户&内核的所有内容,那如何确定到底以什么身份执行程序呢?是通过CPU内的CR3寄存器保存当前进程的状态:用户态使用的是用户级页表,只能访问用户的数据和代码;内核态使用的是内核级页表,只能访问内核的数据和代码。

    无论进程之间如何切换,也能执行的是一份OS代码,因为都是通过3~4G的地址空间,通过同一张内核页表映射而来。

    所谓的系统调用,本质就是进程的切化成为内核身份,根据内核页表找到系统函数,再执行。大部分情况下,OS是可以在进程的上下文直接运行的。

    image-20220830192950123

    为什么一定要切换为用户态执行信号捕捉方法?OS理论上是可以执行用户的代码,但是OS身份特殊,不能轻易执行用户代码,因为可能执行恶意代码。

    3.2 sigaction

    作用和signal()完全类似,它修改的就是handler函数指针数组(但是这个函数还考虑了实时函数,可能更复杂,但是我们暂时不考虑啦~) ——

    #include 
    
    int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
    
    • 1
    • 2
    • 3
    • signum:注册一个信号。

    • act:输入型参数,调用方法。我们发现这个结构体类型和函数名一致,虽然我们自己写并不建议,但是便于记忆。

      image-20220830193009497
      • sa_handler:将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。

      • sa_mask:当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号过程中,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止,即信号不会多次捕捉,不会排队!也就是说普通信号可能会被丢弃,但是有些信号不能被丢失 —— 实时信号,内核中是用链表组织起来的。

        如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

    • oldact:输出型参数,带回老的信号catch方法,当然不想获取设为NULL就行啦~

    #include
    #include
    #include
    #include
    
    void handler(int signo)
    {
      printf("get a signo: %d\n", signo);
    }
    
    int main()
    {
      struct sigaction act;
      memset(&act, 0, sizeof(act)); 
    
      act.sa_handler = handler;
      // act.sa_handler = SIG_IGN; //忽略信号
      // act.sa_handler = SIG_DFL; //默认 - 一般以接收2号信号为结束
      
      //本质是修改当前进程的handler函数指针数组的特定内容
      sigaction(2, &act, NULL);
    
      while(1)
      {
        printf("I am missing you~\n");
        sleep(1);
      }
      return 0;
    }
    
    • 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
    image-20220830193105171

    测试sa_mask,它是signet_t类型,无法直接进行位操作,必须使用信号集操作函数。

    #include
    #include
    #include
    #include
    
    void handler(int signo)
    {
      while(1)
      {
        //正在处理信号... 现实中你不要这样干哦
        printf("get a signo: %d\n", signo);
      	sleep(1);
      }
    }
    
    int main()
    {
      struct sigaction act;
      memset(&act, 0, sizeof(act)); 
    
      act.sa_handler = handler;
      sigemptyset(&act.sa_mask);
      sigaddset(&act.sa_mask, 3);
    
      //本质是修改当前进程的handler函数指针数组的特定内容
      sigaction(2, &act, NULL);
    
      while(1)
      {
        printf("I am missing you~\n");
        sleep(1);
      }
      return 0;
    }
    
    • 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

    观察发现处理2号信号的过程中,3号信号被忽略了。

    4. 可重入函数

    • insert函数一旦重入,有可能出现问题 —— 该函数不可被重入
    • insert函数一旦重入,不会出现问题 —— 可重入函数
    image-20220830193125649

    我们所学到的大部分函数,STL、boast库中的函数,大部分都是不可重入的。

    5. volalite关键字

    #include
    #include
    
    int flag = 0;
    
    void handler(int signo)
    {
        flag = 1;
        printf("change flag 0 → 1\n");
    }
    
    int main()
    {
        signal(2, handler);
    
        while(!flag);
    
        printf("这个进程是正常退出的!\n");
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    带优化选项后,编译器在main函数中没有检测到对flag的修改,于是把flag优化到寄存器中,但是这儿会出问题,屏蔽了内存的修改 ——

    image-20220830193146611

    如何解决呢?在flag前添加关键字volatile ——

    image-20220830193201051

    为什么呢?volatile的作用是,告诉编译器,不要对我这个变量做任何优化,读取必须贯穿式的读取内存,而不要读取中间缓冲区、寄存器 —— 保持内存的可见性。

    还有一个作用 ——防止指令重排。嗯嗯有意思。

    6. SIGCHLD信号

    进程一章讲过用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地轮巡检测查询是否有子进程结束等待清理。采用第一种方,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。

    其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略。父进程可以自 定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。

    请编写一个程序完成以下功能:父进程fork出子进程,子进程调用exit(2)终止,父进程自定 义SIGCHLD信号的处理函数,在其中调用wait获得子进程的退出状态并打印。

    #include
    #include
    #include
    #include
    
    void get_child(int signo)
    {
        //waitpid() 
        printf("get a signal: %d, pid: %d\n", signo, getpid());
    }
    
    int main()
    {
        pid_t id = fork();
        signal(SIGCHLD, get_child);
        if(id == 0)
        {
            //child
            int cnt = 5;
            while(cnt)
            {
                printf("我是子进程: %d\n", getpid());
                sleep(1);
                cnt--;
            }
            exit(0);
        }
        //parent
        while(1);
        return 0;
    }
    
    • 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

    但是会出现僵尸进程 ——

    image-20220830193332413

    如果在信号捕捉函数处waitpid()回收子进程,这样就不用父进程主动等待了。

    如果我不回收子进程呢?可以显式设置忽略17号信号,当进程退出后,会自动释放僵尸进程。

    	signal(SIGCHLD, SIG_IGN);
    
    • 1

    这种方式,只在Linux平台下有效。

    处理多个子进程,并且WNOHANG防止有子进程,了解即可 ——

    #include 
    #include 
    #include 
    void handler(int sig)
    {
        pid_t id;
        while ((id = waitpid(-1, NULL, WNOHANG)) > 0)
        {
            printf("wait child success: %d\n", id);
        }
        printf("child is quit! %d\n", getpid());
    }
    
    int main()
    {
        signal(SIGCHLD, handler);
        if (fork() == 0)
        { // child
            printf("child : %d\n", getpid());
            sleep(3);
            exit(1);
        }
        while (1)
        {
            printf("father proc is doing some thing!\n");
            sleep(1);
        }
        return 0;
    }
    
    • 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
  • 相关阅读:
    金仓数据库KingbaseES数据库管理员指南--15.3. 管理同义词
    游戏道具平台|基于Springboot+Vue实现游戏道具平台系统
    Java核心知识经典面试题来啦(基础语法篇)
    递归下降语法分析程序设计与实现
    作业-11.14
    快速展示原型之Minimal API开发
    BD就业复习第四天
    uniapp vue项目把图片路径从data数据传到css(uniapp css变量)
    【云原生 · Kubernetes】apiserver高可用
    Any to Any 实时变声的实现与落地丨RTC Dev Meetup
  • 原文地址:https://blog.csdn.net/qq_54851255/article/details/126610701