【Linux】进程信号“疑问?坤叫算信号吗?“(下)

简介: 【Linux】进程信号“疑问?坤叫算信号吗?“(下)

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


#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <string>
#include <sys/types.h>
void Usage(std::string proc)
{
    std::cout<<"Usage: \n\t";
    std::cout<<proc<<"信号编号 目标进程\n"<<std::endl;
}
int main(int argc,char* argv[])
{
    if (argc!=3)
    {
        Usage(argv[0]);
        exit(1);
    }
    return 0;
}


我们要完成的工作是写一个和kill-9命令一样的函数,所以我们在main函数中判断如果用户使用我们的kill命令用的参数不对的话,就给用户发一个使用手册然后退出程序,这个使用手册就是教用户如何使用这个kill函数:


2913e454ccc540cf889801f5d69759bb.png


当我运行程序参数用的不对就会给我发一个使用手册,下一步我们完善代码:


在使用kill接口前我们先看看kill接口需要的参数和返回值:


63583f03b81d4cf080f3742ab90ecfb7.png

8ed357a9cb1a4cb4aa14d2cd56d85042.png


#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <string>
#include <sys/types.h>
#include <cerrno>
#include <cstring>
void Usage(std::string proc)
{
    std::cout<<"Usage: \n\t";
    std::cout<<proc<<"信号编号 目标进程\n"<<std::endl;
}
int main(int argc,char* argv[])
{
    if (argc!=3)
    {
        Usage(argv[0]);
        exit(1);
    }
    int signo = atoi(argv[1]);
    int target_id = atoi(argv[2]);
    int n = kill(target_id,signo);
    if (n!=0)
    {
        std::cerr<<errno<<" : "<<strerror(errno)<<std::endl;
        exit(2);
    }
    return 0;
}


首先我们要用的参数都在参数列表中,所以我们需要拿到用户提供的参数,但是由于参数列表为char*类型,所以我们需要将字符串式的信号转为整数,所以我们用了atoi函数,这个函数可以将字符串转为整数,拿到了信号和进程编号后我们就可以使用kill函数了,由于函数成功后返回0,失败返回-1所以我们用了if条件判断,然后我们再写一个死循环的程序等会让这个程序挂着然后用我们的kill程序杀死这个进程:


20a1e29b72b048e3b357eac25800cf70.png


同时因为要生成两个可执行程序所以我们将makefile修改一下:

308b6aa5cbbf4e9a95949f8f4e2befbc.png


下面我们将程序运行起来:


2a9bb7783aa4404e87467b965add2d6d.png50ec32fc89694eff83f5b1d457ef2b0f.png


程序运行起来后我们可以看到我们写的程序成功杀死了一个进程。下面我们看一下raise函数:


ff3dc4678ba3456aa0e90a4d02ec49f6.png


raise函数是谁调用我我就给谁传几号命令,这里的命令是参数,下面我们演示一下:


fb752496f55e44e1a989120d688bdcd6.png

73c8fea2f8074157a84747300c4d149f.png


我们可以看到raise函数的作用确实是谁调用了我我就给谁发信号。


下面我们再看一下abort函数:

40dcec6d0459442b8ae9b0bdb947ce81.png


abort这个函数的含义是给自己发送指定的结束信号:


bc3a8d3c3d5e43808121ebf67c9617d5.png

e564b6baef7d43288b71a125dd4b4b74.png


我们可以看到本来应该打印的end由于abort被迫停止,所以abort是给自己发送指定的结束信号。


2.由软件条件产生信号


软件条件产生信号其实我们在学管道的时候就学过了,我们在学管道的时候讲过,如果管道的读端关闭了写端一直在写,这个时候操作系统就会给管道发送13号信号关闭管道,这就是由软件条件产生信号。下面我们主要讲解alarm函数,这个函数的意思是:


这个函数的返回值是 0 或者是以前设定的闹钟时间还余下的秒数。打个比方 , 某人要小睡一觉 , 设定闹钟为 30 分钟之后响,20 分钟后被人吵醒了 , 还想多睡一会儿 , 于是重新设定闹钟为 15 分钟之后响 ,“ 以前设定的闹钟时间还余下的时间 ” 就是10 分钟。如果 seconds 值为 0, 表示取消以前设定的闹钟 , 函数的返回值仍然是以前设定的闹钟时间还余下的秒数。

下面我们写代码来使用一下这个函数:

11cc28ee13a74f93a1e7f9f2c70fa4f5.png

下面我们用这个代码测试1秒钟CPU可以记多少数:

c28da9097c9d4c17b8a730e32eb95def.png


当我们计数了7万多后程序被alarm函数叫停,而这个数据是真实的吗?CPU1秒才能记这么多数吗?其实不是,因为我们计算要打印出来包含了IO包含了网络,有很多影响的因素,所以这样的测试并不准确,如果测最准确的呢?如下代码:


aa8fcad3f51c4ed592483c5c252587e9.png


首先我们定义一个全局的count计数器,然后用signal捕捉alarm函数,在这期间计数器会一直加加,当1秒达到后alarm发送命令然后被myhandler方法捕捉打印出信号和计数器的数值:


1ca2b6a1af4e497c896d6f80df2c79b3.png


这次的计数才是正常的五亿多。


3.硬件异常产生信号


不知道大家有没有发现,当我们写代码有对空指针进行解引用,或者数组越界,或者右除0操作时,编译的时候都会提示我们出现错误,那么错误是如何被发现的呢?下面用代码来验证:


首先我们重新创建一个文件,然后写一段简单的代码:


#include <iostream>
using namespace std;
int main()
{
    int a = 10;
    a/=0;    //进行除0操作引发异常
    cout<<"div zero ..... here"<<endl;
    return 0;
}

接下来我们直接编译一下:


d51e12cf03d042af89786297d2b7a5af.png


可以看到在编译的时候直接给我们发出警告了,当然我们还是可以继续编译的:


d590143067d1424886dda9cc239631a6.png


运行后我们发现输出了一行Floating point exception也就是浮点数异常,下面我们讲一下原理:


61d7822d51f445aa9be644a817466a72.png


如上图所示,代码是在内存中的某个位置存储,假设a/=0这个代码存储在内存的某个位置,而在CPU中有各种各样的寄存器,我们的代码在运行的时候会被加载到CPU当中,然后CPU会将刚刚那个代码加载到寄存器里,比如上图:将a加载到一个寄存器,将0加载到另一个寄存器,而在CPU做计算时是有一个状态寄存器的,这个状态寄存器会报错我们本次计算是否会有溢出问题,一旦溢出了,那么状态寄存器中的溢出标志位就被置为1了,只要被置为1就说明计算有问题,CPU就立马告知操作系统,一旦操作系统发现确实状态寄存器中的标志位被置为1了,那么操作系统就会向目标进程发送信号,这个信号就是Floating point exception浮点数异常,我们可以在信号中查看这个这个是几号信号:

9e380826093643178f590fa01344ed99.png

经过查询我们发现8号信号就是浮点数异常,因为信号的后三位字母是刚刚报错信号的每个单词的首元素。当然我们也可以验证一下,直接捕捉信号即可:


当然我们也可以先看看这个信号的作用:


ada5db1b0f1f4b219838e27db77fb081.png


我们以前用的九号信号作用就是终止进程,term就是terminal的缩写终止的意思,core是什么呢我们等会再讲:


#include <iostream>
#include <signal.h>
using namespace std;
void handler(int signo)
{
    cout<<"我们的进程确实收到了"<<signo<<"号信号"<<endl;
}
int main()
{
    signal(8,handler);
    int a = 10;
    a/=0;    //进行除0操作引发异常
    cout<<"div zero ..... here"<<endl;
    return 0;
}


当我们将异常信号捕捉后程序就不会停止了:


e56747be602349b89905e37b24960583.png


运行后一直死循环打印,下面我们在捕捉的时候让这个进程退出:


645e68e958644a3691375c1ca3644420.png

7077ffa9c9d740fe8c8bc87fe594a442.png


运行后不再死循环了并且打印了我们要求的返回值。下面我们再试试其他异常:


9f4424730c1047789a459e216a30a6e7.png109e17b94907448c856449f8fbe21a64.png


我们发现程序正常编译但是同样直接结束了,下面我们讲一下关于地址的问题:


2be18391098f4341abf0c823ba05dc9c.png


首先有一个0000~FFFF的进程地址空间,进程地址空间上的红色方框是指针的虚拟地址,实际上是在最右边的物理内存上开辟空间的,当我们对空指针解引用的时候其实访问的是进程地址空间的0号地址,比如向0号地址写100,要经过页表转化到物理内存中,但是页表实际上是做KV关系的,做转化的动作不是由软件完成的,而是由硬件完成的,这个硬件叫MMU,MMU被称为内存管理单元,所以从虚拟地址转化到物理地址采用软硬件结合的方式(以上方式是正常情况),而我们对空指针进行解引用首先指针与页表没有对应的映射关系,对0号地址是没有写权限的,所以我们对空指针写入是非法的。*p = 100这句代码第一步并不是写入,而是首先进行虚拟到物理地址的转换,在转换的时候要进程地址空间是否和页表有映射关系,如果没有映射则MMU会硬件报错,如果有映射还需要看是否有对应的权限,如果没有权限也会报错,如果MMU报错也就是硬件报错操作系统就会识别到,然后操作系统向当前进程的PCB发送信号,以上就是对空指针解引用的报错原理。


总结



以上就是linux信号产生的所有知识,下一篇我们将详细讲解linux信号是如何保存和处理的。


上面所说的所有信号产生,最终都要有 OS 来进行执行,为什么? OS是进程的管理者。


信号的处理是否是立即处理的? 不是。是在合适的时候。

信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?


需要被记录下来,记录在进程PCB中


一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢? 能知道,因为程序员教了。


目录
相关文章
|
4天前
|
Ubuntu Linux
【Linux】详解信号产生的方式
【Linux】详解信号产生的方式
|
4天前
|
Unix Linux
【Linux】详解信号的分类&&如何自定义信号的作用
【Linux】详解信号的分类&&如何自定义信号的作用
|
4天前
|
消息中间件 算法 Linux
【Linux】详解如何利用共享内存实现进程间通信
【Linux】详解如何利用共享内存实现进程间通信
|
4天前
|
Linux 数据库
linux守护进程介绍 | Linux的热拔插UDEV机制
linux守护进程介绍 | Linux的热拔插UDEV机制
linux守护进程介绍 | Linux的热拔插UDEV机制
|
4天前
|
Unix Linux 调度
linux线程与进程的区别及线程的优势
linux线程与进程的区别及线程的优势
|
4天前
|
Unix Linux C语言
|
4天前
|
Linux 调度 C语言
|
4天前
|
安全 Linux
【Linux】详解用户态和内核态&&内核中信号被处理的时机&&sigaction信号自定义处理方法
【Linux】详解用户态和内核态&&内核中信号被处理的时机&&sigaction信号自定义处理方法
|
4天前
|
存储 Linux C++
【Linux】详解信号的保存&&信号屏蔽字的设置
【Linux】详解信号的保存&&信号屏蔽字的设置