Linux信号

文章目录

  • 什么是信号
  • 信号产生
    • 通过键盘产生信号
    • 调用系统函数向进程发送信号
    • 软件条件产生信号
    • 由硬件产生的信号
    • core dump核心转储
    • 信号识别
      • 信号的屏蔽
      • 内核态 && 用户态
      • 信号执行的过程
  • 信号处理
    • 默认处理方式
    • 忽略信号
    • 自定义方式
    • 信号处理函数
  • volatile关键字
  • SIGCHLD

什么是信号

从生活角度来说: 信号是一种条件反射,不管事件有没有发生,但是你对带这件事情的处理方式是固定的。这件事情的发生对你来说就是一种信号。

操作系统也存在信号,实际上os中的信号:是操作系统向进程传达指令的一种操作。操作系统向进程发出信号,进程接受到信号执行相应的动作。

输入指令kill -l 就可以查看所有的信号(注意这里面只有62个信号,分为前31个普通信号和后31个实时信号),如果想要杀死一个特定的进程只需要:kill -signum 进程pid signum 为信号编号
Linux信号_第1张图片
SIGSTOP/SIGKILL信号无法被阻塞,无法被自定义,无法被忽

这里每个信号都有一个字字母标识该信号

信号从操作系统发出到信号被执行一共要经理三个过程:
Linux信号_第2张图片

下面就让我们从这三个方面来对信号深入了解

信号产生

通过键盘产生信号

首先举一个最常用的例子,在使用shell的时候如果一个前台进程卡住了,我们只要在键盘上按下ctrl C就可以终止这个进程(注意这里一定要是前台进程),这里就是信号的一种体现,查阅资料之后发现ctrl C实际上向操作系统发送的是2号信号,如何证明?

在所有的介绍之前首先要先学习一下关于信号的一个最基本的函数:

#include 
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

这个函数的功能是对进程收到特定信号之后的行为进行自定义处理,自定义处理的方式是sighandler_t函数定义,这个函数唯一的参数就是自定义行为信号的编号
参数

  • signum:信号的编号,也就是我们在kill -l指令里面看到的信号的编号
  • sighandler_t handler:自定义信号处理方式的函数的指针 该函数的类型是void (*sighandler_t)(int); ,该函数的参数为自定义信号的编号

我们在进程中将2号信号的处理自定义,运行起来并向该进程发送二号信号(按下ctrl C)

#include
#include
#include
#include

void sighandler(int signo)
{
  printf("收到%d号信号\n",signo);
}
int main()
{
  signal(2,sighandler);
  sleep(100);
  return 0;
}

在这里插入图片描述

  • 我们执行进程之后直接按下ctrl C,得到如上结果
  • 我们执行进程之后,在另一个终端对该进程执行指令kill -2 进程pid ,也得到了如上结果

所以这里就证明了,我们在键盘上按下的ctrl C实际上是操作系统向进程发送了2号信号

调用系统函数向进程发送信号

前面说过可以通过指令将特定进程发送信号,如kill命令
这里实际上还有一些系统调用函数:

kill函数

 #include 
 #include 
 int kill(pid_t pid, int sig);

参数:向指定进程pid发送指定信号sig

返回值:

  • 执行成功返回0
  • 执行失败返回1

raise函数

#include 
int raise(int sig);

向自己的进程发送sig信号,相当于kill(geypid(),sig)

abort函数

#include 
 void abort(void);

实际上这个函数是向自己发送6号SIGABRT信号,如果执行现象如下:

在这里插入图片描述

软件条件产生信号

  • SIGPIPE信号是由管道读写时,如果在管道IO时写端关闭,读端不变,这时就会触发系统向进程发送SIGPIPE信号

  • alarm函数

    #include 
    unsigned int alarm(unsigned int seconds);
    

    参数:为seconds秒之后向当前进程发送SIGALARM信号(14号信号),当信号触发之后的结果为:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UmH6AQ1a-1647596679084)(C:\Users\石海涛\AppData\Roaming\Typora\typora-user-images\image-20220317193708653.png)]

由硬件产生的信号

  • 非法访问内存产生的信号

    非法访问内存之后,系统中的mmu会产生异常,并把这个情况返回给操作系统,操作系统就会向进程发送11号信号:SIGSEGV

    int main()
    {
      int *p;
      *p=1;
      return 0;
    }
    

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kHJ3KGew-1647596679084)(C:\Users\石海涛\AppData\Roaming\Typora\typora-user-images\image-20220317194615045.png)]

  • CPU非法运算产生的信号

    例如我们进行一个除0操作,这时候负责运算的CPU最先检测到信号,将其报告给操作系统,最后由操作系统向进程发送信号

    int main()
    {
      int a=1/0;
      return 0;
    }
    

    运行结果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IuS33qMe-1647596679085)(C:\Users\石海涛\AppData\Roaming\Typora\typora-user-images\image-20220317194113627.png)]

  • 硬件产生的信号的特殊之处
    我们来看下面一段代码:

    #include 
     #include 
     #include 
     using namespace std;
    
     void sighandler(int signum)
     {
     	cout << "接收到" << signum << "号信号" << endl;
     	sleep(1);
     }
    
     int main()
     {
     	signal(8, sighandler);
     	cout << "running" << endl;
     	sleep(2);
     	int p = 1 / 0; // 触发硬件信号
     	return 0;
     }
    

    结果如下:
    Linux信号_第3张图片
    出现了一个很奇怪的现象,为什么os一直向进程发送八号信号?这是我们自定义函数收到信号之后并没有退出进程,CPU的中当前进程的上下文会被保存起来,在此调用该进程的时候,CPU检测硬件标志位,发现异常,继续通过OS向进程发送信号。
    所以 由硬件产生的信号如果不退出进程的话,会一直向进程发送信号

core dump核心转储

有些人在运行上述所有信号造成的进程终止时候有可能会出现如下结果(这咯使用的是除0的代码):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BqiTDPjJ-1647596679085)(C:\Users\石海涛\AppData\Roaming\Typora\typora-user-images\image-20220317195715884.png)]

在原有的结果中我们发现了多出了一个core dump。

core dump是一个进程异常终止之时,将进程的所有用户空间内存数据全部保存到磁盘上,文件名通常是core,用来给用户进程调试使用的。

例如上面异常崩溃之后就会发现多处了一个core 文件

Linux信号_第4张图片

如何打开或关闭core dump

使用指令ulimit -a查看资源

Linux信号_第5张图片

这里我们已经将core file size设置成了1024,如果你没有这个值为0,程序异常就不会出现core dump选项

如何使用core dump

例如上面报错了之后,会生成一个core文件,我们用gdb对崩溃的程序开始调试:

gdb 程序名

然后执行指令core-file 生成的core文件名

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pIdJ2uh6-1647596679087)(C:\Users\石海涛\AppData\Roaming\Typora\typora-user-images\image-20220317202947307.png)]

这时候就会显示文件究竟在哪崩溃的,这里我们没有安装相应的库,所以看不到

信号识别

首先我们需要搞明白一点:信号是一收到就会执行信号吗?

实际上是不是的,信号收到之后不会立即执行,而是等待进程陷入内核态,再从内核态返回用户态的时候才会检查是否收到信号,这期间信号是不会被执行,于是这里就产生了两个问题:

  1. 信号收到到执行这段期间存在哪里?
  2. 什么是内核态,什么是用户态?

信号的屏蔽

在此之前补充一些概念:

  • 信号递达:实际执行信号的处理动作
  • 信号未决(pending):信号从产生到递达之间的状态
  • 信号阻塞(block):将信号一直处于未决状态就叫做阻塞

信号在未决期间的状态是由三张位图(存在进程的PCB中)决定的

  • pending位图:比特位的位置代表信号的编号,比特位的内容(0 or 1)代表是否接受到信号。OS发送信号实际上就是修改进程PCB中的pending位图
  • block位图:比特位的位置代表信号的编号,比特位的内容(0 or 1)代表是否阻塞信号
  • handler位图:用信号的编号作为数组的索引,找到该信号对应的信号的处理方式,然后指向对应的方法
    ,从实现的角度来说handler是一个函数指针数组

Linux信号_第6张图片
上面信号2和信号3 ,都被阻塞了,不管进程有没有收到信号2或3,都不会执行对应的操作。所以阻塞应该理解成一种状态。

信号集sigset_t
有时候我们想认为的控制一个进程的pending位图或者block位图,所以这时候我们就要提供一个接口将两个位图暴露出来,这时候就定义了一种类型sigset_t——信号集,来标识这两个位图。用户可以通过修改这个数据类型的值来改变进程的两个位图
阻塞信号集又称为当前进程的信号屏蔽字

信号集操作函数

#include 
int sigemptyset(sigset_t *set);    //将信号集里面所有位 置0
int sigfillset(sigset_t *set);     //将信号集里面所有位 置1
int sigaddset(sigset_t *set, int signum);    //将指定信号signum的位 置成1
int sigdelset(sigset_t *set, int signum);    //将指定信号signum的位 置成0
int sigismember(const sigset_t *set, int signum);   //判断signum位是否被设置

获取block位图

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

参数

  • how
    Linux信号_第7张图片

  • set:要设置的信号集

  • oldset:将原来的信号集返回出来

获取pending位图

#include 
int sigpending(sigset_t *set);

参数
将pending位图通过set参数传出来

例如:我们先将2号信号block掉,然后发送二号信号打印pending位图,八秒之后解除对2号信号的阻塞,这时2号信号被抵达,执行我们自定义的信号操作:

#include
#include
#include
#include
using namespace std;
void sighandler(int signo)
{
  cout<<"收到"<<signo<<"号信号"<<endl;
}
void func()
{
  sigset_t m;
  sigpending(&m);

  for(int i=1;i<=32;i++)
  {
    if(sigismember(&m,i))
      cout<<"1";
    else 
      cout<<"0";
  }
  cout<<endl;
}
int main()
{
  sigset_t sig;
  sigemptyset(&sig);
  sigaddset(&sig,2);
  sigset_t old_sig;
  sigprocmask(SIG_BLOCK,&sig,&old_sig);   //将2号信号屏蔽

  int count=8;

  while(count--)
  {
    func();
    sleep(1);
  }
  signal(2,sighandler);
  cout<<"解除信号屏蔽"<<endl;
  sigprocmask(SIG_SETMASK,&old_sig,NULL);
  return 0;
}

内核态 && 用户态

Linux信号_第8张图片
前面学进程地址空间的时候低3G的地址空间为用户空间,而高1G的空间是内核空间。

  • 什么是内核态,什么是用户态?
    在计算机的体系结构中,一个程序不仅只是计算,还要和硬件设施进行交互,例如:我们的进程需要开辟一块空间需要和内存交互、进程需要从文件中读取信息也需要和磁盘交互…。这些直接与硬件交互的动作(系统调用)实际上是由操作系统代替进程执行的,由于要确保硬件的安全性,这些动作都被设置了很高的权限(防止恶意进程破坏硬件),所以进程遇到这些硬件方面的需求就会向操作系统请求代替自己执行,操作系统执行指令的过程就叫做进程陷入了内核态。如果只是执行普通的代码,那么就叫做用户态。

  • 用户级页表&&内核级页表
    用户级页表是每个进程所私有的,存储在进程的PCB中的上下文中。而内核级页表是所有进程所共享的,也就是说所有进程共用一个内核级页表!!!

  • 操作系统是如何区分用户态和内核态
    CPU的寄存器储存着进程的状态信息

  • 内核态和用户态最大的区别是什么?
    权限,内核态拥有更高的权限,能看到和操作的资源比用户态要多得多

所以整个过程应该是:

Linux信号_第9张图片

信号执行的过程

前面我们了解到信号是在从内核态切换成用户态的时候递达的,接下来看一下信号被递达的整体你过程

Linux信号_第10张图片

但是如果信号的递达方式是用户自定义的话,整个过程就会大有所不同:
Linux信号_第11张图片

信号处理

信号处理分为三种处理方式:1.默认 2.忽略信号 3.自定义方式

默认处理方式

这个很好理解,就是一个进程受到信号按照该信号的处理方式处理,绝大部分信号的处理方式都是终止进程

忽略信号

这个更好理解了,就是收到信号之后,啥也不干就把信号给忽略了

自定义方式

进程收到信号之后执行用户自己定义的信号处理方式,也就是我们提到的函数typedef void (*sighandler_t)(int);,在这个函数里面用户可以修改进程收到信号之后的操作,我们把这种情况叫做信号的捕捉

信号处理函数

这里我们就要介绍一下一个比较重要的函数:

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

参数:

  • signum:信号的编号

  • 这是一个sigaction结构体,我们这里传入的是我们需要修改的结构体指针,里面包含如下内容:

    The sigaction structure is defined as something like:
               struct sigaction {
                   void     (*sa_handler)(int); //信号自定义函数  
                   void     (*sa_sigaction)(int, siginfo_t *, void *);  //信号自定义函数
                   sigset_t   sa_mask;  //信号屏蔽字
                   int        sa_flags;
                   void     (*sa_restorer)(void);
               };
    
    
    1. void (*sa_handler)(int); 这个就是信号自定义函数,与signal中的一样,是信号自定义处理方法的函数指针。

    2. void (*sa_sigaction)(int, siginfo_t *, void *); 如果struct sigaciton结构体中的sa_flag被定义成SA_SIGINFO,这时候自定义函数就会调用这个,其他情况调用上面的。参数方面:第一个参数为信号的编号,第三个参数一般不会使用,第二个参数是一个结构体指针siginfo_t该结构体包含如下信息:

        siginfo_t {
                     int      si_signo;    /* Signal number */
                     int      si_errno;    /* An errno value */
                     int      si_code;     /* Signal code */
                     int      si_trapno;   /* Trap number that caused
                                              hardware-generated signal
                                              (unused on most architectures) */
                     pid_t    si_pid;      /* Sending process ID */
                     uid_t    si_uid;      /* Real user ID of sending process */
                     int      si_status;   /* Exit value or signal */
                     clock_t  si_utime;    /* User time consumed */
                     clock_t  si_stime;    /* System time consumed */
                     sigval_t si_value;    /* Signal value */
                     int      si_int;      /* POSIX.1b signal */
                     void    *si_ptr;      /* POSIX.1b signal */
                     int      si_overrun;  /* Timer overrun count; POSIX.1b timers */
                     int      si_timerid;  /* Timer ID; POSIX.1b timers */
                     void    *si_addr;     /* Memory location which caused fault */
                     long     si_band;     /* Band event (was int in
                                              glibc 2.3.2 and earlier) */
                     int      si_fd;       /* File descriptor */
                     short    si_addr_lsb; /* Least significant bit of address
                                              (since Linux 2.6.32) */
                 }
      
    3. sigset_t sa_mask;这里定义的是信号屏蔽字,例如我们在执行信号的自定义操作时,这时候又来了一些信号,这时我们可以用信号屏蔽字将特定信号给屏蔽了

  • 信号在递达之后会将pending位图的对应位置0,并且在执行自定义处理方法的时候系统会自动屏蔽当前信号。同时我们还可以是用sigaction函数中的sa_mask来屏蔽其他信号的递达

  • 这里这个结构体和上面的一样,这里不过是一个输出型参数,将修改前的sigaction结构体传出来

volatile关键字

学完了信号之后,让我们用一个例子重新认识一下这个关键字:

我们设置一个全局变量,并修改了二号信号的执行操作——将全局变量的值修改,理论上发送信号2之后,循环就会推出

#include
#include
using namespace std;
int flag=1;
void sighandler(int sig)
{
  flag=0;
  return ;
}
int main()
{
  signal(2,sighandler);
  while(flag);
  cout<<"process quit"<<endl;
}
g++ test.cpp -O3    ##这里-O3是编译优化选项

执行程序你会发现即使你发送了2号信号,循环还在继续。

在这里插入图片描述
原因就在于这里的优化选项,将flag放到了寄存器中,这样运算效率更快,但是信号修改时内存里面flag的值被修改,但是CPU还是拿寄存器中的值进行比较,所以导致了这个情况。

这时对flag变量加上volatile 关键字就可以避免这种错误,volatile关键字就是优化器在用到这个变量时必须每次都小心地重新读取这个变量的值(From Memory),而不是使用保存在寄存器里的备份。

加上violate运行程序结果:

在这里插入图片描述

SIGCHLD

在进程等待那篇博客,我详细描述了父进程等待子进程的函数wait waitpid函数,其实在进程等待也是通过信号传递信息的,在子进程终止的时会给父进程发送17号信号SIGCHLD信号,该信号的默认处理动作是忽略,父进程也可以自定义该信号的处理方式

如下代码自定义SIFCHLD信号的默认处理动作来回收信号

#include
#include
#include
#include
#include
#include
#include
using namespace std;
void sighandler(int signum)
{
  pid_t id;
  while((id=waitpid(-1,NULL,WNOHANG))>0)
  {
    printf("child is quit! %d\n",getpid());
  }
  printf("child quit\n");
}
int  main()
{
  int ret= fork();
  signal(17,sighandler);
  if(ret==0)
  {
    cout<<"child process start "<<getpid()<<endl;
    sleep(4);
    exit(10);
  }
  else 
  {
    while(1)
    {
      cout<<"father is do sth"<<endl;
      sleep(1);
    }
  }
}

你可能感兴趣的:(Linux学习,信号)