零、前言
本章主要讲解学习Linux中的信号,从信号的产生到识别,再到处理的各个时期的详细学习
一、信号入门
1、生活角度的信号
- 示例:
- 你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”
- 当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”
- 在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取”当你时间合适,顺利拿到快递之后,就要开始处理快递了
- 而处理快递一般方式有三种:1. 执行默认动作(幸福的打开快递,使用商品)2. 执行自定义动作(快递是零食,你要送给你你的女朋友)3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话
2、技术应用角度的信号
- 示例:
用户输入命令,在Shell下启动一个前台进程;用户按下Ctrl-C,这个键盘输入产生一个硬件中断,被OS获取解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出
- 示图:
- 注意:
- Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程
- Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号
- 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步的
3、信号及其处理概念
- 信号的基本概念:
- 信号是进程之间事件异步通知的一种方式,属于软中断
- 用kill -l命令可以察看系统定义的信号列表
3、每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到
4、编号1-31的信号是普通信号,在合适的时候进行处理,而编号34-64的信号是实时信号,需要进行立即处理
5、这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal
- 信号处理常见方式:
- 忽略此信号
- 执行该信号的默认处理动作
- 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号
二、信号产生
1、终端按键产生
SIGINT(ctrl+c)的默认处理动作是终止进程,SIGQUIT(ctrl+\)的默认处理动作是终止进程并且Core Dump,这个键盘输入产生一个硬件中断,被OS获取解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出
- Core Dump的概念:
- 当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core+进程id,这叫做Core Dump
- 进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做事后调试
- 一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中),默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息不安全,且产生的core文件内容比较大
注:在开发调试阶段可以用ulimit -c 1024命令限制,允许产生core文件(允许core文件最大为1024K)
- 示例:
#include <iostream> #include <unistd.h> #include <sys/types.h> using namespace std; int main() { while(1) { cout<<"getpid:"<<getpid()<<endl; sleep(1); } return 0; }
- 示图:
注:使用gdb对当前可执行程序进行调试,然后直接使用
core-file core文件
命令加载core文件,即可判断出该程序在终止时的信号,并且定位错误代码
- Core dump标志位:
waitpid函数的第二个参数status是一个输出型参数,用于获取子进程的退出状态。status是一个整型变量,但status不能简单的当作整型来看待,status的不同比特位所代表的信息不同
- 示图:
- 注意:
- 若进程是正常终止的,那么status的次低8位就表示进程的退出码
- 若进程是被信号所杀,那么status的低7位表示终止信号,而第8位比特位是core dump标志,即进程终止时是否进行了核心转储
2、kill命令发信号
首先在后台执行死循环程序,然后用kill命令给它发信号
- 示例:
#include <iostream> #include <unistd.h> #include <sys/types.h> #include <wait.h> using namespace std; int main() { if(fork()==0) { //child while(1) { cout<<"I am child getpid:"<<getpid()<<"ppid:"<<getppid()<<endl; sleep(1); } exit(0); } //father cout<<"I am father getpid:"<<getpid()<<"ppid:"<<getppid()<<endl; int status=0; int ret=waitpid(-1,&status,0); if(ret>0&&WIFEXITED(status)) { cout<<"wait success exit code:"<<WEXITSTATUS(status)<<endl; } else if(ret>0) { cout<<"exit signal:"<<(status&0x7F)<<" core dump:"<<((status>>7)&1)<<endl; } return 0; }
- 结果:
- 注意:
- 指定发送某种信号的kill命令可以有多种写法,上面的命令还可以写成 kill -SIGFPE 18425 或 kill -8 4568 , 8是信号SIGFPE的编号
- kill命令是调用kill函数实现的,kill函数可以给一个指定的进程发送指定的信号;raise函数可以给当前进程发送指定的信号(自己给自己发信号) ;abort函数使当前进程接收到信号而异常终止
- 函数原型:
#include <signal.h> int kill(pid_t pid, int signo); //第一个参数为对应进程的id,第二个参数为想要发送的信号编号 int raise(int signo); //这两个函数都是成功返回0,错误返回-1 #include <stdlib.h> void abort(void); //就像exit函数一样,abort函数总是会成功的,所以没有返回值
3、软件条件产生信号
- SIGPIPE信号:
SIGPIPE信号实际上就是一种由软件条件产生的信号,当进程在使用管道进行通信时,读端进程将读端关闭,而写端进程还在一直向管道写入数据,那么此时写端进程就会收到SIGPIPE信号进而被操作系统终止
- 示例:
#include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> int main() { int fd[2] = { 0 }; if (pipe(fd) < 0){ //使用pipe创建匿名管道 perror("pipe"); return 1; } pid_t id = fork(); //使用fork创建子进程 if (id == 0){ //child close(fd[0]); //子进程关闭读端 //子进程向管道写入数据 const char* msg = "hello father, I am child..."; int count = 10; while (count--){ write(fd[1], msg, strlen(msg)); sleep(1); } close(fd[1]); //子进程写入完毕,关闭文件 exit(0); } //father close(fd[1]); //父进程关闭写端 char buffer[128]={0}; ssize_t s=read(pipe_id[0],buffer,sizeof(buffer)-1);//给结束符留一个位置 if(s>0) { buffer[s]=0;//设置结束符 printf("msg from child:%s",buffer); } else if(s==0) { printf("子进程写端关闭...\n"); } close(fd[0]); //父进程读一次直接关闭读端(导致子进程被操作系统杀掉) int status = 0; waitpid(id, &status, 0); printf("child get signal:%d\n", status & 0x7F); //打印子进程收到的信号 return 0; }
- 结果:
- alarm函数和SIGALRM信号:
alarm函数原型:
#include <unistd.h> unsigned int alarm(unsigned int seconds); //调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程
- 解释:
- 功能:让操作系统在seconds秒之后给当前进程发送SIGALRM信号,SIGALRM信号的默认处理动作是终止进程
- 返回值:若调用alarm函数前,进程已经设置了闹钟,则返回上一个闹钟时间的剩余时间,并且本次闹钟的设置会覆盖上一次闹钟的设置;如果调用alarm函数前,进程没有设置闹钟,则返回值为0
示例:某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数
- 示例:
#include <iostream> #include <unistd.h> #include <signal.h> using namespace std; int cnt=0; void handler(int signo) { cout<<"get a signal:"<<signo<<" cnt:"<<cnt<<endl; exit(0); } int main() { //对信号SIGALRM进行捕获 signal(SIGALRM,handler); alarm(1);//1秒后唤醒 while(1) { cnt++; } return 0; }
- 效果:
注:这个程序的作用是1秒钟之内不停地数数,1秒钟到了就被SIGALRM信号终止
4、硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号
示例:当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程;当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程
- 示例:子进程野指针错误
#include <iostream> #include <unistd.h> #include <sys/types.h> #include <wait.h> using namespace std; int main() { if(fork()==0) { //child int cnt=0; while(cnt<5) { cout<<"I am child getpid:"<<getpid()<<"ppid:"<<getppid()<<endl; sleep(1); cnt++; } int* p=NULL; *p=100; sleep(1); exit(0); } //father cout<<"I am father getpid:"<<getpid()<<"ppid:"<<getppid()<<endl; int status=0; int ret=waitpid(-1,&status,0); if(ret>0&&WIFEXITED(status)) { cout<<"wait success exit code:"<<WEXITSTATUS(status)<<endl; } else if(ret>0) { cout<<"exit signal:"<<(status&0x7F)<<" core dump:"<<((status>>7)&1)<<endl; } return 0; }
- 结果:
在C/C++当中除零,内存越界等异常,在系统层面上,是被当成信号处理的