朋友们、伙计们,我们又见面了,本期来给大家带来信号和信号的产生相关代码和知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!
C 语 言 专 栏:C语言:从入门到精通
数据结构专栏:数据结构
个 人 主 页 :stackY、
C + + 专 栏 :C++
Linux 专 栏 :Linux
目录
1. 信号概念
2. 信号的产生
① 前台进程
② 后台进程
2.1 信号的产生方式
① 通过键盘产生
② 通过系统调用产生
③ 通过硬件异常产生
④ 通过软件条件产生
3. 核心转储(core dump)
1. 信号概念
从生活的角度来说:
道路边的红绿灯信号、上下课的铃声、古时候的狼烟、旗语、手机的来电铃声等等;
当这些信号出现的时候我们可以识别并下意识的做出对应的反应。
在Linux角度来说:
在命令行输入kill -l就可以查看系统可支持的信号列表;
当我们程序运行起来,我们直接使用Ctrl + c 就可以终止进程,这也叫做信号。
- ① 信号没有产生的时候,其实我们已经知道怎么来处理这个信号;
- ② 信号什么时候来,我们并不清楚,信号的到来相对于我目前做的工作来说是异步产生的;
- ③ 信号产生了,我们不一定要立即处理他,而是等到合适的时机来处理;
- ④ 信号到来到我要处理信号的期间,我们需要对已经到来的信号进行暂时保存。
信号是向目标进程发送通知消息的一种机制。
2. 信号的产生
在谈论信号的产生之前先来了解一下Linux中的前、后台进程:
① 前台进程
在命令行中我们输入./exe启动一个进程时启动的是前台进程;
我们自己的程序以前台进程的方式运行,此时我们输入指令时是没有任何效果的!
shell也是一个进程,我们使用命令行时使用的就是shell这个前台进程,所以当使用./exe运行程序时,OS就会自动将shell放到后台,前台进程只能有一个。
一般情况下:Ctrl + c 终止前台进程
Ctrl + \ :终止前台进程
② 后台进程
在命令行输入./exe & 以后台程序的方式运行;
后台进程可以有多个,使用 jobs命令查看所有的后台进程:
使用 fg 任务编号:将指定的后台进程提到前台
前台进程不能被暂停,如果我们正常运行起来的前台进程使用Ctrl + z时暂停,会将该进程必须放到后台;
使用bg 任务编号:将指定后台进程运行起来。
2.1 信号的产生方式
在命令行输入kill -l可以查看所有信号;
没有0号信号,因为在我们程序运行结束之后,成功返回的是0。
信号有对应的编号和名称,我们既可以使用编号又可以使用名称。
1 ~ 31叫做普通信号,34 ~ 64叫做实时信号。
向目标进程发送信号时,对于普通信号来讲,该进程如何知道自己是否收到某种信号?
在进程的PCB中维护一张位图,并且每个进程都有一张函数指针数组表,数组下标和信号编号强相关;
位图中比特位的位置决定信号的编号;
比特位的内容决定是否收到该信号。
当进程收到指定信号时,先去位图中查找对应的信号,然后通过信号编号在函数指针数组中执行对应的方法。
因为OS是进程的管理者,无论信号的产生方式有多少种,永远只能由OS向目标进程发送。
发信号其实是向位图中写入(对对应的比特位操作)
信号的本质:用软件来模拟硬件与CPU之间的中断行为!
① 通过键盘产生
我们在命令行./exe运行起来的进程,当我们在键盘上敲下Ctrl + c组合键时,该进程就会被终止,此时我们从键盘上输入Ctrl + c就是向该进程发送指定的信号。
在合适的时候处理信号的有三种情况:
- 1. 信号的默认行为
- 2. 忽略该信号
- 3. 自定义行为(信号的捕捉)
signal接口可以用于捕捉信号去执行自定义函数方法:
我们输入的Ctrl + c其实是2号信号,输入的Ctrl + z其实是20号信号,输入的Ctrl + /其实是3号信号,接下来我们可以采用信号捕捉的方式来验证一下:
#include <iostream> #include <unistd.h> #include <signal.h> #include <sys/types.h> using namespace std; // 自定义行为的函数返回值必须是void,参数必须是int类型 void handler(int signo) { while (true) { std::cout << "这是一个" << signo << "号信号" << std::endl; sleep(1); } } int main() { signal(2, handler); // 捕捉2号信号 signal(3, handler); // 捕捉3号信号 signal(20, handler);// 捕捉20号信号 while (1) { cout << "running ...., pid: " << getpid() <<endl; sleep(1); } return 0; }
9号信号不可被捕捉,即便是我们使用signal接口进行捕捉,也不能让9号信号执行自定义方法!
② 通过系统调用产生
kill接口:向指定进程发送指定的信号
让我们自己的进程运行5s,然后使用kill系统调用来发送2号信号;
#include <iostream> #include <unistd.h> #include <signal.h> #include <sys/types.h> using namespace std; void handler(int signo) { while (true) { std::cout << "这是一个" << signo << "号信号" << std::endl; sleep(1); } } int main() { signal(2, handler); // 捕捉2号信号 int cnt = 5; while (cnt--) { cout << "running ...., pid: " << getpid() << endl; sleep(1); } // kill(getpid(), SIGINT); // 系统调用接口产生信号 kill(getpid(), 2); return 0; }
raise接口:自己给自己发送指定的信号
#include <iostream> #include <unistd.h> #include <signal.h> #include <sys/types.h> using namespace std; // 自定义行为的函数返回值必须是void,参数必须是int类型 void handler(int signo) { while (true) { std::cout << "这是一个" << signo << "号信号" << std::endl; sleep(1); } } int main() { signal(3, handler); int cnt = 5; while (cnt--) { cout << "running ...., pid: " << getpid() << endl; sleep(1); } // 给自己发送3号信号 raise(SIGQUIT); return 0; }
abort接口:使当前进程收到信号并异常终止
#include <iostream> #include <unistd.h> #include <signal.h> #include <sys/types.h> using namespace std; // 自定义行为的函数返回值必须是void,参数必须是int类型 void handler(int signo) { while (true) { std::cout << "这是一个" << signo << "号信号" << std::endl; sleep(1); } } int main() { signal(6, handler); // 捕捉2号信号 int cnt = 5; while (cnt--) { cout << "running ...., pid: " << getpid() << endl; sleep(1); } abort(); // 收到6号信号 return 0; }
③ 通过硬件异常产生
异常产生的信号比如常见的段错误、野指针问题;
在这里演示一下常见的除0错误产生的信号:
当发生除0异常时,OS直接向进程发生8号信号来终止进程。
接下来我们捕捉一下8号信号看看会发生什么:
#include <iostream> #include <unistd.h> #include <signal.h> #include <sys/types.h> using namespace std; void handler(int signo) { // 注意这里没有循环 std::cout << "这是一个" << signo << "号信号" << std::endl; sleep(1); } int main() { signal(8, handler); // 捕捉8号信号 int a = 10; a /= 0; return 0; }
我们捕捉信号执行的自定义函数没有循环,为什么在结果中会循环打印呢?
当发生除0错误时,CPU内部的状态寄存器(硬件资源)中的溢出标记位会置为1,此时OS就会检测到,并把其解释为kill(targetprocess, SIGFPE)函数调用,就会终止进程,但是我们将SIGFPE进行捕捉之后去执行对应的自定义方法,并没有终止该进程,所以CPU会一直调度该进程,当遇到除0异常时又继续执行我们实现的自定义函数。
当发生一些野指针和越界访问时,此时从由虚拟到物理映射时页表中的MMU硬件单元就会出现异常,就会给进程发送指定信号用来终止进程。
④ 通过软件条件产生
在之前的管道部分就说过,管道的读端关闭,如果写端还是继续写入,OS就会向写端发送SIGPIPE信号来终止写端进程,这就是一种通过软件来产生的信号。
我们可以通过alarm函数来给进程设置一个闹钟,也就是告诉OS在指定时间之后向进程发送SIGALRM信号,默认处理动作是终止当前进程。
alarm接口:
#include <iostream> #include <unistd.h> #include <signal.h> #include <sys/types.h> using namespace std; void handler(int signo) { while (true) { std::cout << "这是一个" << signo << "号信号" << std::endl; sleep(1); } } int main() { signal(SIGALRM, handler); // 3秒之后发送信号 alarm(3); while (true) { cout << "running ...., pid: " << getpid() << endl; sleep(1); } return 0; }
- 所有用户的行为都是以进程的形式在操作系统中表现的;
- 操作系统只要把进程调度好,就能完成所有的用户任务;
- CMOS时钟会周期性的、高频率的向CPU发送时钟中断;
- 操作系统的执行是基于硬件中断的!
3. 核心转储(core dump)
当进程运行异常时,OS会根据异常信息向进程发送对应的信号使进程终止,并且打印的异常信息也很明确,但是我们如何知道异常出现在程序代码的具体的什么位置呢?
当进程运行发生core异常时,OS会将该进程在内存中发生异常的核心上下文数据转储到磁盘中形成一个以该进程pid和core命名的磁盘文件。
使用命令 ulimit -a:查看core文件的大小
使用命令 ulimit -c 大小:修改core文件的大小
在修改完core文件大小之后出现异常时就会转储到磁盘:
为什么我们使用的Linux云服务器这个转储的动作默认是关闭的呢?
- ① 因为发成异常之后形成的core文件太大了;
- ② 如果异常比较多,磁盘被打满,会直接影响服务器的正常运行。
使用core文件:
朋友们、伙计们,美好的时光总是短暂的,我们本期的的分享就到此结束,欲知后事如何,请听下回分解~,最后看完别忘了留下你们弥足珍贵的三连喔,感谢大家的支持!