Linux_进程信号(进程信号生命周期_Core Dump调试_进程信号捕捉_系统调用向进程发送信号_阻塞信号_信号集函数_处理信号内核态与用户态_C语言volatile关键字_SIGCHLD信号)(

Posted dodamce

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux_进程信号(进程信号生命周期_Core Dump调试_进程信号捕捉_系统调用向进程发送信号_阻塞信号_信号集函数_处理信号内核态与用户态_C语言volatile关键字_SIGCHLD信号)(相关的知识,希望对你有一定的参考价值。

文章目录

1.进程信号的生命周期

1.在进程收到信号前,进程内部可以识别信号,并且对应信号做出相应的措施。进程信号属于进程内部的特有属性。
2.当信号到来时,进程可能不会立即处理信号,信号暂时被进程保存起来。
3.进程开始处理信号,进程处理信号有三种处理方法

  • 默认行为(按照信号内置的处理方案处理信号)
  • 自定义行为(信号捕捉,按照自己设定的方法处理信号)
  • 忽略信号(不做任何处理)

2.信号的种类与记录

信号的种类

如上图白色背景的是普通信号,黑色背景的是实时信号

信号本质是系统提供的宏值,宏的名称为后面的英文,宏的值为前面的数值。
eg:

普通信号的记录(位图)

信号记录在进程的(task_struct)PCB中,以位图这种数据结构来记录普通信号是否产生。

普通信号的范围是1到31
一个无符号整数是32位,第0位空开正好可以储存信号。


进程收到信号本质是:操作系统修改进程内部的信号位图。

3.信号产生

命令发送信号(kill -信号编号/信号名称 -进程pid)

eg:向进程发送2号信号

键盘按键向前台进程发送信号

eg:ctrl+c向前台进程进程发送2号信号

4.Core Dump核心转储


查看信号手册,Action中Core表示当进程收到对应信号时,将进程在内存的核心数据储存到磁盘上。为了定位问题。
命名一般为core+错误进程的pid

开启核心转储(ulimit -c)

ulimit -a查看core file size的大小,如果大小为0表示关闭核心转储

core file size大于0表示开启核心转储



6716是退出进程的进程ID

Core Dump调试(gdb下core-file+core文件名)


进程退出时waitpid()函数输出型参数status函数结束后获得的值如上图,倒数第9位代表core dump

退出码:(status>>8)&0xff
退出信号:(status&0x7f)
core dump:(status>>7)&1

eg:以除零错误举例子


gdb下core-file+core文件名


如上图,直接定位到问题所在位置,进程退出因为收到8号信号,在第7行

*为什么C/C++程序会崩溃

当发生除零,野指针,越界错误时,程序崩溃,本质是由操作系统向对应错误进程发送信号导致的。

cpu中存在状态寄存器.当发生除零错误时,状态寄存器中的溢出标志位会从0变为1。
操作系统是软硬件资源的管理者,操作系统识别到硬件溢出标志位变化后,将这种信号包装为信号发送给对应进程,进而导致进程崩溃。

同理:例如野指针和越界问题是因为进程地址空间在MMU(硬件单元+页表)映射到物理内存时,操作系统识别到MMU的状态信息中不存在对应的映射关系,向对应页表的进程发送信号使程序崩溃。

这些错误一定在硬件上会有所表现,操作系统识别到硬件错误使得进程崩溃。

5.信号捕捉(signal函数signal.h)


传入要捕捉的信号编号和返回值为void函数指针。这个函数的参数的值为信号编号
eg:

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

void hander(int sig)//sig的值就是注册的信号编号

  printf("got a signal! signal=%d\\n",sig);


int main()

  for(int i=1;i<32;i++)//捕捉1到32号信号
  
    signal(i,hander);
  
  while(1)
    sleep(1);
  
  return 0;


注意:centos 7中9号信号无法被捕捉,当进程收到9号信号时一定会退出

6.向指定的进程发送指定的信号(kill函数signal.h+sys/types.h)

eg:模拟kill命令

kill命令的结构为:kill+进程pid+信号编号 所以要向main函数传入命令行参数

argv[0]代表程序名称,argv[1]代表进程pid,argv[2]代表信号编号

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<signal.h>
#include<stdlib.h>
void help(const char*Str)

  printf("use %s +pid +signal num\\n",Str);


int main(int argc,char*argv[])

  if(argc!=3)
    help(argv[0]);
    return 1;
  
  pid_t Id=atoi(argv[1]);
  int signal=atoi(argv[2]);

  kill(Id,signal);
  return 0;

进程自己向自己发送信号(raise函数signal.h)


参数解释:sig要发送的信号编号

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

void hander(int arg)

  printf("Get Signal %d\\n",arg);
  signal(2,hander);//防止捕捉一次信号后失效,这里在注册一次2号信号


int main()

  signal(2,hander);
  while(1)
  
    printf("Set Signal\\n");
    sleep(1);
    raise(2);//每隔1s向自己发送2号信号
  

进程自己向自己发送6号信号(abort函数stdlib.h)

#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
void hander(int arg)

  printf("Get Signal %d\\n",arg);


int main()

  signal(6,hander);
  while(1)
  
    printf("Set Signal\\n");
    sleep(1);
    abort();
  

由软件条件产生信号(alarm函数unistd.h)

在进程与进程用管道通信时,当读取端关闭后,写入端会立即被操作系统终止。本质是操作系统向进程发送13号信号SIGPIPE,当前写入条件不允许这种信号称为由软件条件产生的信号



函数参数:
传入一个时间,在这个时间后,进程会收到14号信号终止

返回值为当接受到SIGALRM信号后与设定的时间的差值。
当正常运行时返回值为0,比如设定3秒在2秒时就受到了SUGALRM(14号)信号,返回值为1

eg:

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

int cout=0;
void hander(int arg)

  printf("Get Signal %d\\n",arg);
  printf("cout=%d\\n",cout);



int main()

  signal(SIGALRM,hander);
  alarm(1);
  while(1)
  
    cout++;
  

7.阻塞信号

  1. 进程实际处理信号的操作称为信号递达
  2. 信号从产生到递达的状态称为信号未决(Pending)
  3. 进程可以阻塞某种信号(Block)
  4. 被阻塞的信号状态为未决,直到进程解除对此信号的阻塞,才执行递达动作。
  5. 阻塞和忽略不同,信号被阻塞就不会递达,忽略是递达之后一种处理动作

深入理解信号在进程PCB中的存储(block,pending位图,handler函数指针数组)

当进程收到信号后,先将pending位图的对应位置改为1,在看block位图对应比特位值是否为1.如果对应的信号没有被阻塞,则开始递达在handler数组中找对应的递达方法,否则对应的信号被阻塞。

注意:对于普通信号,如果在这个信号被阻塞的过程中,信号产生多次,在Linux中只记录一次这个信号;对于实时信号而言,在递达前如果产生多次,这些信号被依次放入队列中。

信号集操作函数修改block,获取pending位图(sigset_t)

信号集操作函数都在signal.h的头文件下,其调用成功返回0失败返回-1。特殊的是sigismember函数其返回值是一个bool值,用来判断一个信号集中是否包括某种信号,返回1代表包括,0代表不包括,-1代表调用失败

sigset_t变量定义在用户区的一个变量,它的修改不会影响进程,我们要通过系统调用将这个设置好的变量传入到操作系统中,让操作系统修改进程的pcb达到修改位图的目的

sigemptyset函数(初始化sigset_t变量)


函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号

sigaddset函数(将信号添加到sigset_t变量中)

sigdelset函数(将信号从sigset_t中删除)

sigfillset函数(将sigset_t位图中所有比特位置1)

函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。

完成的也是sigset_t变量的初始化

sigismember函数(判断一个信号集的有效信号中是否包含某种信号)


1代表包括,0代表不包括,-1代表调用失败

sigprocmask函数(读取或更改进程的信号屏蔽字(阻塞信号集)修改block位图)


参数解释:

how参数可选值

SIG_BLOCKset中包含了想要添加到当前信号屏蔽字的信号
SIG_SETMASK设置进程信号屏蔽字与set相同
SIG_UNBLOCKset中包含了我们想要解除阻塞的信号


oset:当修改进程的block位图时,原来进程的block位图通过oset输出型参数输出。

返回值:成功为0,失败-1;

sigpending函数(获取进程的pending位图)


set:是输出型参数,将进程的pending位图输出返回。
成功返回0,失败返回-1.

demo_打印进程的pending位图

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

void PrintPending(sigset_t*pending)

  int i;
  for(i=1;i<=31;i++)//要判断所有信号是否在sigset_t变量中
  
    if(sigismember(pending,i))//判断第i信号是否在pending中,如果存在打印1,否则打印0
    
      printf("1 ");
    
    else 
    
      printf("0 ");
    
  
  printf("\\n");//刷新缓冲区


void hander(int arg)

  printf("%d signal 递达\\n",arg);


int main()

  //注册2号信号
  signal(2,hander);
  sigset_t set,oset;
  //信号集初始化
  sigemptyset(&set);
  sigemptyset(&oset);

  sigaddset(&set,2);//将2号信号添加到set中

  sigprocmask(SIG_SETMASK,&set,&oset);//更改进程的信号屏蔽字(阻塞信号集)以覆盖式修改block位图

  sigset_t pending;

  int cout=0;
  while(1)
  
    sigemptyset(&pending);

    sigpending(&pending);//获取当前进程的pending位图

    PrintPending(&pending);
    sleep(1);
    cout++;
    if(cout>=5)//将进程的信号屏蔽字复原
    
      sigprocmask(SIG_SETMASK,&oset,NULL);
    
  
  return 0;

运行结果为:

如上图,阻塞了2号信号后向进程发送2号信号pending位图2号位置值变为1,代表收到了这个信号;之后取消了对2号信号的阻塞,2号信号递达后被捕捉执行自定义行为,pending位图中对应位置的值从1到0。

8.处理信号

进程在收到信号后在递达要处理信号时不是立即处理,而是在内核态切换到用户态时进行信号的处理

内核态与用户态

用户态:执行用户自己的代码时系统的状态称为用户态。

内核态:执行系统调用的代码,本质是调用内核的代码,必须要内核级别的权限

每一个进程不仅拥有用户级页表,还有一张内核页表,用来映射内存中的操作系统代码和进程的进程地址空间,实现进程切换等操作。

如上图,进程地址空间中的内核空间通过内核页表映射到内存中的操作系统代码。
每个进程的进程地址空间中的内核空间映射到的都是内存中的操作系统代码

只有在内核态才可以访问内核空间,内核态通常用来执行操作系统的代码状态,权限较高。
用户态是一种执行用户普通代码的状态,是一种受监管的状态

内核态与用户态切换

cpu上存在寄存器可以标识当前处于用户态还是内核态。

同时cpu上也存在寄存器保存页表,状态切换本质上是切换cpu上的用户页表和内核页表。

切换过程如下图

需要注意的是:
当用户自定义信号捕捉时,处于内核态也不可以访问用户态的代码,为了防止用户执行危险操作。所以此时要先从内核态切换为用户态访问用户自定义捕捉代码,之后再进入内核态,最后再从内核态到用户态,返回到主控制流中断的位置。

大体切换流程如下图

sigaction函数(与signal函数相似实现信号捕捉_读取和修改与指定信号相关联的处理动作)

参数解释:
sig是要捕捉的信号
oact是输出型参数,是将原来信号的处理方法通过这个输出型参数返回,不关心可以设置为NULL.
act是我们自定义的方法
struct sigaction结构体:

1.sa_handler:设置的信号捕捉方法。
SIG_IGN:忽略信号
SIG_DFL:默认操作
自定义捕捉
2.sa_sigaction:处理实时信号
3.sa_flag:包含一些选项,一般设置为0
4.sa_restorer:一般设置为NULL
5.sa_mask:
如果此时进程正在处理2号信号,系统会自动阻塞2号信号。保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。设置sa_mask可以在处理2号信号的过程中同时屏蔽其他信号

返回值:
调用成功则返回0,出错则返回- 1
eg:

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

struct sigaction act,oact;

void handler(int signo)

  printf("Get %d signal\\n",signo);
  //恢复2号信号处理动作
  sigaction(2,&oact,NULL);


int main()

  //初始化结构体
  memset(&act,0,sizeof(act));
  memset(&oact,0,sizeof(oact));
  act.sa_flags=0;
  act.sa_handler=handler;
  sigemptyset(&act.sa_mask);//初始化sa_mask;
  sigaction(2,&act,&oact);
  while(1)
  
    printf("Hello Linux\\n");
    sleep(1);
  
  return 0;

9.C语言volatile关键字(保持内存的可见性)

分析下面的代码:

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

int flag=0;

void hander(int signo)

  printf("Get %d Signal\\n",signo);
  flag=1;


int main()

  signal(2,hander);
  while(!flag);
  printf("End\\n");
  return 0;

当接受到2号信号时,flag变为1,while循环条件不满足,此时会打印End


但是flag在main函数执行流中没有被修改,在编译器优化程度较高时,可能将flag变量放入到寄存器变量中。此时如果接受到2号信号,在内存中的flag变为1,但是cpu寄存器中的flag还是0,while循环看不到,程序死循环

gcc -O3是较高的优化级别

所以给flag变量加上volatile关键字,每次在检测这个变量的值时,先从内存中读取,再放到寄存器中,保证内存的可见性

10.SIGCHLD信号(通过信号的方式回收子进程)

为了避免僵尸进程的出现,子进程在退出时必须由父进程等待。
但在子进程退出时会发送SIGCHLD信号。我们可以捕捉这个信号,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可
eg:

#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
#include<sys/wait.h>

void handler(int signo)

  printf("Get %d signal\\n",signo);
  int ret=0;
  while((ret=waitpid(-1,NULL,WNOHANG))>0)//非阻塞式等待任意进程,不关心退出码
  
    printf("wait succeed pid=%d\\n",ret);
  


int main()

  signal(SIGCHLD,handler);

  if(fork()==0)//子进程
  
    printf("clild exit child pid=%d\\n",getpid());
    sleep(2);
    exit(1);
  
  while(1);
  return 0;

以上是关于Linux_进程信号(进程信号生命周期_Core Dump调试_进程信号捕捉_系统调用向进程发送信号_阻塞信号_信号集函数_处理信号内核态与用户态_C语言volatile关键字_SIGCHLD信号)(的主要内容,如果未能解决你的问题,请参考以下文章

Linux学习_signal信号说明

进程信号

Linux_信号与信号量

Linux练习_进程间信号练习

exit()与_exit()函数的区别

信号_什么是信号_学习信号有什么意义