进程信号

本文深入探讨了信号在操作系统中的作用,从概念到实际应用,包括信号的产生(如硬件中断、命令生成)、注册、注销、处理方式(如阻塞、自定义回调函数)以及特殊信号`SIGCHLD`和`SIGPIPE`的处理。此外,还讨论了`volatile`关键字在多线程环境中的意义和重入函数的概念。
摘要由CSDN通过智能技术生成

了解信号

概念

  信号是进程之间事件异步通知的一种方式,属于软中断;用于向某个进程通知一件事情,打断该进程当前的操作,去处理通知过来的事情;

举个例子来说明一下这个过程:
1.用户输入命令,然后再 shell 中启动一个前台程序;
2.用户按下 ctrl + c命令后,被操作系统获取到之后解释成信号,然后发送给目标前台进程;
3.进程接收到信号并处理,这是一个中断信号,所以处理结果为进程退出;
在内核中的表示

  信号在内核中的表示示意图如下,这其中每一项的概念在后面都会解释的;
在这里插入图片描述

注意
  1. shell 可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像ctrl + c这种控制键产生的信号;
  2. ctrl + c这种控制键产生的信号只能发给前台进程,一个命令后面加个&可以放到后台运行,这样 shell 不必等待进程结束就可以接受新的命令,启动新的进程;
  3. 前台进程在运行过程中用户随时可能按下ctrl + c而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的;
周期
  • 生命周期:产生——注册——注销——处理,下面会详细讲解的;

信号产生

非代码中产生
硬件产生
  • ctrl + c:中断信号,终止当前在前台运行的进程;
  • ctrl + \:退出信号,退出当前在前台运行的进程;
  • ctrl + z:停止信号,停止当前在前台运行的进程,将进程置为停止状态;
  • 异常处理:例如做除以 0 的操作,那么 CPU 中的运算单元会产生异常,然后将异常解释为 SIGFPE 信号发送给进程,再比如当前进程访问了非法内存地址,MMU 会产生异常,内核将这个异常解释为 SIGSEGV 信号发送给进程;
命令产生
  • kill -l:查看所有系统定义的信号列表;
    • 非可靠信号:下表中的 1 ~ 31 号信号;
    • 可靠信号:下表中的 34 ~ 64 号信号;
      在这里插入图片描述
  • man 7 signal:查看以上罗列出的这些信号各自在什么条件下产生,以及会进行怎样默认的处理动作;
  • jobs:显示当前作业信息;
  • fg 数字:继续运行某个停止状态的进程,这个数字是使用jobs命令显示出来的作业前面的数字序号;
  • kill pid:杀死指定 pid 的进程,原理为给进程发送了终止信号;
  • 停止进程是杀不死的,因为停止的进程是不会处理发送过来的信号的,等到变为运行状态了才会处理发送过来的信号;僵尸进程也是杀不死的,因为它本身就已经死了;
  • kill -信号名称前的数字/信号名称 pid:向指定 pid 的进程发送指定信号;
  • kill -9 pid:强制杀死指定 pid 的进程,原理为给进程发送了强制终止 SIGKILL 信号;
  • 可以强制杀死停止状态的进程,但是仍无法杀死僵尸进程,因为不能杀死一个已经死了的进程,所以我们一定要尽量避免僵尸进程的出现;
  • kill -19 pid:强制停止指定 pid 的进程,原理为给进程发送了强制停止 SIGSTOP 信号;
由代码中产生的
  • int kill(pid_t pid, int sig);:给指定 pid 的进程发送指定信号,成功返回 0,错误返回 -1;
  • int raise(int sig);:给当前进程自身发送一个指定信号,成功返回 0,错误返回 -1;
  • unsigned int alarm(unsigned int seconds);:在 seconds 秒之后给当前进程自身发送一个 SIGALRM 信号,默认动作为终止当前进程,返回上一次设置的闹钟剩余的秒数;
    • 举例:某人要小睡一觉,设定闹钟为 30 分钟之后响,20 分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为 15 分钟之后响,那么此时的返回值就是之前剩余的 10 分钟;
  • void abort(void);:给进程自身发送一个 SIGABRT 信号,默认动作为使当前进程异常终止,无返回值;
  • int sigqueue(pid_t pid, int sig, const union sigval value);:给指定进程发送一个指定信号的同时携带一个数据过去;
#include <stdio.h>                                                                                                                                                         
#include <unistd.h>    
#include <stdlib.h>    
#include <signal.h>    
int main (int argc, char *argv[])    {  
    //kill(进程ID, 信号值),给指定进程发送指定信号
    kill(getpid(), SIGINT); //结果:终止当前进程
    //给当前进程发送指定信号
    raise(SIGINT); //结果:终止当前进程
    //定时函数,时间到了执行信号 AIGALRM 的处理动作
    alarm(3); //结果:3 秒后终止当前进程
    while(1) {    
        printf("-------\n");    
        sleep(1);    
    }    
    return 0;    
}    

信号注册

注册相关概念
  • 未决:信号从产生到处理之间的状态,称为信号未决 (Pending),由 pending 位图来表示;
  • 注册:接收到信号后,经由 pending 位图表示的信号则就是已被注册的信号;
注册方式
  • pending位图:在进程 pcb 中有一个 pending 位图,它的下标对应了每个信号的序号,如果接收到了某一个信号,则将信号序号对应的下标位置元素置为 1,如果没有接收到信号,那么该值默认为 0;
    在这里插入图片描述
  • sigqueue链表:一个双向链表,当接收到一个信号后,将会在该链表中记录信息节点;

注意:当某一个信号向进程发送了多次,可靠信号和非可靠信号的注册方式是有区别的:

  • 非可靠信号:信号被发送了多次后,如果信号没有被注册,则先将 pending 位图修改为 1,然后将信号节点加入 sigqueue 链表中,如果被注册了,那么则什么也不用做,链表中不会出现多个相同信号的信息节点;
  • 可靠信号:信号被发送了多次后,如果信号没有被注册,则先将 pending 位图修改为 1,然后将信号节点加入 sigqueue 链表中,如果被注册了,那么就在链表中添加多个相同信号的信息节点;
  • 举例:看看可靠信号与非可靠信号发送多次之后有什么区别?(这里涉及到了一些信号阻塞的接口,下面会讲到,这里不需要过分纠结)
#include <stdio.h>                                                                                                                                                         
#include <unistd.h>    
#include <stdlib.h>    
#include <signal.h>    
    
void sigcb(int signo) {    
    printf("recv signal:%d\n", signo);    
}    
int main (int argc, char *argv[]) {    
    signal(SIGINT, sigcb);    
    signal(40, sigcb);    
    //阻塞信号的接口:sigprocmask(操作类型, 信号集合, 信号集合)    
    sigset_t set;    
    sigemptyset(&set);    
    sigfillset(&set);    
    sigprocmask(SIG_BLOCK, &set, NULL);    
    //这里让进程暂停一下,然后向进程发送信号
    printf("回车后,继续运行\n");    
    getchar();    
    //解除信号阻塞,然后看看效果
    printf("解除信号阻塞,查看结果\n");    
    sigprocmask(SIG_UNBLOCK, &set, NULL);    
    while(1) {    
        sleep(1);    
    }    
    return 0;    
}    

在这里插入图片描述

信号注销

  • 操作:将信号信息从进程 pcb 中移除,所进行的操作为:修改位图、删除节点,对于可靠信息和非可靠信息也是有区别的:
    • 非可靠信号:从 sigqueue 链表中删除节点,然后修改 pending 位图为 0;
    • 可靠信号:从 sigqueue 链表中删除节点,然后检查链表中是否还有相同的结点,如果没有相同结点,那么就修改 pending 位图为 0;

信号处理

概念
  • 递达:进程接收了信号之后,执行信号的处理动作称为信号递达 (Delivery),实际上就是打断进程当前操作,然后去执行接受到的信号对应的处理函数;
  • handler表:在进程 pcb 中,每一个信号都有一个 handler 表,表中的内容表示下标所对应的信号被自定义的处理动作,当进程接收到一个信号时,先进行注册,等到处理时如果发现该信号被自定义过,那么就会以信号的序号为下标,去 handler 表中找到对应的处理方式,然后执行相应的动作;
    在这里插入图片描述
处理方式
  • 默认处理方式:就是系统原本定义好的信号处理方式;
  • 忽略处理方式:它进行了处理,但是它处理的动作就是什么也不做;
  • 自定义处理方式:进行信号捕捉,用户自己定义回调函数,然后使用相应的信号捕捉接口来改变信号的默认处理动作;
    • void(*sighandler)(int);:回掉函数的格式,用户要按照这个格式来自己定义回调函数,一个包含一个整形参数的无返回值函数;
    • sighandler_t signal(int signum, sighandler_t handler);:信号捕捉接口;
      • signum:信号值,表示要修改哪个信号的处理方式;
      • handler:更改的信号的新处理方式,有三个选项;
        • SIG_DFL——信号原本的默认处理方式;
        • SIG_IGN—— 忽略处理方式,什么也不做就是它的处理方式;
        • 自定义函数的函数指针,必须按照规范来实现;
#include <stdio.h>                                                                                                                                                         
#include <unistd.h>    
#include <stdlib.h>    
#include <signal.h>    
//对 SIGALRM 信号的回调函数
void sigcb(int signo)    {    
    printf("recv signal:%d\n", signo);    
    printf("时间到了,吃一口红烧肉吧~\n");    
    //这里可以继续调用 SIGALRM 信号,这样可以持续进行定时
    alarm(3);    
}    
int main (int argc, char *argv[])    {    
    //signal(信号, 处理方式),自定义信号的处理动作    
    signal(SIGALRM, sigcb);    
    //定时函数,时间到了执行信号 AIGALRM 的处理动作,由于该信号被自定义了,所以会执行自定义函数
    alarm(3);    
    while(1) {    
        printf("-------\n");    
        sleep(1);    
    }    
    return 0;    
}    
  • 信号捕捉的流程:
    在这里插入图片描述
阻塞方式
概念
  • 阻塞:进程可以选择阻塞 (Block) 某个信号,被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作;
  • block位图:在进程 pcb 中除了 pending 位图、handler 位图之外,还有一个 block 位图,用来表示被阻塞信号的集合,它的下标对应了每个信号的序号,如果要阻塞某一个信号,则将信号序号对应的下标位置元素置为 1,如果不需要阻塞信号,那么该值默认为 0;
    在这里插入图片描述
具体操作
  • int sigprocmask(int how, const sigset_t* set, sigset_t* oldset);:对自己设置的 set 位图中的进程进行阻塞或者解除阻塞;
  • how:操作类型,主要分为以下三类:
    • SIG_BLOCK:在原来 block 位图的基础上,再阻塞 set 位图中的信号,执行的操作是:block位图 = block位图 | set位图;
    • SIG_UNBLOCK:解除 set 位图中信号的阻塞,执行的操作是:block位图 = block位图 & set位图取反;
    • SIG_SETMASK:将 block 位图设置为 set 位图的值,只阻塞当前设置的 set 位图,执行的操作是:block位图 = set位图;
  • set:自己设置的位图,这其中为 1 的比特位代表了对应序号的信号是要被阻塞还是解除阻塞;
  • oldset:保存进行改变之前的 block 位图,以便后续进行恢复,如果设置为 NULL 则表示不需要保存;
  • 返回值:成功返回 0,失败返回 -1;
  • 下面介绍一些有关 set 值设置的五个接口:
    • int sigemptyset(sigset_t* set);:清空 set 集合,使其所有位全置位 0;
    • int sigfillset(sigset_t* set);:添加所有信号到 set 集合中;
    • int sigaddset(sigset_t* set, int signum);:添加指定的单个信号 signum 到 set 集合中;
    • int sigdelset(sigset_t* set, int signum);:从 set 集合中移除指定信号 signum;
    • int sigismember(const sigset_t* set, int signum);:判断信号 signum 是否在 set 集合中;
#include <stdio.h>                                                                                                                                                         
#include <unistd.h>    
#include <stdlib.h>    
#include <signal.h>    
    
void sigcb(int signo) {    
    printf("recv signal:%d\n", signo);    
}    
int main (int argc, char *argv[]) {    
    signal(SIGINT, sigcb);    
    //sigprocmask(操作类型, 信号集合, 信号集合)    
    //定义 set 值
    sigset_t set;    
    //先清空原始内容
    sigemptyset(&set);    
    //将所有信号都放入set中
    sigfillset(&set);    
    //然后将set中的信号全部阻塞
    sigprocmask(SIG_BLOCK, &set, NULL);    
    //这里让进程暂停一下,然后向进程发送信号
    printf("回车后,继续运行\n");    
    getchar();    
    //解除信号阻塞,然后看看效果
    printf("解除信号阻塞,查看结果\n");    
    //解除set中信号的阻塞
    sigprocmask(SIG_UNBLOCK, &set, NULL);    
    while(1) {    
        sleep(1);    
    }    
    return 0;    
}    

在这里插入图片描述

信号应用

两个特殊信号
  • 下面这两个信号很特殊,是操作系统定义的不可被阻塞、不可被自定义、不可被忽略的信号,就是用来处理紧急情况的,目的在于防止让进程处于一种无法处理的状态;
    • SIGKILL:9 号信号,强制杀死信号;
    • SIGSTOP:19 号信号,强制停止信号;
SIGCHLD信号
  • 概念:当一个子进程退出后,就会给父进程发送子进程状态改变的信号 SIGCHLD;
  • 行为:这个信号的默认处理动作就是忽略,什么都不做;
  • 结果:因为这个信号被接受之后就忽略了,所以我们需要在父进程中使用wait系统调用接口来阻塞等待子进程的退出,避免出现僵尸进程,但是阻塞等待会很浪费父进程的资源;
  • 解决:如果不想等待,那么我们可以使用信号捕捉signal接口来解决,我们可以自定义 SIGCHLD 信号的处理动作,这样父进程在接受到 SIGCHLD 信号就自动处理,不用在那傻等了;
    • signal(SIGCHLD, 自定义回调函数);:执行自己定义的处理函数,但是由于该信号是不可靠信号,可能会导致信号丢失;
      • 举例:在一个子进程退出后向父进程发送了一个SIGCHLD信号,不过并没有来得及处理这个信号,此时又有其他子进程退出并向父进程发送了该信号,那么后面发的这些信号将不会被父进程接受处理;
      • 处理:所以我们需要在一次SIGCHLD信号中处理完所有的子进程退出,具体做法为利用循环,然后进行wait非阻塞等待,直到没有子进程退出了,才会将SIGCHLD信号注销,后续如果又有子进程退出,那么就可以再次接收到SIGCHLD信号;
    • signal(SIGCHLD, SIG_IGN);:显式忽略,当子进程退出后自动释放资源,父进程不用去管它,所以并不会遇到上面的信号丢失问题;
  #include <stdio.h>                                                                                                                                                         
  #include <unistd.h>    
  #include <stdlib.h>    
  #include <signal.h>    
  #include <sys/wait.h>    
  //自定义SIGCHLD信号的处理动作
  void sigcb(int no) {    
      printf("有子进程退出了\n");    
      waitpid(-1, NULL, 0);    
  }    
  int main (int argc, char *argv[]) {    
  	  //信号捕捉重定义
      signal(SIGCHLD, sigcb);    
      pid_t pid = fork();    
      if (pid == 0) {    
      	  //如果没有信号捕捉操作,那么就需要父进程阻塞等待3秒,浪费资源
          sleep(3);    
          exit(0);    
      }    
      while(1) {    
          printf("------\n");    
          sleep(1);    
      }    
      return 0;    
  } 
SIGPIPE信号
  • 概念:当管道所有读端被关闭,如果此时再进行 write 操作,那么会触发异常对应的信号 SIGPIPE,该信号会使进程异常退出;
  • 解决:有时候出现这样的异常我们并不想退出进程,那么我们也可以使用信号捕捉signal接口来自定义信号处理动作,是执行其他动作还是忽略都行;

其他

volatile关键字
  • 功能:保持内存可见性,意思就是让 CPU 每次访问变量时都从内存中重新访问数据;
  • 目的:防止编译器过度优化,在代码完成后,一般编译器都会进行优化的,对于有些经常访问的变量的值都会在 CPU 的寄存器中保存,这样就不需要频繁访问内存去取元素,但是这样也会在当变量修改后出现问题,因此在变量前面加上这个关键字就可以避免过度优化问题出现;
  • 举例:下面的代码中,在死循环时向进程发送一个 SIGINT 信号,此时 a 变为 0,
    • 如果全局变量 a 被 volatile修饰的话,那么 CPU 会从内存中重新获取变量,因为 a = 0,所以死循环就会结束;
    • 如果全局变量 a 没有被 volatile修饰的话,那么 CPU 就不会从内存中重新获取变量,a 依然保持为 1,死循环持续进行,不会结束;
#include <stdio.h>                                                                                                                                                         
#include <unistd.h>    
#include <stdlib.h>    
#include <signal.h>    
//一个被volatile修饰的全局变量   
//volatile int a = 1;    *****11*****
//没有被volatile修饰的全局变量   
//int a = 1;             *****22*****
//自定义处理函数
void sigcb(int no) {    
    a = 0;    
    printf("%d\n", a);    
}    
int main (int argc, char *argv[]) {    
	//信号处理改变
    signal(SIGINT, sigcb);  
    //死循环  
    while(a) { }    
    printf("程序正常退出\n");    
    return 0;    
}
重入函数
  • 概念:在不同的执行流程中,多个流程同时进入同一个函数开始执行;
  • 不可重入函数:函数在被重入后,会造成数据的二义性或者逻辑混乱的函数;
    • 调用了malloc / free函数,因为malloc函数是用全局链表来管理堆的;
    • 调用了标准 I/O 库函数,标准 I/O 库的很多实现都以不可重入的方式使用全局数据结构;
    • 可重入函数体内使用了静态的数据结构;
  • 可重入函数:函数在被重入后,不会出现问题的函数;
    • 不使用全局变量或静态变量;
    • 不使用用malloc或者new开辟出的空间;
    • 不调用不可重入函数;
    • 不返回静态或全局数据,所有数据都有函数的调用者提供;
    • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据;
  • 举例:在下面的代码中,当程序执行后,按ctrl + c向进程发送信号,此时由于 test 函数是不可重入函数,所以打印出来的结果会出现问题;
    出现问题原因:
#include <stdio.h>                                                                                                                                                         
#include <unistd.h>    
#include <stdlib.h>    
#include <signal.h>     
int a = 0, b = 0;      
int test() {    
    a++;    
    sleep(2);    
    b++;    
    printf("%d + %d = %d\n", a, b, a+b);    
}    
//信号回调函数
void sigcb(int no) {    
    test();    
}    
int main (int argc, char *argv[]) {    
	//重定义信号处理
    signal(SIGINT, sigcb);    
    test();    
    return 0;    
}  

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值