Linux操作系统 - 信号

Posted TangguTae

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux操作系统 - 信号相关的知识,希望对你有一定的参考价值。

目录

signal函数

信号的产生

信号处理

相关操作函数

信号捕捉


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

首先看看有哪些信号 kill -l

编号为34以后的信号为实时信号。

之前在进程操作的文章中讲过kill -9是用来杀掉一个进程,给指定的进程发送SIGKILL信号,好比告诉进程你该结束了。

还有在进程间通信中,在利用管道进行进程间通信时,当读数据的进程退出后,写数据的进程会收到系统发来的13号信号SIGPIPE。

或者平时在命令行输入ctrl c 其实是前台向正在运行的进程发送了2号信号SIGINT。

其实每个信号都有自己的默认的处理函数,对捕捉到的信号进行执行默认的处理函数,部分信号可以自定义处理函数。


signal函数

第一个参数signum是信号的编号,表明要重新定义几号信号

第二个参数是一个函数指针,返回值是void,参数是int类型。这个函数指针就是指向用户要自定义处理动作的函数。

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;

void handler(int sig) //自定义处理函数                   

  cout<<"catch a signal "<<sig<<endl;


int main()

  signal(2,handler);//自定义捕捉
  while(true)
  
    cout<<"i am running..."<<endl;
    sleep(1);
  
  return 0;

此时在ctrl c发现终止不了程序,原因是我们已经修改了2号信号默认的处理函数。(此时在退出用ctrl \\(这个是发送的3号信号SIGQUIT))

注意:9号信号不能自定义捕捉

所有的信号都必须经过OS发出给各个进程。

信号的产生

1、通过键盘输入

例如ctrl c,ctrl \\

2、通过系统函数

kill函数

kill -  信号名或者信号编号   进程pid

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;

void handler(int sig)

  cout<<"catch a signal "<<sig<<endl; 


int main()

  //把2、3号信号自定义捕捉
  signal(2,handler);
  signal(3,handler);
  while(true)
  
    cout<<"i am running..."<<endl;
    sleep(1);
  
  return 0;

运行程序,先看一下他的pid,

 

此时ctrl c和ctrl \\都不可以结束掉进程。

可以发送9号信号,kill -9 

 

3、通过软中断

SIGPIPE信号(管道相关)

SIGALRM信号

通过alarm函数进行定时,时间到了会给当前进程发送SIGALRM信号,该信号的默认操作是终止当前进程。

4、硬件错误

比如说越界发生的段错误

#include<iostream>                       
#include<signal.h>
#include<unistd.h>
using namespace std;

void handler(int sig)

  cout<<"catch a signal "<<sig<<endl;
  sleep(1);


int main()

  //signal(11,handler);//将11号信号自定义捕捉
  int *p=nullptr;
  *p = 10;
  return 0;


如果自定义捕捉,可以发现是11号信号。

虚拟地址映射真实地址的过程出错,实际上是硬件mmu等报错,由操作系统接收。


信号处理

当信号产生时,并不是立即处理的,会先进入一个状态(未决),等到合适的时机才会进行处理。

信号递达(handler)

信号的处理动作,包括默认自定义捕捉忽略

信号未决(pending)

是一种状态(产生到递达之间的状态)。

信号阻塞(block)

进程可以阻塞一个信号,被阻塞的信号一旦产生,将一直处于未决状态,直到进程解除对此信号的阻塞,才会执行递达。

由于信号是发送给进程的,进程肯定有相关的数据结构

举个例子:

例如1号信号SIGHUP的block =0,pending = 0,说明信号未被阻塞,信号也未产生。

2号信号SIGINT的block = 1,pending = 1,说明信号已经产生,处于未决状态,但是又由于信号被阻塞了,所以在等待解除阻塞。

3号信号SIGQUIT的block = 1,pending = 0,信号未产生,但是可以被阻塞。

相关操作函数

在用户这一端,我们只能去修改block标志位,使得让某些信号进入阻塞状态。

sigprocmask函数

用来修改block的函数

第一个参数how决定要以哪种方式修改block。

假设当前信号屏蔽字为mask 

SIG_BLOCK方式的意思是:mask = mask | set (add set)

SIG_UNBLOCK :mask = mask&~set (remove set)

SIG_SETMASK:mask= set (equal)

第二个参数set,用户传入设置block,具体意思根据第一个参数

第三个参数oldset,记录前一次的屏蔽字

信号集操作函数

sigset_t声明的变量,sigset_t其实是 unsigened long

接下来这一组函数本质上是用来位操作的,设置好block的值之后,传入上面的sigprocmask函数的set。

 

sigpending函数

用来读取当前进程未决信号集。

例子

#include<iostream>                            
#include<signal.h>
#include<unistd.h>
using namespace std;

void show_pending(sigset_t pending)//按位显示pending信号集

  for(int i=1;i < 32;i++)
  
    if(sigismember(&pending,i))
      cout<<"1";
    else 
      cout<<0;
  
  cout<<endl;

void handler(int sig)

  cout<<"catch a signal "<<sig<<endl;


int main()

   //定义信号集
  sigset_t pending;
  sigset_t block,oldblock;
    //初始化
  sigemptyset(&pending);
  sigemptyset(&block);
  sigemptyset(&oldblock);
  //将2号信号阻塞
  sigaddset(&block,2);
  sigprocmask(SIG_BLOCK,&block,&oldblock);

  cout<<"开始阻塞"<<endl;
//打印pending信号集
  for(int i=0;i < 5;i++)
  
    sigpending(&pending);
    show_pending(pending);
    sleep(1);
  
  //自定义捕捉
  signal(2,handler);
  //解除阻塞
  sigprocmask(SIG_UNBLOCK,&block,nullptr);
  cout<<"解除阻塞"<<endl;
  while(1);  
  return 0;
                                          

可以看到当发送2号信号后,2号信号处于pending状态。一旦解除阻塞,就会递达(合适的时机)

相对应的未决信号就会被清空

信号捕捉

合适的时机:从内核态切换到用户态时,才进行相关的检测

捕捉信号:信号处理动作是用户自定义的函数。

如果不是用户自定义的函数,即系统默认的处理函数,只需要一次用户到内核状态切换的过程。

如果是用户自定义的捕捉函数

在自定义捕捉函数时,总共需要四次切换,用户到内核、内核到用户切换各两次。

举个例子: 用户程序注册了SIGQUIT信号的处理函数sighandler。当前正在执行main函数,这时发生中断或异常切换到内核态。在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数, sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行。

问题

为什么要这么复杂而不直接在内核调用信号处理函数?

内核权限更高,调用用户自定义的处理函数有风险,之所以引入内核和用户两种模式,也是为了操作系统的安全。

以上是关于Linux操作系统 - 信号的主要内容,如果未能解决你的问题,请参考以下文章

linux内核—进程调度时机

Linux进程通信 | 信号

Linux中的信号

使用GCD中的dispatch_semaphore(信号量)处理一个界面多个请求(把握AFNet网络请求完成的正确时机)

Linux信号及工作原理

Linux中断的系统调用