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函数:
当我运行程序参数用的不对就会给我发一个使用手册,下一步我们完善代码:
在使用kill接口前我们先看看kill接口需要的参数和返回值:
#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程序杀死这个进程:
同时因为要生成两个可执行程序所以我们将makefile修改一下:
下面我们将程序运行起来:
程序运行起来后我们可以看到我们写的程序成功杀死了一个进程。下面我们看一下raise函数:
raise函数是谁调用我我就给谁传几号命令,这里的命令是参数,下面我们演示一下:
我们可以看到raise函数的作用确实是谁调用了我我就给谁发信号。
下面我们再看一下abort函数:
abort这个函数的含义是给自己发送指定的结束信号:
我们可以看到本来应该打印的end由于abort被迫停止,所以abort是给自己发送指定的结束信号。
2.由软件条件产生信号
软件条件产生信号其实我们在学管道的时候就学过了,我们在学管道的时候讲过,如果管道的读端关闭了写端一直在写,这个时候操作系统就会给管道发送13号信号关闭管道,这就是由软件条件产生信号。下面我们主要讲解alarm函数,这个函数的意思是:
这个函数的返回值是 0 或者是以前设定的闹钟时间还余下的秒数。打个比方 , 某人要小睡一觉 , 设定闹钟为 30 分钟之后响,20 分钟后被人吵醒了 , 还想多睡一会儿 , 于是重新设定闹钟为 15 分钟之后响 ,“ 以前设定的闹钟时间还余下的时间 ” 就是10 分钟。如果 seconds 值为 0, 表示取消以前设定的闹钟 , 函数的返回值仍然是以前设定的闹钟时间还余下的秒数。
下面我们写代码来使用一下这个函数:
下面我们用这个代码测试1秒钟CPU可以记多少数:
当我们计数了7万多后程序被alarm函数叫停,而这个数据是真实的吗?CPU1秒才能记这么多数吗?其实不是,因为我们计算要打印出来包含了IO包含了网络,有很多影响的因素,所以这样的测试并不准确,如果测最准确的呢?如下代码:
首先我们定义一个全局的count计数器,然后用signal捕捉alarm函数,在这期间计数器会一直加加,当1秒达到后alarm发送命令然后被myhandler方法捕捉打印出信号和计数器的数值:
这次的计数才是正常的五亿多。
3.硬件异常产生信号
不知道大家有没有发现,当我们写代码有对空指针进行解引用,或者数组越界,或者右除0操作时,编译的时候都会提示我们出现错误,那么错误是如何被发现的呢?下面用代码来验证:
首先我们重新创建一个文件,然后写一段简单的代码:
#include <iostream> using namespace std; int main() { int a = 10; a/=0; //进行除0操作引发异常 cout<<"div zero ..... here"<<endl; return 0; }
接下来我们直接编译一下:
可以看到在编译的时候直接给我们发出警告了,当然我们还是可以继续编译的:
运行后我们发现输出了一行Floating point exception也就是浮点数异常,下面我们讲一下原理:
如上图所示,代码是在内存中的某个位置存储,假设a/=0这个代码存储在内存的某个位置,而在CPU中有各种各样的寄存器,我们的代码在运行的时候会被加载到CPU当中,然后CPU会将刚刚那个代码加载到寄存器里,比如上图:将a加载到一个寄存器,将0加载到另一个寄存器,而在CPU做计算时是有一个状态寄存器的,这个状态寄存器会报错我们本次计算是否会有溢出问题,一旦溢出了,那么状态寄存器中的溢出标志位就被置为1了,只要被置为1就说明计算有问题,CPU就立马告知操作系统,一旦操作系统发现确实状态寄存器中的标志位被置为1了,那么操作系统就会向目标进程发送信号,这个信号就是Floating point exception浮点数异常,我们可以在信号中查看这个这个是几号信号:
经过查询我们发现8号信号就是浮点数异常,因为信号的后三位字母是刚刚报错信号的每个单词的首元素。当然我们也可以验证一下,直接捕捉信号即可:
当然我们也可以先看看这个信号的作用:
我们以前用的九号信号作用就是终止进程,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; }
当我们将异常信号捕捉后程序就不会停止了:
运行后一直死循环打印,下面我们在捕捉的时候让这个进程退出:
运行后不再死循环了并且打印了我们要求的返回值。下面我们再试试其他异常:
我们发现程序正常编译但是同样直接结束了,下面我们讲一下关于地址的问题:
首先有一个0000~FFFF的进程地址空间,进程地址空间上的红色方框是指针的虚拟地址,实际上是在最右边的物理内存上开辟空间的,当我们对空指针解引用的时候其实访问的是进程地址空间的0号地址,比如向0号地址写100,要经过页表转化到物理内存中,但是页表实际上是做KV关系的,做转化的动作不是由软件完成的,而是由硬件完成的,这个硬件叫MMU,MMU被称为内存管理单元,所以从虚拟地址转化到物理地址采用软硬件结合的方式(以上方式是正常情况),而我们对空指针进行解引用首先指针与页表没有对应的映射关系,对0号地址是没有写权限的,所以我们对空指针写入是非法的。*p = 100这句代码第一步并不是写入,而是首先进行虚拟到物理地址的转换,在转换的时候要进程地址空间是否和页表有映射关系,如果没有映射则MMU会硬件报错,如果有映射还需要看是否有对应的权限,如果没有权限也会报错,如果MMU报错也就是硬件报错操作系统就会识别到,然后操作系统向当前进程的PCB发送信号,以上就是对空指针解引用的报错原理。
总结
以上就是linux信号产生的所有知识,下一篇我们将详细讲解linux信号是如何保存和处理的。
上面所说的所有信号产生,最终都要有 OS 来进行执行,为什么? OS是进程的管理者。
信号的处理是否是立即处理的? 不是。是在合适的时候。
信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?
需要被记录下来,记录在进程PCB中
一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢? 能知道,因为程序员教了。