进程信号
信号引子:
生活角度的信号
你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,
你该怎么处理快递。也就是你能“识别快递”
当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那
么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不
是一定要立即执行,可以理解成“在合适的时候去取”。
在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知
道有一个快递已经来了。本质上是你“记住了有一个快递要去取”
当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1. 执行默认动
作(幸福的打开快递,使用商品)2. 执行自定义动作(快递是零食,你要送给你你的女朋友)3. 忽略快
递(快递拿上来之后,扔掉床头,继续开一把游戏)
快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话
技术应用角度的信号
用户输入命令,在Shell下启动一个前台进程。
. 用户按下 Ctrl-C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程
. 前台进程因为收到信号,进而引起进程退出
前台进程and后台进程
前台进程:
是在终端中运行的命令,那该终端就是进程的控制终端,一旦这个终端关闭,进程也随之消失
我们在前台进程中,我们可以通过ctrl+c打断
后台进程:
后台进程也就做守护进程,是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。 不受终端控制,它不需要终端的交互;Linux的大多数服务器就是使用守护进程实现的。比如Web服务器的httpd等。
Ctrl+C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程
结束就可以接受新的命令,启动新的进程。
显然我们让该程序放在后台后,我们用Ctrl+C不可以打断进程
但是按命令是有效果的,因为这是后台进程,不会影响命令。如果想要查看该后台进程,可以按jobs,看到当前后台进程:
如果想要将后台进程变为前台进程,可以按fg [jobs中对应的号],这里对应的是1,就是fg 1。这时,按ctrl+c就可以结束进程了。
Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生
的信号。
前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行
到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步
(Asynchronous)的。
信号概念:
信号是进程之间事件异步通知的一种方式,属于软中断。
用kill -l命令可以察看系统定义的信号列表
每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定 义 #define
SIGINT 2
我们发现这里有62个信号,其中34以上的是实时信号,我们本章就只讨论31号信号以下的,不讨论实时信号,这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal
信号处理常见方式概览
(sigaction函数稍后详细介绍),可选的处理动作有以下三种:
- 忽略此信号。
- 执行该信号的默认处理动作。
- 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉
(Catch)一个信号(自定义函数接口)
信号的过程:
信号产生前:
信号是会被进程记住的有没有产生 + 什么信号产生),实际上进程信号会被记录在pcb中,总所周知,pcb是一个结构体,因此就够提里面有一个位图变量来记录接受什么几号信号
假设接收了5号信号:
进程接收了几号信号,就会在第几位bit位上置为1,
pcb是内核的数据结构,因此我们并没有权限去修改pcb结构体里面的位图变量,因此只有OS才有权限去修改
所以无论信号怎么产生,最终一定只能是OS来进行信号的设置。
产生信号:
因为接受信号后,只有OS才有权限去修改pcb结构体的位图变量,因此只有OS才可以发送信号,发送信号的方式有很多种。
通过终端按键产生信号
SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump,现在我们来验证一
下。
这个其实就是我们刚刚用Ctrl+C去终止进程,我们也可以在命令行中发送其他信号给os,由OS发送给进程
但实际上除了按ctrl-c之外,按ctrl-\也可以终止该进程:
ctrl+c本质是2号信号,Ctrl+\本质是3号信号
在具体展开说明之前,来看下信号的接口函数:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
-
sighandler_t:返回值为void,参数为int的一个函数指针
-
signum:对哪个信号设置捕捉信号
-
handler:是一个函数指针,这个函数允许用户自定义对信号的处理动作
这里的SIGINT就是2号信号:
#include<iostream> #include<signal.h> #include<unistd.h> using namespace std; void handler(int signo) { cout << "我是一个进程,刚刚获取了一个信号:" << signo << endl; } int main() { //SIGINT:2号信号 //这里不是调用handler方法,这里只是设置了一个回调,让SIGINT(2)产生的时候,该方法才会被调用 //如果不产生SIGINT(2),该方法不会被调用! signal(SIGINT, handler); sleep(3); cout << "进程已经设置完了" << endl; sleep(3); while (true) { cout << "我是一个正在运行中的进程:" << getpid() << endl; sleep(1); } return 0; }
上述代码并不是调用handle函数,而是设置了一个回调,当我们调用2号信号,我们才回调handle函数,如果不调用2号信号就不回调
ctrl + c本质就是给前台进程发送2号信号给目标进程,上述结果中我们每按一次ctrl-c,就获得一个2号信号,目标进程默认对2号信号的处理,是终止自己,但是现在我们更改了对2号信号的处理,这就是我们设置了用户自定义处理动作。为了终止该进程,我们使用了ctrl-\来终止该进程。
上述测试结果也足矣说明键盘是可以产生信号的!
我们不止可以对2号信号设置handle函数,我们也可以对3号信号设置handle函数,如果我们对所有的信号设置handle函数,那我们是不是可以让进程当枪不入杀不死呢?
我们来验证一下是否可以对9号信号设置handle函数:
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
void handler(int signo)
{
cout << "我是一个进程,刚刚获取了一个信号:" << signo << endl;
}
int main()
{
for (int sig = 1; sig <= 31; sig++)
{
signal(sig, handler);//设置所有的信号的处理动作,都是自定义动作
}
sleep(3);
cout << "进程已经设置完了" << endl;
sleep(3);
while (true)
{
cout << "我是一个正在运行中的进程:" << getpid() << endl;
sleep(1);
}
return 0;
}
我们发现我们依然可以通过9号信号将进程杀死!!!
结论是31个信号,我们可以对30个信号进行设置,但是我们唯独不可以对9号信号设置handle函数
总结用户层产生信号的方式:键盘产生
注意这个是键盘产生的信号,不是键盘发送的信号,是OS发送的信号。
问:OS是如何发送信号的?
OS能找到每个进程的take_struct,也能找到当前显示器上前台进程的take_struct,每一个进程的take_struct内部都有一个位图,OS在拿到了对应的信号后,将这个对应的位置由0设为1,OS就完成了信号的发送(OS发送信号,也可以说成是写入信号)
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
我们之前在进程等待waitpid中提及过core dump但是我们并没有进行讲解,今天我们来了解一下core dump
pid_t waitpid(pid_t pid, int *status, int options);
waitpid函数的第二个参数status是一个输出型参数,用于获取子进程的退出状态。status是一个整型变量,但status不能简单的当作整型来看待,status的不同比特位所代表的信息不同,具体细节如下(只关注status低16位比特位)ÿ