【Linux】进程信号(一)信号原理、产生方式、调试技巧
信号快速认识
1、在现实世界中有各种各样的信号,如红绿灯,上下课铃声,当没有信号的时侯我们也能识别并处理这些信号,是因为我们以前见过,所以记住了信号的特征的信号的处理方法。
2、信号是异步产生的,并用于信息事件的通知。
3、信号到来的时候,我们可能正在做更重要的事情,所以信号的处理过程,可能不是立即进行的,而是在之后合适的时间处理,所以这就需要我们先把这个信号保存下来。
4、进程处理信号有三种行为:
默认动作
忽略信号
自定义捕捉
部分linux信号介绍(signal)
1、信号名本质就是宏,宏值就是对应信号的编号。
2、信号一共有62个,没有33、34。
3、1-31为普通信号,34-64为实时信号,我们只研究普通信号,实时信号常用于实时场景,实时操作系统用的更多。
4、man 7 signal 指令查看信号详细说明,如下所示(大部分信号的默认处理动作都是终止进程,如Core、Term):
5、系统调用signal可以用来更改进程收到信号后的默认行为(只用修改一次,后续该进程收到修改过默认行为的信号后都会执行自定义行为):
我们以ctrl+c为例,它会像当前进程发送2号信号SIG_INT,示例如下:
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
void myhandler(int x)
{
std::cout << "当前进程:" << getpid() << "捕获到了一个信号:" << x << std::endl;
}
int main()
{
// signal(SIGINT, SIG_IGN);
// signal(SIGINT, SIG_DFL);
signal(SIGINT, myhandler);
while (true)
{
std::cout << "我是一个进程,pid:" << getpid() << std::endl;
sleep(1);
}
return 0;
}
除了将默认行为改为我们自己实现的方法,也可以用系统提供的宏,如SIG_IGN(表示进程收到特定信号后会忽略它)、SIG_DFL(表示进程收到特定信号后会执行默认行为)。
6、SIG_IGN、SIG_DFL本质就是将整型变量进行类型转换为函数指针变量,可以把0、1看作函数指针,我们自己实现的函数的函数指针不可能为0、1,所以就把0、1作为宏。
#define SIG_ERR ((__sighandler_t) -1) /* Error return. */
#define SIG_DFL ((__sighandler_t) 0) /* Default action. */
#define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */
结论1:键盘可以向目标进程发送信号。
前台/后台进程
1、命令行启动的进程,默认是前台进程,查进程状态时后面会有+,比如S+ 。
2、当我们启动进程时在后面带 & (如 ./testsig & ),会让该进程进入后台运行,查进程状态时后面没有+。
3、linux一次登陆状态时,会有bash进程和其他各种启动的进程,但是,系统任何时候只允许一个进程处在前台,其他进程处在后台。(分屏相当于登陆了两次,有两次登陆状态,所以每一个屏都有一个前台进程)
4、区分前后台进程方式:键盘输入的数据将来会交给哪个进程,该进程就是前台进程。(这里可以解释为什么系统只允许一个进程处在前台,因为键盘输入只有一个)
5、fork创建子进程后,父进程退出,子进程会由1号进程领养,此时子进程会自动把自己变为后台进程,所以我们用ctrl+c无法终止该子进程,因为只有前台进程可以接收来自键盘的信号。这时我们只能通过kill命令终止该子进程。
结论二:kill命令可以向目标进程发送信号。
6、这里的知识还可以解释一个现象,当我们用bash启动一个进程后,bash就无法解析执行我们在命令行输出的指令了,因为只有前台进程可以获取键盘的输入,当bash启动进程后该进程就变成前台运行了,bash就会自动变成后台进程。
信号的本质
1、我们知道普通信号一共有31个,没有0号信号,所以进程的PCB里维护了一个int类型的位图变量用来记录进程收到的信号,int一共有32个比特位,比特位的位置表示信号编号,比特位的内容表示是否收到信号。
2、向指定进程发信号本质就是修改指定进程的task_struct中信号位图的对应位置由0变1。
3、进程通过位图对应位置是0还是1来识别部分信号。
4、发送信号本质会修改内核数据,而只有OS有权利修改内核数据,所以:
结论三:无论信号的发送方式有多少种,最终,全都需要借助OS向目标进程发送信号。
信号的产生
1、键盘产生,ctrl+c发送二号信号SIGINT,ctrl+\发送三号信号 (部分信号不可被捕捉,不可被忽略,意思是无法通过signal系统调用更改进程收到这些信号的行为,如9号,19号,它们又被称为管理员信号)
2、kill命令产生(2本质就是3,因为kill命令底层就会调用系统调用kill)
3、系统调用产生:kill
4、软件条件产生
5、异常产生
前三个很好理解本文就不细讲,后两个下文会开专题讲解。
用系统调用kill模拟实现kill指令
kill系统调用是给任意进程发送任意信号。
// mykill.cc
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <string>
int main(int argc, char* argv[])
{
if(argc != 3)
{
std::cout << "usage: " << argv[0] << " signumber pid" << std::endl;
return 1;
}
int signumber = std::stoi(argv[1]);
pid_t mypid = std::stoi(argv[2]);
int n = kill(mypid, signumber);
if(n < 0)
{
perror("kill");
return 2;
}
return 0;
}
//myprocess.cc
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while (true)
{
std::cout << "我是一个进程:" << getpid() << std::endl;
sleep(1);
}
return 0;
}
raise
给自己发送任意信号,相当于kill(getpid(),sig);
abort
给自己发送指定信号:SIGABRT(6),6号信号的默认行为是终止进程。
6号信号是既9、19之后又一个比较特殊的信号,当我们自定义该信号行为时,就算没有手动写让进程退出,但当进程收到6号信号执行了自定义行为后照样会退出。所以abort通常用来结束任务,一般在程序出现重大错误后可以调用abort结束程序。
软件条件产生信号
这个产生信号的方式比较抽象,我们结合例子理解,在之前我们说到当向读端全部关闭的管道进行写操作时OS会向写端发送SIGPIPE,这里的SIGPIPE就是典型的软件条件不满足产生的信号。
alarm
alarm另一种软件条件产生信号的方式。
1、alarm系统调用产生的SIGALRM(14)信号也是由软件条件产生的信号,alarm会对当前进程设置一个闹钟,当时间到后OS就会向该进程发送SIGALRM信号,该信号的默认行为也是让进程退出。
2、alarm有一个特点,设定一次alarm只会起一次效果,如果想让alarm重复执行,就需要重复设定alarm。
3、alarm系统调用一次只允许一个进程设置一个闹钟,如果多次设置以罪行设置的为准。
4、要取消闹钟就对alarm传入0:alarm(0)。
5、alarm系统调用的返回值是0或者是以前设定的闹钟时间还余下的秒数,比如alarm还在运行时重新调用alarm设置闹钟或者取消闹钟就会返回前一个alarm设定的闹钟还余下的秒数。
下面小编用代码模拟实现OS的行为,现在看不懂没关系,后面我们讲OS时还会把这段代码拿出来。
pause系统调用会使当前进程暂停,当进程收到信号时才会被唤醒。
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <functional>
#include <vector>
using func_t = std::function<void()>;
std::vector<func_t> cb;
void disk()
{
std::cout << "我是一个刷盘任务" << std::endl;
}
void sched()
{
std::cout << "我是一个进程调度任务" << std::endl;
}
void handler(int n)
{
for(auto& f : cb)
{
f();
}
std::cout << "我是一个信号捕捉函数,我被调用了" << std::endl;
alarm(1);
}
int main()
{
cb.push_back(disk);
cb.push_back(sched);
signal(SIGALRM, handler);
alarm(1);
while(true)
{
pause();
}
return 0;
}
异常产生信号
最常见的会产生信号的异常是除0和野指针,我们知道当代码出现异常后进程就会崩溃,而进程崩溃的原因就是当程序出现异常被OS识别后,OS会发送信号:SIGFPE(float point exception,对应除0) SIGSEGV(segmentation fault(core dumped),对应野指针)给指定进程,这两个信号的默认行为都是终止进程。
下面我们详细聊聊OS是如何识别到除0和野指针问题并终止对应进程的。
除零产生信号
我们知道计算机的运算操作是由CPU完成的,所以当我们进行除操作时内存中的除数和被除数变量会存入CPU的寄存器中,CPU中除了有保存变量值的寄存器,也有一个状态寄存器,状态寄存器还会有一个比特位充当溢出标志位,当我们正常进行除操作时状态寄存器的溢出标志位默认为0,除后的结果可以正常被保存在CPU的寄存器ecx中,但当我们进行除0操作时,除后是一个无穷大的数,它会超过寄存器ecx的最大保存位数,这时状态寄存器的溢出标志位会被置为1,这时站在CPU的角度说明本次运算在硬件上报错了,而作为计算机软硬件资源管理者的OS一定需要知道这个报错,OS中会维护一个指向当前正在被COU调度的进程PCB的指针(task_struct* current),这时OS就会向该进程发送SIGFPE信号,终止掉该进程,这时该进程在CPU中的硬件上下文就没有了,状态寄存器的溢出标志位也恢复正常了,CPU就恢复到了正常工作状态。
但当我们捕捉SIGFPE信号并改变它的行为让收到该信号的进程不退出时,这时发生除0错误后进程就不会退出,并且会一直重复硬件上下文的 “保存 -> 恢复”,如果SIGFPE信号的handler是打印语句的话,那么就会一直重复打印。 原理如下: 当进程触发信号(如SIGFPE)时,操作系统会执行以下步骤: 保存硬件上下文:将当前 CPU 的程序计数器(PC,记录下一条要执行的指令地址)、寄存器值、栈指针等硬件状态保存起来。
执行信号处理函数:跳转到用户注册的handler函数(如你的handler)执行。
恢复硬件上下文:handler执行完毕后,操作系统会恢复之前保存的硬件上下文,让进程回到 触发信号的那条指令(即a /= 0;) 继续执行。
野指针产生信号
首先我们要明白一点,发生野指针错误时是不一定触发硬件错误并发送信号杀掉进程的,比如数组越界访问。 当我们的示例代码执行到*p = 10时,MMU拿着为0的虚拟地址进行虚拟到物理地址映射时会发生错误,并把错误的地址0写入到CR2寄存器中,并且MMU本身也会提示虚实转化出错了,这时OS就会发现错误,先出错进程发送SIGSEGV信号,终止掉该进程。
逻辑链:野指针->硬件报错->OS发信号->进程退出
键盘产生信号
首先冯诺依玛体系结构告诉我们CPU并不和外设直接打交道,所以CPU和外设如键盘只有控制总线连接,而没有数据总线连接。
下面我们来看具体过程,OS并不会一直检测键盘的按键是否被按下,这样效率太低了,OS是以接受外设的硬件中断来知道按键是否被按下,也就是说当键盘按下时就会向CPU触发硬件中断,来让OS读取键盘中的数据。下面是具体过程:
OS内部不仅提供了处理键盘数据的代码,也为其他外设如磁盘、网卡提供了对应的处理代码,因为外设几乎都会向CPU发送硬件中断来和CPU进行交互,CPU收到中断后需要由OS处理对应外设任务。所以OS内部存在一个函数指针数组,存放了各种外设的中断处理方法,这个数组叫做中断向量表,所以这个中断向量表本质就是OS的一部分。
总结
信号都是由操作系统产生并传递的,因为信号会修改task_struct中的位图变量,只有OS有权力修改内核数据。
但谁让OS产生信号是不确定的,可能是用户(kill系统调用,kill命令),也可能是OS内部的软件条件。
core和term的区别
我们前面已经了解到了信号的默认处理行为core和term都是让进程终止,那它们到底有什么不一样呢,下面来详细谈谈。
首先我们回顾一下以前的知识,进程退出有三种情况:正常退出结果对,正常退出结果不对,异常退出,我们学了前面知识就明白了进程异常退出都是因为收到信号了,无论是野指针、除0、键盘ctrl+c…还是kill。
当进程收到信号时对于某些信号如2(SIGINT)、9(SIGKILL)是不用做进一步追踪的,因为是用户主观发送的信号,但对于某些信号如8(SIGFPE)、11(SIGSEGV)是进程因错误收到的信号,对于用户来说相当于是被动收到的信号,这时是需要OS为用户做进一步追踪的,比如明确的代码的哪一行导致的错误。
这里就要用到我们在讲进程等待时埋下的一个伏笔,coredump标志位,我们之前已经介绍了进程退出时会带出int类型的status参数,它的高16位不看,次低8位表示退出码,在程序正常退出时有意义,因为它是否为0表示程序运行结果的对与错,当进程异常退出时,一定是进程收到了信号,这时程序结果的对与错就不重要了,这时需要重点关注是什么信号导致了进程退出,status的低七位记录了这一信息。
但是我们那时跳过了status的第八位,core dump标志位,这里小编就可以解释了。当进程异常退出时,我们不但要关注是什么信号导致了该进程退出,也想知道什么原因导致的程序异常,所以OS为了支持让程序员进一步追踪问题出现在哪,就让core dump充当标志位,当进程被core信号退出时(如8、11),该进程satus的core dump会被设为1,并且OS会在进程退出的时候,把该进程运行的上下文数据转存到当前目录下,形成一个core文件,后面程序员就可以通过这个core文件溯源问题,所以core dump也被称作核心转储。 当进程被term信号退出时,core dump会被设为0,并且OS不会形成core文件。
确实core的文件大小默认为0,我们要解开这个core的核心转储功能就需要手动设置core文件大小
————————————————
版权声明:本文为CSDN博主「乌萨奇也要立志学C++」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/2501_90265152/article/details/154835117