0


linux信号 | 学习信号三步走 | 全解析信号的产生方式

    **前言:**本节内容是信号, 主要讲解的是信号的产生。信号的产生是我们学习信号的第二个阶段。 我们已经学习过第一个阶段——信号的概念与预备知识(没有学过的友友可以查看我的前一篇文章)。 以及我们还没有学习信号的第三个阶段——信号的保存与处理。 

** ps:本节内容主要为信号的产生, 需要友友们了解信号的概念以及信号的预备知识后再进行学习。**

    信号的产生有很多种, 但是**无论信号如何产生, 最终一定是由 OS 发出的**。 ——为什么? 因为**OS是进程的管理者**。 就比如弟弟惹到了姐姐, 然后姐姐向爸爸告状, 爸爸收到姐姐的告状后就会去找弟弟, 然后教育一顿。 这里面爸爸就是执行者OS, 然后弟弟就是被管理者, 姐姐就是发送的那个信号。  

键盘组合键

    产生信号的第一种方法是键盘组合键, 信号组合健种最常见**ctrl + C**, 我们在运行一个程序,如果**程序陷入死循环, 我们这个时候无论输入任何指令都是没有用的**。 所以**就要使用ctrl + C将程序退出**。 **这里我们点击ctrl + C, 本质其实就是给进程发送了一个信号**。 这个信号是2号信号。

    同样的, 键盘组合键还有另外一种比较常见的组合键就是**ctrl + \**。 同样也是给进程发送信号, 不过信号序号是3号信号。 当进程识别到这些信号, 就会终止, 退出自己。 
    这里有一个函数, 用来捕捉我们的信号, 叫做signal

** 这里面有两个参数, 第一个参数是signum,意思是要捕捉的信号编号。 第二个参数是handler, 意思是捕捉到信号后,重新定义的信号的处理方式。 **

    如下是一个2号信号的捕捉操作:

kill命令

    kill 命令可以用来终止正在运行的进程。kill命令通过发送不同的信号给目标进程, 如我们经常使用的kill -9。 kill命令的使用方法是kill 命令序号 PID。

    这里博主利用信号的捕捉来验证我们的kill命令。(注意, 接下来实验过程中我们可以发现signal并不能捕捉9号和19号命令), 下面是代码:
#include<iostream>
using namespace std;
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<signal.h>

void myhander(int x)
{
  cout << "signal is " << x << endl;
}

int main()
{
  for (int i = 1; i <= 31; i++)
  {
    signal(i, myhander);
  }

  while (true)
  {
    cout << "hello signal , PID : " << getpid() << endl;
    sleep(1);
  }
  return 0;
}

接下来我们就完成编译。 并且打开两个终端, 一个终端用来发送信号即可:

    博主上面对于1 ~ 31个信号没有全部进行测试, 但是友友们可以全部测试一下。 然后就能发现, **除了9号和19号, 其他的信号都可以被捕捉**。 并且也验证了我们的**kill命令可以给进程发送信号**。——那么, 为什么9号和19号信号要暴露出来, 不能被捕捉。 是因为**这两个信号, 一个是杀掉进程, 一个是暂停进程**。 **如果我们今天想写一个恶意病毒, 恶意软件。那么我们把9号、19号信号一捕捉, 操作系统就杀不掉这个病毒了, 那怎么办**? 所以, **必须要将9号和19号暴露出来。 **

系统调用

kill 系统调用

接口

kill系统调用是发送一个信号给进程。这里的第一个参数进程pid, 意思是发给哪一个进程。第二个参数是sig, 表示发送哪一个信号。

kill调用的返回值是如果成功, 零被返回。 如果失败, -1被返回。

应用

这里我们定义一个proc程序, 用来循环打印一条语句:

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

void myhander(int signum)
{
  cout << "signal is : " << signum << endl;
}


int main()
{
  for (int i = 1; i <= 31; i++)
  {
    signal(i, myhander);
  }
  while (true)
  {
    cout << "i am a process,  pid : " << getpid() << endl;
    sleep(1);
  }
  return 0;
}
    这里之所以要signal捕捉信号是因为要检测当前进程是否收到了信号, 正确的信号。  并且这个语句执行后, 会循环打印进程自己当前的pid。 
    然后, 我们创建另一个程序, mykill, 用来封装kill系统调用。
#include<iostream>
using namespace std;
#include<sys/types.h>
#include<signal.h>
#include<unistd.h>
#include<cstring>

int main(int argc, char* argv[])
{
  if (argc == 3)
  {
    kill(stoi(argv[0]), stoi(argv[1]));
  }
  else
  {
    cout << "please set mykill.exe + pid + sig" << endl;
    exit(1);
  }
  return 0;
}

运行结果:

raise——发送一个信号给调用者

接口

    ![](https://i-blog.csdnimg.cn/direct/f5ddd2c89c4c46cca788fa4e18a73295.png)

这里面的参数只有一个sig, 意思是要给当前进程发送哪一个信号。 返回值int, 当成功时返回零, 失败时返回非零。

应用

#include<iostream>
using namespace std;
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<signal.h>

void myhander(int x)
{
  cout << "signal is " << x << endl;
}

int main()
{
  signal(2, myhander);
  int cnt = 5;

  while (true)
  {
    cout << "hello signal , PID : " << getpid() << endl;
    sleep(1);
    cnt--;
    if (cnt == 0) raise(2);
  }
  return 0;
}

运行结果(然后我们运行起来可以发现, 5秒之后, 确实给我们发送了一个2号信号。

abort

接口

这一个调用是引起一个正常的进程进行终止。

应用

运行结果:

我们运行这个程序, 就会发现, 这个进程5秒之后, abort了。 这个abort其实就是6号信号, 所以我们这里可以直接捕捉他。

但是问题是, 我们捕捉了, 但是也退出了!!注意, 这个退出不是信号的问题, 这个是abort的问题。 abort不仅仅捕捉了我们的6号信号, 而且还做了一个工作——必须让我们这个进程终止。 这个abort在底层其实也相当于是有一个kill(getpid(), 6)。 但是还多做了一些工作, 比如exit之类的。

异常

    异常博主认为是最重要的信号的产生方式。 因为我们在运行程序的时候, 遇到的信号, 基本都是由异常引起的。 我们的野指针发送信号, 我们的除零错误要发送信号。 

    我们平时运行程序,退出的方式无非就是三种——代码跑完, 结果正确; 代码跑完,结果不正确; 代码没跑完, 出现异常。 

    这里我们先写一段代码, 看一下异常:

上面的运行结果就能看出发生了异常, 直接报错。 但是问题是, 上面的报错是31个信号的哪一种呢? ——这里其实是八号信号, 如下图:

我们可以使用自定义捕捉这个异常:

    然后运行, 就能看到下图的情况:

    这里我们会发现, 虽然我们知道了这个错误是八号信号。 但是问题是他怎么一直在死循环打印呢? 我们知道, 我们的捕捉函数里面, 没有退出进程, 只有一条语句, 那么也就是说, 我们的程序发生除零错误不会退出, 进程一直再跑, 所以OS一直发送信号, 进程也就一直都在打印信号。 

    我们换成下面的代码:        
void myhander(int signum)
{
  cout << "get a signum : " << signum << endl;
}

int mian()
{
  signal(SIGFPE, myhander);
  cout << "pointer error before " << endl;
  sleep(1);

  int* p = nullptr;
  *p = 100;

  sleep(1);
  cout << "pointer error after " << endl;
  return 0;
}

这个进程我们会发现, 两秒之后也退出了!并且发生了段错误。 段错误在信号中是11信号,SIGSEGV, 如何验证和上面的八号信号的验证方式是一样的。这里直接贴运行结果。 同样会死循环打印语句, 不发生退出。 ——因为11号信号被捕捉。

     上面捕捉信号也是在处理异常信号。 那么问题来了, 处理异常信号, 一定会退出吗? 答案是不一定, 一般情况下, 如果是默认动作, 异常会推出。 但是如果是自定义动作, 也就是信号被捕捉, 这个时候异常不一定会推出。 但是一旦异常推出, 一定是执行了某个信号的处理方法。 

    但是我们虽然可以捕捉信号, 不让它退出,只不过这个意义不大, 因为我们的进程已经发生了错误了。那么我们大概率还是要让这个异常终止。 

    

异常:为什么除零, 或者野指针会给进程发送信号呢?

除零

    我们上面说过,** 进程接收到信号, 一定是OS发出的,** 一旦我们的**计算机里面出现了除零错误**或者**野指针问题**, 操作系统又识别到了这些问题, 那么**操作系统就会给我们的进程发送信号**。 **收到信号后我们的进程对于信号的默认处理动作就是终止自己, 所以它就直接崩溃了**。 但是这里的**问题不是操作系统会检测到除零或者野指针,而是如何检测到除零或者野指针**:

    首先, 我们知道cpu中有着许许多多的寄存器。 其中, EIP、PC指针指向我们的进程的上下文。 

    其中, 状态寄存器有标记位, 是把寄存器按照比特位级别设计的。 状态寄存器里面的位数各自代表什么含义是由芯片制造商定好的。

    状态寄存器里面**有一个标记位**, 叫做**一处标记位**。 当我们**除零的时候, 结果就会非常大**。 **那么就溢出了。 这个标记为就从0变成1了。**

    而且,要知道, 我们cpu中的数据, 其实都是进程的上下文。 虽然我们修改的是cpu中的状态寄存器。 但是进程只影响他自己。 ——什么意思? 就是说,我们的进程是不断切换的, 所以寄存器里面的数据也是不断切换的。 当我们的进程切换的时候, 会把我们自己的上下文带走, 下一个进程将自己的上下文放上去。 所以进程之间是不会有影响的。 所以, 不要认为cpu发生了异常, 就说明操作系统出问题了。 因为我们的用户的各种行为, 都是被进程包裹的, 硬件异常是代表这个进程出现了异常, 并不会波及我们的操作系统。 也就是说, 引起出错的永远是进程, cpu出错, 我们的系统照跑不误。 
    那么问题来了, 我们的cpu这个时候发生溢出了, 我们的操作系统要不要知道呢? 为什么呢? ——**必须要知道!操作系统必须得知道!因为操作系统是硬件的管理者!!!cpu也是硬件!!**操作系统在运行我们的进程的时候, 会有类似检查或者中断的方法, 得知**我们的cpu出现溢出了, 然后操作系统向进程发送信号, 进程收到信号后, 就崩溃了。** ——简而言之就是我们这个**除零问题被转化为了硬件问题**, 表现在硬件上面, 进而被操作系统识别到。 被操作系统识别, 操作系统就能对信息做处理。 操作系统的处理并不影响整个系统的稳定性, 影响的是当前进程, **因为cpu内的问题, 是属于进程的问题**。

野指针

 上面的进程我们的进程在通过页表进行查表的时候, 这个查表不是操作系统直接来查的, 因为查表是很费时间的。 ——这个查表是由一个MMU, 内存的管理单元来完成的。

    在上面的图中, 我们的cpu读到的都是虚拟地址。 从虚拟地址到物理地址, 需要经过MMU的转化, 而野指针就是这个转化失败了——要么没有映射关系, 要么越界了, 越权了。 转化失败, MMU报错, 代表里面的硬件发生了报错, 转化失败后的地址会放到另一个寄存器里面, 这个报错, 也能被cpu识别到。 

    那么, 操作系统是怎么知道, 我们的cpu是溢出了, 还是野指针了呢? ——因为**对应的是cpu内不同类型的寄存器的报错!!! **

    那么既然报错后, 操作系统会发送信号让进程崩溃, 但是如果我们将信号捕捉, 不让进程崩溃呢? ——那么就意味着我们的进程在异常的时候也要一直被调度运行!!!所以为什么我们上面会看到死循环呢? 就是因为我们的进程发生了问题, 但是我们没有修正这个问题, 硬件问题一直存在, 随着我们的调度, 那么我们的上下文的错误就一直存在。 所以操作系统就一直检测到这个错误, 它就会一直发信号, 我们就能一直捕捉这个信号。 !!!

    对于异常的进程来讲, 即便向后运行, 结果可能也是错误的, 所以进程发生异常, 就应该是终止掉, 所以大部分信号的默认动作都是终止掉进程。 ——但是**为什么会有捕捉信号呢?** 因为**捕捉信号, 并不是用来解决问题, 而是用来告诉用户, 你是为什么挂掉的!!**
    那么, 为什么操作系统这么温和? **当硬件报错的时候, 不直接将进程的pcb, 页表, 地址空间全部干掉呢?** ——这是因为**进程里面有可能保存着许多的重要的数据, 如果直接将这些东西干掉,这些数据会丢失, 所以要设置信号, 告诉上层哪里出错了, 为什么出错了, 然后让上层自己想办法解决。**

软件条件

alarm

    上面我们讲述的异常也可以成为软件异常。现在有另外一种, 叫做软件条件。 ——这个软件条件叫做闹钟。 

    alarm是在我们的系统里面设置一个闹钟, 一旦闹钟响了, 那么就会给我们的进程传送一个信号。 这个参数就是多少秒之后会给我们发送这个信号。下面是这个接口的返回值:

    这个返回值是提前醒来, 距离闹钟响之间剩余的时间。  

下面我们写这么一串代码:

然后运行它

    我们会发现, 虽然闹钟到达事件后会发送信号, 但是仅仅只是发送一次!这是因为这个闹钟不是异常, 它只响一次。 那么问题来了, 当我们重复设置闹钟的时候, 加入设定了一个5秒的闹钟, 但是过了三秒又设置了一个闹钟, 这个时候会发生什么情况呢? ——答案是第一个闹钟剩余的时间会返回。 

    为了验证, 我们可以使用下面这串代码进行测试。

按照上面的结论, 我们猜测最终打印出来的是6:

    我们的闹钟是如何确定时间的呢? ——使用时间戳和参数, 我们的进程, 我们知道进程可以直接获取时间, 我们的闹钟里面一定有一个时间戳的成员变量的。 未来我们进程获取时间戳变量, 填到闹钟里, 闹钟再根据参数, 两者一加, 就是未来时间了!!!

    那么知道了未来时间, 我们操作系统里面又维护着当前时间, 当这个时间 >= 未来时间的时候, 就能知道闹钟到没到时间了。 

** 那么我们未来系统中一定存在着大量的闹钟, 我们如果想要确定哪个闹钟响过了, 就把他去掉。 可以使用什么数据结构呢? ——堆**

dump

    我们在进程控制的时候, 曾经说过wait的返回值status, 这个**返回值当中的前八个比特位是代表的退出码**, **第0 ~ 7是代表的终止信号**, **第8个比特位我们说过叫做core dump标志**。 

    这个core dump是什么当时我们并没有说过, 这里可以说了。 我们看下面的图:里面有term, 有core。 还有其他的动作, 但是其他的动作我们不考虑, 我们只看**core和term**。 这个dump比特位, 就是代表的是core或者term。**为零是term, 为1是core**。 

    现在我们来重新写一下代码, 看一下我们的dump比特位:

现在我们来看一下:

对于2号信号:

对于8号信号:

    **两次都是dump0**, 但是我们的**8是core**, **2是term**。 这是为什么呢? 是什么呢 ? 怎么办? ——这是因为**云服务器上面的core默认是关闭的**。
    我们可以看一下**ulimit -a**, 这个**是查看系统当中的标准的配置**。 

这里面有一个core file size的选项, -c就是用来查看。 当前它是设为0的。 我们可以使用 -c选项后边加一个数字, 可以用来设置core。

    这一次, 我们再来使用8号信号。 就能看到结果变了。  

** 更重要的是, 这里生成了一个core.4017, 也就是刚刚进程的PID——打开系统的core dump功能, 一旦进程出现异常, 我们的OS会将进程在内存中的运行信息, 给我们dump(转储)到进程的当前目录(磁盘)形成core.pid文件——这个就叫做核心转储(core dump)**

    上面的core.pid有什么用? ——我们的程序在运行时发生错误, 我们最想要知道的是什么呢? ——是不是定位错误的位置, 也就是在哪一行出错了。 我们可以使用core dump数据, 来定位原始代码当中, 在运行过程中哪方面出错了。

    我们使用gdb调试左边的代码, 然后进入core.pid文件里面, 就能看到错误信息了。 

** core文件能够让我们复现问题之后, 直接定位到出错行。 就是先运行, 再core-file。 ——这个叫做事后调试。 而我们边跑边调叫做事中调试。**

    现在的问题是, 为什么云服务器是要把它进行关闭的。 我们知道core文件比较大。 在当代服务器上面, 如果一个服务器挂掉了, 是要让运维重新起来的。 可是大公司的后端服务器集群非常多。 如果运维手动去起, 那么太慢了。 所以大公司要做很多很多自动化运维的手段, 比如说我们的服务器挂掉了, 那么我们要自动的检测服务器出问题了, 第二点就是先将服务器启动起来, 最后才是根据日志排查问题。 

    所以,**一旦服务器挂掉了,****在很多系统当中会自动重启**。 可是, 问题是一般人一年才挂一次, 半年挂一次, 结果呢你写的服务只要跑起来就挂。 虽然在大部分公司, 这样的程序员根本不会有, 但是可能会出现这样的问题。 要知道, 我们的**计算机的速度是很快的**, 我们的**计算器一晚上的重启, 如果每一次重启都会生成一个core-pid文件**, **那么就会导致本来是云服务器上面的一个服务挂掉了, 结果后来变成了磁盘空间被core-pid文件打满了。****那么就可能连操作系统都挂掉了。** 所以, core dump的功能在线上一般都要禁掉, 保证我们的系统重启功能一直都要有效。 不要让core dump冲击磁盘, 影响服务。 

——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!

标签: linux 服务器 后端

本文转载自: https://blog.csdn.net/strive_mianyang/article/details/142499822
版权归原作者 打鱼又晒网 所有, 如有侵权,请联系我们删除。

“linux信号 | 学习信号三步走 | 全解析信号的产生方式”的评论:

还没有评论