Linux信号

文章目录

  • 一、信号的概念
  • 二、ctrl+c与信号
    • 2.1前台与后台进程
    • 2.2查看信号
    • 2.3信号的处理方式
    • 2.4ctrl+c信号与硬件
  • 三、信号的产生
    • 3.1键盘组合键
    • 3.2kill命令
    • 3.3系统调用
      • 3.3.1kill函数
      • 3.3.2raise函数
      • 3.3.3abort函数
    • 3.4异常
      • 3.4.1除0错误
      • 3.4.2野指针
      • 3.4.3解析
    • 3.5软件条件
      • 闹钟
    • 3.6core dump标志
  • 四、信号的发送和保存
    • 4.1信号的发送
    • 4.2信号的保存
      • 4.2.1信号常见相关概念
      • 4.2.2handler表
      • 4.2.3pending表
      • 4.2.4block表
  • 五、信号的处理
    • 5.1信号集sigset_t
    • 5.2信号集操作函数
    • 5.4sigprocmask
    • 5.5sigpending
    • 5.6代码演示
  • 六、再谈信号捕捉
    • 6.1再谈进程地址空间
    • 6.2浅谈操作系统
    • 6.3浅谈寄存器
    • 6.4小结
  • 七、补充
    • 7.1sigaction函数
      • 7.1.1处理信号的时机
      • 7.1.2自动屏蔽
    • 7.2可重入函数
    • 7.3volatile关键字
    • 7.4SIGCHLD信号

一、信号的概念

生活中收到快递取件码,等红绿灯,婴儿哭啼都是收到了对应的信号

  • 我们能够识别这些信号
  • 知道这些信号的处理方法--该如何做--我们学习过了
  • 信号到来,我们可能不会立即处理,信号产生后--信号处理时中间有一个时间窗口--需要记住信号到来
  1. 进程必须能够识别和处理信号,信号没有产生,也要具备处理信号的能力--信号的处理能力,内置于进程功能
  2. 进程收到具体信号,可能不会立即处理,在合适的时候处理
  3. 进程具有保存哪些信号已经发生的能力

二、ctrl+c与信号

当我们循环打印的时候是可以通过ctrl+c来杀掉进程的

2.1前台与后台进程

当我们运行起来一个程序的时候,我们会发现输任何指令都没有用,这种是前台进程

当我们添加&在运行程序后面时,就变成了后台进程,此时我们可以执行指令但是ctrl+c杀不掉进程,因为bash变成前台进程了

在Linux中,一次登录,一个终端,一般会配上一个bash,每一个登录,只允许一个进程时前台进程,可以允许多个进程是后台进程

一开始bash是前台进程可以接收指令,当我们运行程序之后bash就变成后台进程了就接收不了指令了

所以前台进程和后台进程的本质区别就是谁能获取键盘的输入

ctrl+c在进程变成后台进程的时候是收不到的,发给的是前台进程bash--bash杀不掉是因为内部对信号做了特殊处理

2.2查看信号

复制代码
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+1  47) SIGRTMIN+13
 48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-1  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. kill - l:查看所有的信号
  2. ctrl+c的本质就是收到了2号信号SIGINT
  3. 1号-31号信号--普通信号
  4. 34号--64号信号--实时信号--立即处理信号

2.3信号的处理方式

  1. 默认动作
  2. 忽略
  3. 自定义动作--信号的捕捉

当我们面对小婴儿啼哭时,父母会默认去照顾宝宝,行人会去忽略,但是有些好心人可能就会去哄宝宝,这就对应了上面三种情况

ctrl+c:进程收到了2号信号--默认就是终止自己

复制代码
#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

signal函数:设置信号的自定义处理方法--修改特定进程对信号的处理动作

typedef void (*sighandler_t)(int):返回值为void参数int(表示收到了哪一个信号)的函数指针类型

signum:信号编号

handler:自定义捕捉动作

复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;

void myhandler(int signo)
{
  cout<<"get a signal is "<<signo<<endl;
}

int main()
{
 signal(SIGINT,myhandler);
  while(1)
  {
   cout<<"i am not signal"<<endl;
   sleep(1);
  }
  return 0;
}

当我们发送2号信号的时候不再是终止进程而是处理我们设定好的自定义动作,并且确认了ctrl+c发送的就是2号信号

  1. 信号的处理方式只能选择一个进行处理,你执行默认动作就不能执行自定义动作
  2. signal函数方法只需要设置一次,往后都是有效的,玩游戏的时候你和你的朋友约定好了就是五排,之后没凑齐五个人都不会开
  3. signal函数方法是在我们收到了信号之后才调用的,只有你玩游戏的时候才会触发五排约定,其他时间不会
  4. 不能给所有的信号都设置对应的处理方法,防止不能杀死或暂停进程
  5. 信号捕捉方法是可以被多个signal函数所使用的,所以为了区分信号就需要传入参数来区分

2.4ctrl+c信号与硬件

  1. 操作系统先知道键盘被按下
  2. 键盘文件有对应自己内核的文件描述符和缓冲区
  3. 键盘读取本质是把键盘硬件上的数据拷贝到键盘文件对应的缓冲区--前提是知道键盘上有数据,操作系统不可能去轮询查看硬件是否有数据
  4. CPU上有很多针脚,键盘和CPU是可以间接的连接的,虽然CPU不从键盘当中读数据,但是键盘可以在硬件上向CPU发送硬件中断 ,每一种中断都有中断号的概念,从中断号就可以识别出是哪个硬件,通过向CPU寄存器充放电来写入中断号
  5. 操作系统在启动的时候会生成中断向量表就是数组,存放方法(访问外设)地址,当CPU收到了中断号之后会立马让操作系统根据中断号在中断向量表找对应的读取方法,就能让键盘上的数据从外设拷贝到内存
  6. 信号 就是以软件的形式模拟的硬件中断
  7. 操作系统在拷贝数据时会对数据做判断时数据还是控制(ctrl+c就会转化成为2号信号发送回进程),是控制就不会放到缓冲区,拷贝前和拷贝完成都会发送中断
  8. 当我们向键盘写入数据的时候,如果想要在显示器显示出来就可以将键盘的缓冲区拷贝一份给显示器,这就叫做回显,所以我们在上面转为后台进程运行程序的时候,虽然我们输入的回显的是乱的指令在显示器上,可是在键盘的缓冲区读取到的却是完整的指令,所以执行并不影响

前台进程在运行过程中用户随时可能按下ctrl+c而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步--程序在运行过程期间不会因为没有信号而停止运行

三、信号的产生

信号产生的方式,无论信号是如何产生,都是由操作系统统一发给进程的,因为操作系统是进程的管理者

3.1键盘组合键

比如(2)ctrl+c、(3)ctrl + \、(19)ctrl+z

复制代码
  for(int i=1;i<=31;i++)
  {
    signal(i,myhandler);
  }

不是所有的信号都能被捕捉的,9号(杀进程)和19号(暂停进程)就不能被捕捉,防止不能杀掉或者暂停异常进程

3.2kill命令

kill signo pid

3.3系统调用

3.3.1kill函数

复制代码
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

发送信号给进程

复制代码
void Usage(string proc)
{
cout<<"Usage\n\t"<<proc<<" signo pid\n\t";
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
Usage(argv[0]);
exit(1);
}
int signo=std::stoi(argv[1]);
pid_t pid=std::stoi(argv[2]);
int n=kill(pid,signo);
if(n==-1)
{
perror("kill");
exit(1);
}
return 0;
}

3.3.2raise函数

复制代码
#include <signal.h>

int raise(int sig);

给调用者发送对应的信号

复制代码
void myhandler(int signo)
{
  cout<<"get a signal is "<<signo<<endl;
}
int main()
{
  signal(2,myhandler);
  int cnt=0;
  while(true)
  {
   cout<<"i am a process pid is "<<getpid()<<endl;
   sleep(1);
   cnt++;
   if(cnt%2==0) raise(2);
  }
}
复制代码
kill(getpid(),2);

raise就相当于kill这样

3.3.3abort函数

复制代码
#include <stdlib.h>

void abort(void);

引起一个正常的进程直接终止,SIGABR就是6号信号

复制代码
while(true)
{
cout<<"i am a process pid is "<<getpid()<<endl;
abort();
}
复制代码
void myhandler(int signo)
{
  cout<<"get a signal is "<<signo<<endl;
}
int main()
{
  signal(6,myhandler);
  int cnt=0;
  while(true)
  {
   cout<<"i am a process pid is "<<getpid()<<endl;
   abort();
  }
}

但是我们对6号信号捕捉它还是终止了

我们调用kill -6还是不能够让这个进程终止

所以我们abort除了要捕捉对应的自定义方法,内部还必须让进程终止

3.4异常

3.4.1除0错误

复制代码
int main()
{
  cout<<"proc begin "<<endl;
  sleep(1);
  int a=1;
  a/=0;
  sleep(1);
  cout<<"proc end "<<endl;
  return 0;
}

除0错误本质是收到了8号信号SIGFPE

当我们捕捉八号信号的时候

复制代码
void myhandler(int signo)
{
    cout<<"get a signal is "<<signo<<endl;
}
signal(SIGFPE,myhandler);

此时进程并没有退出,默认是进程终止,捕捉后执行自定义动作,但是问题是信号一直被触发

3.4.2野指针

复制代码
int* p=nullptr;
*p=100;

野指针错误本质是收到了11号信号SIGSEGV

复制代码
void myhandler(int signo)
{
    cout<<"get a signal is "<<signo<<endl;
}
signal(SIGSEGV,myhandler);

进程收到异常信号不一定会退出,会执行异常信号的处理方法

3.4.3解析

除0错误

  1. CPU内部有寄存器eip记录代码运行到哪一行
  2. CPU内部存在状态寄存器,状态寄存器内部有标志位,标志位有一个溢出标志位,当除0的时候就是相当于处于无穷小后变成无穷大就相当于越界,溢出标志位由0->1
  3. 这些寄存器内的数据很多都属于进程上下文,该进程出异常的进程上下文与进程切换无关,老师批改作业是一个一个批改的,这个学生出错了和下一个学生的批改无关,虽然我们修改的是CPU内部的状态寄存器,不会影响其他进程,也不会波及操作系统
  4. 操作系统是硬件的管理者,可以检测到CPU内部的状态,然后检测出异常向CPU发送信号,导致进程崩溃

野指针

  1. 页表地址的转化是由MMU(内存管理单元)完成的
  2. 野指针是虚拟到物理地址转化失败
  3. CPU内存在寄存器,虚拟地址转换失败后会将虚拟地址放到这个寄存器当中
  4. 转化失败CPU内部会发生硬件报错就会被操作系统识别到

小结:

  1. 操作系统通过不同种类寄存器的报错来识别具体是溢出还是越界等报错
  2. 在进程发生异常不崩溃时,代表这个进程也要一直被调度运行,上下文错误一直存在,操作系统一直检测到进程有这个异常,就一直给这个进程发送信号
  3. 我们无法在进程运行时解决这个异常,毕竟解决就要改硬件的状态寄存器,异常能让我们捕捉不是让我们解决,而是让我们了解是什么原因异常了

3.5软件条件

在管道文件中,一方进程把读端关闭了,由于操作系统不做任何浪费效率的事情,操作系统就会杀掉写端的那个进程,会发送13号信号SIGPIPE给进程

闹钟

复制代码
#include <unistd.h>

unsigned int alarm(unsigned int seconds);

调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发14号信号SIGALRM,该信号的默认处理动作是终止当前进程

返回值:闹钟响的时候的剩余时间

复制代码
  int n=alarm(5);
  while(1)
  {
    cout<<"proc is running"<<endl;
    sleep(1);
  }

对14号信号进行捕捉

复制代码
signal(SIGALRM,myhandler);

闹钟只响一次,响过之后不会再响,因为不是异常

闹钟的返回值其实是当你设置了新的闹钟,上一个闹钟可能还有剩余时间,比如上一个闹钟设置100秒,我新设置的一个闹钟3秒,那么新的闹钟响的时候返回的就是上一个闹钟的剩余时间97秒

复制代码
void myhandler(int signo)
{
  cout<<"get a signal  "<<signo<<endl;
  int ar=alarm(3);
  cout<<"last alarm is "<<ar<<endl;
}
int main()
{
  signal(SIGALRM,myhandler);
  int n=alarm(100);
  while(1)
  {
    cout<<"proc is running pid is "<<getpid()<<endl;
    sleep(1);
  }
}

当我们向进程发送14号信号的时候执行对应的捕捉方法新的闹钟提前响了,返回的就是上一个闹钟的剩余时间了

操作系统可能会有大量的进程设置闹钟,所以操作系统就要对这些闹钟先描述再组织,对闹钟的管理就变成了对链表的增删查改,闹钟由于需要提前设置未来时间就需要用到时间戳,当当前时间大于未来时间就代表超时了

3.6core dump标志

core dump标志为0/1表示是core还是term

默认云服务器上的core功能是被关闭的

我们可以通过ulimit -a查看core文件是否被关闭

如果是0 就可以通过ulmit -c 大小 来进行打开core文件来设置大小

打开系统的core dump功能,一旦进程出现异常,操作系统会将进程在内存中的运行信息,形成core.pid文件 ,这个文件我们可以通过gdb调试

通过core-file core.pid文件来查看具体是哪一行的出错信息

这就是先运行,再core-file事后调试

四、信号的发送和保存

4.1信号的发送

  1. 普通信号在进程PCB(task_struct)中使用位图进行管理保存,用比特位的位置表示是哪一个信号,比特位的0/1表示有无信号
  2. 发送信号本质就是操作系统修改进程PCB内位图的信号位置对应的比特位
  3. 常规信号在处理之前产生多次只计一次,而实时信号在处理之前产生多次可以依次放在一个队列里面

4.2信号的保存

4.2.1信号常见相关概念

  • 实际执行信号的处理动作称为信号递达(Delivery)
  • 信号从产生到递达之间的状态,称为信号未决(Pending)
  • 进程可以选择阻塞(Block)某个信号
  • 被阻塞的信号产生时将保持在未决状态,直到进程接触对此信号的阻塞,才执行递达的动作
  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会被递达,而忽略是在递达之后可选的一种处理动作

操作系统会在进程的PCB内核中维护三张关于信号的表分别是block表,pending表和handler表

4.2.2handler表

信号的范围是[1,31],每一种信号都有对应的处理方法

在进程的PCB当中就要为信号维护一份handler表,类型是函数指针数组

handler表里存放着信号默认处理方法的地址

复制代码
 sighandler_t signal(int signum, sighandler_t handler);

当设置进用户提供的自定义方法时,根据signum来找到数组下标,将用户自定义的hanler方法的地址填入进数组下标即可

  • 信号忽略--SIG_IGN---信号在递达(执行处理方法)之后对该信号的不执行

    int main()

    {
    signal(2,SIG_IGN);
    while(1)
    {
    cout<<"process is running "<<endl;
    sleep(1);
    }
    return 0;
    }

在我们收到2号信号之后对该信号执行忽略的处理方法

  • SIG_DFL--执行信号的默认处理方法

    signal(2,SIG_DFL);

  • handler--执行信号的自定义处理方法

自定义方法我们就不在这里做过多阐述了

4.2.3pending表

pending表是位图结构用来记录是否收到信号以及收到了哪些信号

信号产生过,没被阻塞,pending表的状态变化就是对应的信号由0->1->0,代表收到了信号以及处理完成该信号的处理方法,在信号执行处理方法之前就会由1->0(前提是未被阻塞,可以处理)

信号产生过,被阻塞,pending表的状态变化就是对应的信号由0->1,只代表收到了该信号,但没对该信号做处理

4.2.4block表

block表是位图结构用来记录特定信号是否被屏蔽

特定信号的比特位为1代表该信号被阻塞,即便信号产生,也不会对该信号处理,直到信号从block表被移除

五、信号的处理

5.1信号集sigset_t

因为pending表和block表都是用位图管理起来的,所以操作系统为了方便对数据管理,统一给用户提供用相同的用户数据类型sigset_t来存储

这个类型可以表示信号的有效无效状态

在block表阻塞信号集里有效和无效表示是否被阻塞

在pending表未决信号集里有效和无效表示该信号是否处于未决状态(被处理)

5.2信号集操作函数

用户不能自己通过位操作来对进程pcb里面的信号集,因为操作系统是进程的管理者,理应由操作系统对进程的内核数据结构进行修改,所以我们需要使用系统调用接口来对信号集做处理

复制代码
#include <signal.h>
int sigemptyset(sigset_t *set);//清空信号集,全置0
int sigfillset(sigset_t *set);//设置位图全为1
int sigaddset (sigset_t *set, int signo);//在信号集中添加信号
int sigdelset(sigset_t *set, int signo);//在信号集中删除信号
int sigismember(const sigset_t *set, int signo);//判断信号是否在信号集当中

5.4sigprocmask

调用sigprocmask函数可以读取或更改进程的信号屏蔽字(阻塞信号集)

复制代码
#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t
*oldset);

how:指定操作选项

  • SIG_BLOCK:set包含了我们希望添加到当前阻塞信号集的信号,相当于mask=mask|set(本质就是在原来的基础之上做新增)
  • SIG_UNBLOCK:set包含了我们希望从当前阻塞信号集中解除阻塞的信号,相当于mask=mask&~set(去掉我们传入的集合里的所有信号)
  • SIG_SETMASK:设置当前阻塞信号集为set所指向的值,相当于mast=set(覆盖式的设置全新的字段)

set:输入型参数,用于修改信号集

oset:输出型参数,保存修改之前的阻塞信号集,以便于恢复

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

5.5sigpending

调用sigpending函数可以读取当前进程的未决信号集,通过set参数传出

复制代码
#include <signal.h>

int sigpending(sigset_t *set);

set:输出型参数,带出pending表当中的数据

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

5.6代码演示

复制代码
void PrintPending(sigset_t& pending)
{
  for(int i=31;i>=1;i--)
  {
    int n=sigismember(&pending,i);//判断信号是否在信号集
    if(n>0)
    {
      cout<<"1";
    }
    else
    {
      cout<<"0";
    }
  }
  cout<<endl;
  cout<<endl;
}
void handler(int signo)
{
  cout<<" catch a signo "<<signo<<endl;
}
int main()
{
  signal(2,handler);
  cout<<"pid is "<<getpid()<<endl;
  
  //1.将2号信号添加到阻塞信号集
  sigset_t bset;
  sigset_t oset;
  sigemptyset(&bset);
  sigemptyset(&oset);//清空信号集
  sigaddset(&bset,2);

  //但是这只是在用户空间栈上的操作并没有设置进内核

  sigprocmask(SIG_SETMASK,&bset,&oset);

  //2.打印进程的pending表
  sigset_t pending;
  int cnt=0;
  while(1)
  {
  sigpending(&pending);//带出pending表
  PrintPending(pending);//打印pending表
  sleep(1);
  
  //解除阻塞,以便观察到现象
  cnt++;
  if(cnt==20)  
  sigprocmask(SIG_SETMASK,&oset,nullptr);
  }
  
  //3.发送2号信号

  return 0;
}

现象:当我们使用kill命令像进程发送2号信号后,pending表收到了2号信号但是没有递达由0->1,20秒后解除对2号信号的屏蔽,执行2号信号的处理动作,pending表由1->0

试想如果把所有的信号都屏蔽是不是所有的信号都不能被递达了呢

答案肯定是不可能的,就如我们先前9号和19号信号不可以被捕捉一样操作系统肯定会防着我们

复制代码
int main()
{
   cout<<"pid is "<<getpid()<<endl;
  sigset_t bset,oset;
  sigemptyset(&bset);
  sigemptyset(&oset);
  for(int i=1;i<=31;i++)
  {
    sigaddset(&bset,i);
  }
  sigprocmask(SIG_SETMASK,&bset,&oset);
  sigset_t pending;
  while(1)
  {
   sigpending(&pending);
   PrintPending(pending);
   sleep(1);
  }
  return 0;
}

9号和19号不可被屏蔽

六、再谈信号捕捉

  1. 信号的处理是在我们的进程从内核态返回到用户态的时候,进行信号的检测和处理,pending表,block表等都属于内核的数据结构,进程只有处于内核态才能对信号做处理
  2. 当我们调用系统调用的时候,操作系统会自动把我们的身份从用户身份变为内核身份,当返回的时候才会把我们的内核身份变为用户身份
  3. CPU内部可以给自己发生中断,比如int 80就可以让用户态陷入内核态
  4. 内核态--允许访问操作系统的代码和数据
  5. 用户态--只能访问用户自己的代码

6.1再谈进程地址空间

  1. 进程地址空间中0-3GB我们称之为用户地址空间,3-4GB称之为内核地址空间

  2. 内核地址空间映射的是操作系统的代码和数据,因为操作系统最先被加载到内存,所以操作系统的代码和数据一般都会被加载到物理内存最低侧的位置

  3. 目前认为内核地址空间也需要有内核级页表

  4. 所有进程共用一份内核级页表,独立用户级页表,所以每个进程看到3-4GB的内容都是一样的,整个系统中,进程再怎么切换,3-4GB的空间的内容是不变的

  5. 进程视角:我们调用系统中的方法,就是在我们自己的地址空间中进行执行的

  6. 操作系统视角:任何一个时候,都有进程执行,我们想执行操作系统的代码,就可以随时执行,因为任何进程自己的内核地址空间都能通过内核级页表找到操作系统的代码和数据

6.2浅谈操作系统

操作系统的本质:基于时间中断的一个死循环

  1. 计算机硬件中,有一个时间芯片,每隔很短的时间,向计算机发送时间中断,让操作系统被动的执行代码
  2. 时间芯片向CPU发送时间中断,操作系统内就会执行中断向量表里面的代码,比如检查什么的
  3. 操作系统完全就是死循环,然后等待着时间中断来让他执行代码
  4. 这就是硬件在推动着操作系统在走,为什么代码加载到内存就可以跑了,因为操作系统在调度,操作系统也是软件,是由硬件推动着操作系统在走的
  5. 虽然操作系统的代码和数据是被所有进程共享的,但是我们也不是随意就能够调用的,用户无法访问操作系统

6.3浅谈寄存器

  1. CPU当中有一个ecs寄存器
  2. ecs有最低的两个比特位排列组合一共是00,01,10,11,0表示内核态,3表示用户态
  3. 所以想访问内核态所对应的代码,就要把ecs寄存器的低两位,由3设为0,进入内核态就允许你访问操作系统的数据,如果是3就不让你访问
  4. 所以CPU提供了一个方法叫做int 80就可以陷入内核态

6.4小结

  1. 在执行指令的时候,由于某种原因需要调用操作系统的代码和数据进入内核态,调用int 80,CPU内的ecs寄存器低两位由3->0
  2. 内核执行完操作系统的代码返回时做检测,遍历pending列表,检查是否有信号,有信号检查是否被block了,直到没被block执行对应的hanler方法,就把信号处理了,SIG_DFL执行信号的默认处理方法,SIG_IGN什么也不做返回,处理之前pending表对应的比特位就需要由1->0
  3. 执行自定义方法,就需要先切换成用户态的身份执行完对应的自定义方法,再调用sigreturn(入栈时添加)再次回到内核,因为当初是直接进的内核不能从用户回去,要从内核回去

从内核态返回到用户态的时候,进行信号的处理

  1. 交界处代表做信号检测
  2. 圆圈处代表状态切换一共有四次
  3. 调用系统调用从用户态陷入到内核态切换一次
  4. 执行自定义捕捉方法从内核态到用户态一次
  5. 执行完方法用户态返回到内核态一次
  6. 最后处理完成由内核态返回到用户态一次

那我们平常没有调用系统调用呢,进程是会被调度的,只要被调度,时间片必然会被消耗,消耗完毕就要将进程从CPU上剥离下来,当二次调度时就要将进程的上下文恢复到CPU上,这肯定是在内核态,然后在CPU上跑的还是用户的代码,就要从内核态返回到用户态

七、补充

7.1sigaction函数

复制代码
#include <signal.h>

int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);

sigaction和signal函数一样都能完成对特定信号的捕捉

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

act:输入型参数,修改信号处理方法

oldact:输出型参数,把老的信号处理方法返回,不想保存则设置为nullptr

这个结构体里被划掉的都是实时信号,我们暂时不用关系

第一个就是对应捕捉信号的处理方法

7.1.1处理信号的时机

我们一直都在阐述pending由1->0是在处理信号之前,现在我们就通过代码来观察这个现象

复制代码
void PrintPending()

{
  sigset_t pending;
  sigpending(&pending);
  for(int i=31;i>=1;i--)
  {
   int n=sigismember(&pending,i);

    if(n>0)
    {
      cout<<"1";
    }
   else
    {
      cout<<"0";
    }
 }
  cout<<endl;
  cout<<endl;
}

void handler(int signo)
{
  cout<<"catch a signo is "<<signo<<endl;
  PrintPending(); 
}

int main()
{
  cout<<"pid is "<<getpid()<<endl;
  struct sigaction act,oact;
  memset(&act,0,sizeof(act));
  memset(&oact,0,sizeof(oact));
  act.sa_handler=handler;
  sigaction(2,&act,&oact);
  while(1)
  {
    cout<<"process is running "<<endl;
    sleep(1);
  }
  return 0;
}

我们在自定义捕捉函数中添加打印pending表

如果在处理之后由1->0,那么在自定义捕捉函数的pending表就应该有1

可是没有,所以处理信号是在执行处理方法之前就已经清0了

7.1.2自动屏蔽

  1. 当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字

  2. 当信号处理函数返回时自动恢复原来的信号屏蔽字

  3. 保证了在处理某个信号时,如果这种信号再次产生,会被阻塞到当前处理结束为止

  4. 信号被处理的时候,对应的信号也会被添加到block表中,防止信号捕捉被嵌套调用

    void handler(int signo)
    {
    cout<<"catch a signo is "<<signo<<endl;
    while(1)
    {
    PrintPending();
    sleep(1);
    }
    }

我们让信号处理一直处于运行状态这样第一次发2号信号后就一直处于处理状态

如果再次发送2号信号处于屏蔽的话,pending表的2号位就会置1

如果不对信号做屏蔽的话,由于自定义处理函数是处于用户态

当再次发送信号后就会执行系统调用接口重返内核态再次进入自定义捕捉函数

所以为了防止自定义函数被无限重复调用,就需要在处理期间将特定信号加入信号屏蔽字

sa_mask:同时还想屏蔽更多信号

复制代码
sigemptyset(&act.sa_mask);

sigaddset(&act.sa_mask, 1);

sigaddset(&act.sa_mask, 3);

sigaddset(&act.sa_mask, 4);

当我们向对应的阻塞信号集添加想要屏蔽的更多信号,就能完成对信号的屏蔽效果

7.2可重入函数

  1. 当我们执行头插操作的时候,当执行到第二步,还没有让头节点和新节点连接的时候,由于进程切换的时候刚好停留在这一步,这时候刚好有信号来
  2. 当我们恢复进程上下文的时候,是从用户态进入到内核态,这时候就要对信号做检测和处理,当我们执行信号的自定义捕捉方法时,这个方法也是头插
  3. 这个函数是sighandler执行流进入的,这个函数就相当于被进入了两次(被重复进入)
  4. 当执行完sighandler执行流的头插操作后返回main执行流的头插,头节点就会指向原来的节点,这时候新的节点没有人指向,就会导致节点丢失,内存泄露

insert函数被main和handler执行流重复进入,导致出错了,这个就叫做不可重入函数,否则叫做可重入函数

目前我们学过的大部分函数都是不可重入的

main函数和sighandler函数这两套函数栈帧没有调用和被调用的关系,是并列的

7.3volatile关键字

volatile关键字:防止编译器过度优化,保持内存的可见性

复制代码
int flag=0;
void handler(int signo)
{
  cout<<"catch a signo is "<<signo<<endl;
  flag=1;
}
int main()
{
  signal(2,handler);   
  while(!flag); //flag为0表示假,为1表示真
  cout<<"process quit..."<<endl;
  return 0;
}

当我们发送信号时就会对flag的值做修改,条件判断不成立,原本不会打印语句的但是经过信号处理之后就会了

但是在优化条件 下,flag变量可能被直接优化到CPU内的寄存器,因为CPU一是检测到flag的值没有做修改,而是flag做的是逻辑运算

编译器在编译的时候有若干编译选项

在编译的时候有这些优化级别

-O0是没有优化的

复制代码
g++ -o $@ $^ -O1 -std=c++11

此时我们再次发送2号信号就不会执行打印的代码了,但是flag执行了变为1 的操作,就应该退出判断然后打印,这是有原因的

  1. 因为CPU识别到在main函数中flag的值并没有做修改,它看不到在handler内部做修改,至少在main执行流内部没有做修改,就不再把变量的内容从内存里获取,而是保存到寄存器中,提高效率

  2. 所以在执行完handler执行流的flag修改以后flag内存当中的值改变了,但是CPU不读,没有用

  3. 因为优化,导致我们内存不可见了

  4. 所以volatile就能防止编译器过度优化,保持内存的可见性

    volatile flag=0;

7.4SIGCHLD信号

子进程退出的时候,不是静悄悄的退出

子进程在退出的时候,会主动向父进程发送17号SIGCHLD信号

复制代码
void handler(int signo)
{
  cout<<"catch a signo "<<signo<<endl;
}
int main()
{
  signal(17,handler);
  pid_t id = fork();
  if(id==0)
  {
    while(1)
    {
      cout<<"i am a child process "<<getpid()<<endl;
      sleep(1);
      break;
    }
    cout<<"child quit "<<endl;
    exit(0);
  }
  while(1)
  {
    cout<<" father running "<<endl;
    sleep(1);
  }
  return 0;
}

子进程休眠1秒以后自动退出

这样我们就验证了子进程退出的时候确实要发送17号信号给父进程

所以我们可以试着把子进程等待写入到信号捕捉函数中

复制代码
void handler(int signo)
{
  sleep(5);
  pid_t rid=waitpid(-1,nullptr,0);
  cout<<"catch a signo "<<signo<<"child pid is"<<rid<<endl;
}
int main()
{
  signal(17,handler);
  pid_t id = fork();
  if(id==0)
  {
    while(1)
    {
      cout<<"i am a child process "<<getpid()<<endl;
      sleep(5);
      break;
    }
    cout<<"child quit "<<endl;
    exit(0);
  }
  while(1)
  {
    cout<<" father running "<<endl;
    sleep(1);
  }
  return 0;
}

父子进程同时运行五秒,子进程僵尸状态5秒以后,父进程回收子进程

如果我们有5个子进程同时退出呢

复制代码
void handler(int signo)
{
  sleep(5);
   pid_t rid;
  while ((rid = waitpid(-1, nullptr, 0))>0)
  {
    cout << "catch a signo " << signo << "child pid is" << rid << endl;
  }
}

5个进程同时退出我们进行阻塞等待,持续循环,当没有进程可以回收时返回-1循环退出

如果我们只退出一半的话,就是比如说1,2号被回收,3号卡在那里,因为时阻塞等待所以不会返回,如果一直卡在这里就会导致主函数代码无法运行,所以我们采用非阻塞等待,等待不到返回0立马退出,等待有子进程发信号的时候再接收信号进入函数

复制代码
void handler(int signo)
{
  sleep(5);
   pid_t rid;
  while ((rid = waitpid(-1, nullptr, WNOHANG))>0)
  {
    cout << "catch a signo " << signo << "child pid is" << rid << endl;
  }
}
int main()
{
  signal(17, handler);
  for (int i = 0; i < 5; i++)
  {
    pid_t id = fork();
    if (id == 0)
    {
      while (1)
      {
        cout << "i am a child process " << getpid() << endl;
        sleep(5);
        break;
      }    
      cout << "child quit " << endl;
     exit(0);
    }
    sleep(3);
  }
  while (1)
  {
    cout << " father running " << endl;
    sleep(1);
  }
  return 0;
}

如果我们不关心子进程的退出码,就不想要回收,能不能让子进程自己释放掉呢

父进程可以调用sigaction/signal将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程,此方法只保证对Linux可用

复制代码
signal(17, SIG_IGN);

这样就能够进行自动回收

但是我们之前写进程等待的时候也没有对17号信号做处理

这个也是IGN啊为什么不会做自动清理呢

官方手册对信号的默认处理动作是SIG_DFL,只不过缺省的action设置为IGN

也就是默认处理动作设置的方法是忽略,一个动作是忽略,两个进入的是不一样的宏

相关推荐
梦飞翔2383 小时前
Linux
linux
大聪明-PLUS4 小时前
Linux IIO研究(二)
linux·嵌入式·arm·smarc
_dindong4 小时前
Linux网络编程:Socket编程预备
linux·运维·网络·学习
deng-c-f4 小时前
Linux C/C++ 学习日记(25):KCP协议:普通模式与极速模式
linux·学习·kcp
Net_Walke4 小时前
【Linux系统】系统编程
linux·运维·服务器
_dindong4 小时前
Linux网络编程:宏观网络体系
linux·网络·笔记·学习
猫林老师4 小时前
OpenHarmony南向开发环境搭建 - 深入理解Ubuntu、DevEco Device Tool与HPM
linux·运维·ubuntu·harmonyos·openharmony
爱装代码的小瓶子5 小时前
Linux下的权限与文件
linux·运维·服务器
ggaofeng6 小时前
linux中mount的本质是什么?自己如何实现一个伪文件系统
linux·mount·自己实现伪文件系统