一.引号的产生
1.什么是信号
在生活中,信号的产生是多种多样的,比如红绿灯,闹钟等等,Linux中的信号是什么呢?
1.在Linux中,信号的本质是一种通信方式,由用户或操作系统发送信号,通知进程,某些事情已经发生,同事进程在合适的时间以合适的方式进行处理。信号本身,可能不会被立即处理,也一定要被保存下来。操作系统可以识别这些信号。
2.结合进程,分析信号
a.进程要分析信号,必须要识别信号即,进程要接收并且要具备其如何处理的动作。
b.进程如何识别信号呢?通过程序员设置的方式让进程去识别。
c.信号的产生是随机的,所以进程的处理方式可能不是立即处理,而是将信号存储起来,在合适的场合处理。
d.进程会降临是信号进行存储,方便之后的处理。
e.一般而言,信号的产生相对于进程来说是异步的。
2.信号的产生
a,通过键盘的组合键产生
比如:通过Ctrl+c终止进程,这就是一种通过键盘向进程发送信号,让进程退出了。
处理信号的常见方式:
默认;忽略;自定义一共有这三种处理方式。
如何理解这些组合键是如何变成信号的?
键盘的工作方式是通过中断的方式进行的,OS解释组合键,然后在进程列表中查找,找到前台运行的进程,OS将信号写入当前的进程结构中的位图中。
常见的信号:
前31个信号为普通信号,后31个信号为实时信号。
如何理解信号被进程保存
进程必须具有保存信号的相关数据结构的位图,进程PCB内部保存了信号,信号发送的本质是OS向目标进程发送对行的信号。OS修改进程内部PCB所对应的位图结构,完成发送进程的过程。
键盘产生的信号:
signal函数:signal(int signum, sighandler_t handler);
第一个参数为信号,可以是信号的编号,也可以是信号的名称,第二个参数是一个回调函数,通过回调函数处理信号,可以为默认,忽略,自定义。默认和忽略所对应的参数名称为:SIG_DFL,SIG_IDO。自定义的参数需要自己写回调函数去完成。通过回调的方式,去修改对应的信号捕捉方法。
比如:
static void handle(int signam) { std::cout<<"我是一个进程"<<std::endl; } int main(int argc,const char* argv[]) { signal(2,handle);//信号函数 if(argc!=3) { while(true) sleep(1); } return 0; }
默认处理动作是终止进程并且Core Dump(核心转储)
Core Dump是什么?
当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部 保存到磁盘上,文件名通常是core,这叫做Core Dump。我们可以通过使用gdb调试查看core文件查看进程退出的原因,这也叫事后调试。
Core Dump使用演示:
一个进程允许产生多大的core文件取决于进程的Resource Limit,这个信息报错在PCB中。默认情况下,不允许产生core文件,因为core文件中比较大(容易浪费资源),包含用户密码等敏感信息,不安全。我们可以通过命令ulimit -a 查看系统允许我们产生多大的core文件。可以发现的是,系统是不允许产生这个core文件的,但是我们可以通过命令ulimit -c size 修改,允许产生size大小的core文件。
下面我们写一个程序验证一下:int main(int argc,const char* argv[]) { signal(2,SIG_DFL); if(argc!=3) { while(true) { cout<<"Hello word"<<endl; sleep(1); } } return 0; }
结果:
core dumped标志是否发生核心转储。主要是为了调试。
可以看到core.2549这个文件很大,所以在一般生产环境下都需要关闭核心转储功能。
b.系统调用接口产生信号
用户调用系统接口,执行OS所对应的代码,OS设置参数,OS向目标进程写入,OS修改对应的位图结构,进程后续执行所对应的信号。介绍三个系统调用函数 kill函数 raise函数 abort函数。
kill函数
给任意进程发送任意信号
#include <signal.h>
int kill(pid_t pid, int sig);
参数:pid 进程pid sig 要发送的信号
raise函数
给进程自己发送信号
#include <signal.h> int raise(int sig);
- 1
- 2
参数:
- sig:要发送的信号
返回值: 成功返回0,失败返回-1
和kill比较:raise函数相当于kill(getpid(), sig)
abort函数
功能: 使用当前进程收到信号而异常终止(发送6号信号)
函数原型:#include <stdlib.h> void abort(void);
c.由软件条件产生信号
SIGPIPE是一种由软件条件产生的信号,在“管道”中已经介绍过了。主要介绍alarm函数 和SIGALRM信号。
alarm函数
int main() { //signal(SIGALRM, handler); alarm(5); //int count=0; while(true) { count++; cout<<"count:"<<count<<endl; sleep(1); } }
结果:
所以:
功能: 设定一个闹钟,操作系统会在闹钟到了时送SIGALRM 信号给进程,默认处理动作是终止进程
函数原型:#include <unistd.h>
unsigned alarm(unsigned seconds);
1
2
参数:second:设置时间,单位是s
返回值: 0或者此前设定的闹钟时间还余下的秒数d.硬件异常产生
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。
这里给大家介绍两个硬件异常:CPU产生异常 和 MMU产生异常CPU产生异常 发生除零错误,CPU运行单元会产生异常,内核将这个异常解释为信号,最后OS发送SIGFPE信号给进程。
MMU产生异常: 当进程访问非法地址时,mmu想通过页表映射来将虚拟转换为物理地址,此时发现页表中不存在该虚拟地址,此时会产生异常,然后OS将异常解释为SIGSEGV信号,然后发送给进程。
2.信号的保存
捕捉信号
先思考一个问题:信号是什么时候被处理的
首先,不是立即被处理的。而是在合适的时候,这个合适的时候,具体指的是进程从用户态切换回内核态时进行处理。
- 用户态: 处于⽤户态的 CPU 只能受限的访问内存,用户的代码,并且不允许访问外围设备,权限比较低。
- 内核态: 处于内核态的 CPU 可以访问任意的数据,包括外围设备,⽐如⽹卡、硬盘等,权限比较高。
- 注意: 操作系统中有一个cr寄存器来记录当前进程处于何种状态
进程空间分为用户空间和内核空间。此前我们介绍的页表都是用户级页表,其实还有内核级页表。进程的用户空间是通过用户级页表映射到物理内存上,内核空间是通过内核级页表映射到物理内存上。
进程有不同的用户空间,但是只有一个内核空间,不同进程的用户空间的代码和数据是不一样的,但是内核空间的代码和数据是一样的。
上面这些主要是想说:进程处于用户态访问的是用户空间的代码和数据,进程处于内核态,访问的是内核空间的代码和数据。信号处理函数:
sigaction函数:
功能:可以读取和修改与指定信号相关联的处理动作
函数原型:#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);signum: 要操作的信号
act:一个结构体
sa_handler:SIG_DFT、SIG_IGN和handler(用户自定义处理函数)
sa_sigaction:实时信号处理的函数,我们不关心
sa_mask:一个信号屏蔽字,里面有需要额外屏蔽的的信号
sa_flags:包含一下选项,这里我们给0
sa_restorer:我们这里不使用。结构体如下:
struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); };
返回值: 成功返回0,失败返回-1
3.信号的处理