1 生活角度的信号
你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”,当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取。在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取”当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1. 执行默认动作(幸福的打开快递,使用商品)2. 执行自定义动作(快递是零食,你要送给你你的女朋友)3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话。
2 技术应用角度的信号
我们之前提到过,可以通过kill -l
命令来查看信号:
[grm@VM-8-12-centos lesson16]$ kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX
其中我们将标号34以上的叫做实时信号 ,本文章不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明 man 7 signal每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到。
我们之前讲过的,当显示屏中不断有数据在刷屏时我们可以用ctrl+c来终止进程,其本质用户按下ctrl+c ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出。但是我们也提过前台进程可以用ctrl+c终止前台进程,但是却不能终止后台进程,我们将可执行程序运行时加上&就能够变成后台进程。此时我们只有通过kill -9命令来杀死进程。
注意:
Ctrl+C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl+C 这种控制键产生的信号。
前台进程在运行过程中用户随时可能按下 Ctrl+C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步。(Asynchronous)的。
3 信号的产生
3.1 由系统调用向进程发信号
3.1.1 signal
其实signal本质上来说并不是向进程发送信号,而是在收到了信号后我们用户自定义处理信号的方式,我们上面提到过,信号的处理方式有三种:
- 执行默认动作
- 执行自定义动作
- 忽略信号
首先我们来看看signal的介绍:
我们观察参数,发现其中有一个函数指针,这个函数指针是由我们自己实现的,也就是当我们收到特定的信号时我们自定义的采取怎样的方式去处理。
我们可以写代码来验证一下:
signal.cc:
#include<iostream> #include<string> #include<signal.h> #include<unistd.h> using namespace std; void hander(int signo) { cout<<"get a signal:"<<signo<<endl; exit(2); } int main() { signal(2,hander); while(1) { cout<<"my pid is:"<<getpid()<<endl; sleep(1); } return 0; }
当我们在键盘上敲下kill -9 进程pid
时我们可以观察到下面现象:
这是由于我们自定义了信号的处理方式,当进程收到了2号信号时就不会执行默认2号信号的处理方式,而是执行了我们自己定义的处理方式(用函数指针实现)
注意:signal(2, handler)调用完这个函数的时候,hander方法被调用了吗?
答案是没有的,是接受到2号信号后才回调这个hander方法。
3.1.2 kill
int kill(pid_t pid, int signo);
这个函数的使用很简单,通过参数的命名我们就能够知道如何给指定进程发送信号。
那我们可以自己实现一个mykill:
#include <iostream> #include <cstring> #include <cerrno> #include <string> #include <signal.h> #include <unistd.h> #include <sys/types.h> using namespace std; void Usage(string proc) { std::cout << "\tUsage: \n\t"; std::cout << proc << " 信号编号 目标进程\n" << std::endl; } int main(int argc, char *argv[]) { if (argc != 3) { Usage(argv[0]); return 1; } int sig = stoi(argv[1]); int pid = stoi(argv[2]); int n = kill(pid, sig); return 0; }
其实很好理解,杀死目标进程我们是创建了新进程来处理。
3.1.3 raise
int raise(int signo);
kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。raise函数可以给当前进程发送指定的信号(自己给自己发信号)。
除此之外还有一个C语言的abort函数(使当前进程接收到信号而异常终止)
#include <stdlib.h>
void abort(void);
就像exit函数一样,abort函数总是会成功的,所以没有返回值。
3.2 由软件条件产生信号
SIGPIPE是一种由软件条件产生的信号,在“管道”中已经介绍过了。本节主要介绍alarm函数 和SIGALRM信号
#include <unistd.h> unsigned int alarm(unsigned int seconds); 调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数.
我们来看看下面这个程序:
int cnt=1; void hander(int sigo ) { cout<<cnt<<endl; exit(1); } int main() { signal(14,hander); alarm(1); while(true) { cout<<cnt++<<endl; } return 0; }
这个程序的作用是统计出1秒中cnt累加的次数,我们运行起来看看:
当我们修改代码,不要加上输出语句再试试:
我们发现第二次的cnt累加次数明显远大于第一次的值,这其实也是很好理解的因为IO很慢
3.3 硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号(8号)发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV(11号)信号发送给进程。
我们可以写程序来验证一下:
#include<iostream> #include<signal.h> using namespace std; void hander(int sign) { cout<<"初零错误"<<endl; } int main() { signal(SIGFPE,hander); int a=10; cout<<a/0<<endl; return 0; }
当我们运行时:
为什么会一直在重复打印呢?原因是在于我们自定义8号信号时执行但是没有退出程序,而除零错误·一直存在,所以会一直在显示屏上刷屏。野指针问题也类似。
3.4 通过终端按键产生信号
SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump,现在我们来验证一下。
什么是Core Dump?
当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。 首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K: ulimit -c 1024.
那我们究竟如何查看当前进程的资源上限呢?
我们可以使用ulimit -a:
我们发现在云服务器上默认是关闭了核心转储文件的,如果我们想设置可以使用命令:
ulimit -c XXX
来自定义的设置好核心转储文件大小。
我们之前通过man 7 signal
命令查看时:
不难发现有些信号是带有Core的,Term的默认执行动作是终止进程,没有其他动作;而Core是先会进行核心转储,再终止进程的。
我们可以来试试:
当我们写了一个除零错误的代码时让其自定义执行默认动作时:
就会生成一个core.xxx的文件,这个文件就是核心转储文件,那么这个文件有啥用呢?
我们在用gdb调试时可以使用core-file core.xxx来帮助我们快速定位到错误所在,但是当我们没重复运行一次时就会重新生成一个core文件,所以云服务器上是默认关闭掉CoreDump文件。不知道大家忘记了没有,我们在讲解进程控制时就已经提过了一个Core Dump标志:
是否具有core dump是由这个标志位所决定的,这个标志位的获取方法有很多种,我这里给出一种:
(status<<7)&1
3.5 总结思考一下
上面所说的所有信号产生,最终都要有OS来进行执行,为什么?OS是进程的管理者.
信号的处理是否是立即处理的?在合适的时候.
信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?
一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?
如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?
4 信号的保存
4.1信号其他相关常见概念
- 实际执行信号的处理动作称为信号递达(Delivery)
- 信号从产生到递达之间的状态,称为信号未决(Pending)。
- 进程可以选择阻塞 (Block )某个信号。
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
- 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。