目录
1、信号入门
通过自然世界中人对信号的基本理解:
接下来是对进程的分析:
我们可以看到信号都是宏
那么还有一个问题:信号是如何发送的以及如何记录的?
首先回答信号是如何记录的:普通信号的编号是从【1,31】,所以信号应该用位图来保存信号数据,信号的记录是进程的task_struct(PCB)->结构体变量,本质更多的是为了记录信号是否产生。
如何发送:进程收到信号,本质是进程内信号位图被修改了,也只有OS才有资格修改进程内的数据,因为操作系统是进程的管理者,所以绝对有资格修改进程数据,本质就是OS直接去修改目标进程task_struct中信号位图。(信号发送只有OS有资格,但是信号发送的方式可以有多种)
1.1、技术应用角度的信号
1、用户输入命令,在shell下启动一个前台进程。
用户按下Ctrl-C,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程
前台进程因为收到信号,进而引起进程退出
#include <stdio.h> #include <unistd.h> int main() { while(1) { printf("hello world!\n"); sleep(1); } return 0; }
当我们用ctrl+c这个组合键结束这个进程的本质是,操作系统识别到ctrl+c这个组合键,操作系统将ctrl+c解释成了2号新号,也就是SIGINT。
这个就是处理信号三种方案中的默认动作,为了要能够让信号自定义,有下面这个接口:
#include <signal.h>
typedef void(*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
第一个参数是信号编号,也就是信号中的1-31。
第二个参数的类型是一个函数指针,且是一个回调函数,相当于我们可以通过signal,提前向进程注册一个对信号的处理方法。
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <signal.h> #include <stdlib.h> void sigcb(int signo) { printf("get a sig: %d\n", signo); } int main() { signal(2,sigcb); while(1) { printf("hello world!\n"); sleep(1); } return 0; }
可以看到这次ctrl+c的时候,操作系统没有终止进程,因为默认行为被我们改成了自定义行为。这里无论使用ctrl+c还是kill -2 id操作都是执行我们的自定义行为,程序不会被终止,如果要退出进程,只能发送其他的退出信号
1.2、注意
1、Ctrl+C产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进场结束就可以接受新的命令,启动新的进程。
2、Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像Ctrl+C这种控制键产生的信号。
3、前台进程在运行过程中用户随时可能按下Ctrl+C而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步的。
1.3、信号概念
信号是进程之间事件异步通知的一种方式,属于软中断。
1.4、用kill -l命令可以查看系统定义的信号列表
每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到
编号34以上的是实时信号,暂不做讨论。其他信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明:man 7 signal
1.5、信号处理常见方式概览
可选的处理动作有以下三种:
1、忽略此信号
2、执行该信号的默认处理动作
3、提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉一个信号
2、产生信号
2.1通过终端按键产生信号
SIGINT的默认动作是终止进程,SIGQUIT的默认动作是终止进程并且Core Dump,现在我们来验证一下:
可以看到,CTRL+\对应的是三号信号:SIGQUIT,3号信号默认的动作是Core,表示在结束的时候它有一个动作叫核心转储。
使用ulimit -a指令查看系统资源
看到core file size的大小为0,意味着它的核心转储是关闭的,那么我们为它设置一个值:
接着再运行一遍程序:
可以看到,进程也退出了,而且后面还多了一个(core dumped),用ls查看的时候也多了一个core.1751这个临时文件,这个1751数字叫做发生这次核心转储进程的id。
一个进程在终止的时候有很多终止方式,其中Terminal一般是直接退出,也可以理解成是我们手动的让它退出了,但不做任何转储文件的dump(转储),而我们如果自己打开了核心转储,并且我们收到了信号(不同的信号又不同的作用,不同的信号是一种不同的错误类别),而有些信号是需要进行和核心转储的。
比方说,代码运行的时候出错了,我们关心的是代码为什么出错了,我们之前讲的代码的三种退出方式:1、代码跑完结果对,2、代码跑完结果不对,3、代码运行中的时候出错。前两个最起码跑完了,最后根据退出码就能判断哪里有问题,那么第三种:代码运行中的时候出错了,我么也要有办法判定是什么原因出错了。
我们在平时出现第三种情况的时候,我们一般式通过调试来判断哪里出了问题,但其实还有Linux中的一种方法就是通过核心转储功能:把进程在内存中的核心数据转储到磁盘上,core.pid->核心转储文件。目的是为了调试、定位问题。一般云服务器是属于线上生产环境,默认是关闭的。
打开的状况我们上面那也进行了演示。那么还有一个问题:
为什么在云服务器上核心转储功能默认是关闭的呢?
比方说我们在服务器上写一个网络服务或者定期执行的一个任务,这个服务可能因为某种异常而挂掉,如果你打开了核心转储,那么挂掉之后会在本地的磁盘文件中生成corn文件,这个无可厚非,但是一般大的互联网公司在服务挂掉的时候,最重要的事情不是在乎是因为什么原因挂掉的,重要是的想尽快的让它恢复正常。因为BUG不是经常时间,而是偶尔的事情。所以重要的是先让服务跑起来,不要让公司收到太大的影响。当服务回复之后再对故障进行排除工作。
如果是小问题的话那么就先让服务恢复出来,然后再进行检查,但是如果出了大问题,而且有一个一崩就重启的功能,那么已重启就崩,崩了就重启,如此往复。就会出现大量的core file文件:
而且我们可以看到,这种文件一个都要1MB多,每个都不小,要说重启很长时间,那么我们去排查的时候会发现core文件将某个分区或者磁盘文件都占满了,最终导致服务想重启都没法重启,甚至操作系统都挂了,所以默认是关闭的。
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。
下面通过一个实例来观察一下:
可以发现出现了一个浮点型异常,而且多出了一个core.27575这个core dumped文件。
通过gdb和core.27575文件找到了问题在20行,这样我们就快速定位到了刚刚的代码是因为什么原因出错的,这个调试叫做事后调试,也就是程序崩溃了再进行调试。
为什么C/C++进程会崩溃?
本质就是因为收到了信号。
那为什么会受到信号?
首先我们要知道,信号都是又OS发送的,那么OS又怎么识别到有进程触发了问题呢?
OS在进行正常运行的时候发现CPU内有一个计算机状态标志位发生了除0错误,然后操作系统就立马定位当前运行的那个进程,所以就来进行终止。
所以操作系统识别到了硬件错误,然后将这个硬件错误解释(包装)成信号发送给目标进程。
其实本质就是找到这个进程的PCB,向目标的位图比特位由0置1,然后这个进程在合适的时候处理8号信号时默认就给“自己终止了”。
所以错误最终一定会在硬件层面上有所表现,进而被OS识别到,所以进场最后才会崩溃。
2.2、调用系统函数向进程发信号
这里我们要用到的接口是kill
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
我们写了一个重复打印的mytest,然后通过系统调用kill掉了mytest进程,可以看到已经成功的使mytest退出。
还有两个给自己发送信号的接口
#include <signal.h>
int raise(int sig);
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <signal.h> void handler(int signo) { printf("get a sig: %d\n", signo); } int main() { signal(2, handler); while(1) { printf("I am a process, pid: %d\n", getpid()); sleep(1); raise(2); } return 0; }
#include <stdlib.h>
void abort(void);
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <signal.h> #include <stdlib.h> void handler(int signo) { printf("get a sig: %d\n", signo); } int main() { signal(6, handler); while(1) { printf("I am a process, pid: %d\n", getpid()); sleep(1); abort(); } return 0; }
可以看到,运行起来后,接收到的是6号信号,而且接收到一次之后就退出了,可是上面明明对6号信号进行了捕捉。
这是因为有些信号是可以被捕捉,有些信号不可以被捕捉,6号信号既被捕捉了,也被终止了,这就是6号信号,abort的作用很像我们一直用的exit(),但是exit()是正常终止,而abort()的本质是通过信号来终止,是自己终止自己,但是要说明的是,exit()本质上是函数,只要是函数就说明它可能会失败,而abort函数总是会成功(函数无返回值)。
2.3、由软条件产生信号
我们之前的异常本质上是由软件引起的,但最终引起的问题是在硬件上,也就是CPU的状态寄存器出了问题,MMU转化出了问题,所以最后我们就看到操作系统识别硬件出了错误,然后转化成信号发送给进程。
软件条件产生信号:在我们写管道那里的时候说,有一端是读端,有一端是写端,如果将读端关闭,写端一只写,那么写端就会被立刻终止。这样的原因就是写入的软件条件不满足,也就是当前管道式不允许你写入的,所以我们当时就收到了一个SIGPIPE这个信号,这个信号就是由于软件条件产生的信号,所以就是我们写入的条件不成熟,这就是软件条件。
当然还有其他的软件条件,就是alarm(闹钟)函数。
这个函数的返回值是0或者是以前设定的闹钟时间余下的秒数。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒后给当前进程发SIGALRM信号,该信号的默认处理动作是终止当前进程。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
void handler(int signo)
{
printf("get a sig: %d\n", signo);
}
int main()
{
alarm(1);
int count = 0;
while(1)
{
printf("count is: %d\n", count++);
}
return 0;
}
这个代码的意思是,1秒后发送14号信号SIGALRM,然后在这1秒内看能进行多少次count++并打印出来,我们可以看到,在五万次左右,但是这其实不代表真实的速度,因为我们这里是在外设打印了就会慢很多。而且也会有网络的原因,我们在网络上计算,然后再发送过来,就会慢。
这里我们改一改:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
int count = 0;
void handler(int signo)
{
printf("count is: %d\n", count);
exit(1);
}
int main()
{
signal(14, handler);
alarm(1);
while(1)
{
count++;
}
return 0;
}
可以看到,我们直接让它累加,最后再打印,就可以看到会加到很大,这就是因为在累加的时候没有进行IO,所以我们得知,如果计算机在进行IO的时候,效率非常低。
2.4、硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
3、总结思考一下
1、上面所说的所有信号产生,最终都要由OS来进行执行,为什么?
答:OS是进程的管理者
2、信号的处理是否是立即处理的?
答:在合适的时候
3、信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?
答:需要。记录在进程的PCB中,有对应的PCB位图
4、一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?
答:知道:默认、自定义、捕捉
5、如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?
答:本质就是OS根据某种信号类别,直接去修改PCB位图中的0 1序列,进而达到发送信号的目的