进程信号
信号入门
生活角度的信号
- 你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”,也就是你意识里是知道如果这时候快递员送来了你的包裹,你知道该如何处理这些包裹
- 当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”。你可以暂时将包裹搁置,等到有空的时候再处理。
- 在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取”
- 当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1. 执行默认动作(幸福的打开快递,使用商品)2. 执行自定义动作(快递是零食,你要送给你你的女朋友)3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)
- 快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话
技术应用角度的信号
什么是Linux信号?
-
信号是进程之间事件异步通知的一种方式,属于软中断。
-
信号本质是一种通知机制,用户or操作系统通过发送一定的信号,通知进程,某些事件已经发生,进程可以根据信号的种类来进行相关处理、
结合生活信号,对信号的一些小结论
- 进程要处理信号,必须具备信号的“识别”能力,和此能力是程序员给进程植入的,可以理解成出生就自带的
- 信号种类繁多,因此进程收到信号后,有可能是立即执行的,也有可能是后续处理的
- 信号能够被进程所保存,因此也就具备后续处理的能力
- 一般而言,信号的产生相对于进程而言是异步的,也就是说,信号发送的时候,进程是正在干自己的事情的,随时都有可能收到信号,具备识别能力但不代表提前得知
先简单理解一下信号被进程保存
-
进程内部有相关的数据结构位图,信号每次被发送给进程时,此时信号处于未决状态,那么用来保存信号的字段表中信号编号对应的位置就会由0变1
信号处理的三种方式
-
默认处理
-
忽略
-
自定义捕捉
产生信号
通过终端按键产生信号
-
我们平常在写代码时,其实经常都有在跟信号接触,看以下代码:
#include <stdio.h> #include <unistd.h> int main() { while(1){ printf("I am a process, I am waiting signal!\n"); sleep(1); } }
由于没有退出条件,因此进程一直处于死循环,此时我们在键盘里键入:Ctrl+c 能够将进程终止,其实按下组合键后本质就是向当前在前台运行的进程发送了终止信号,进程也立马对此信号做出了反应,终止掉了进程。这就是产生信号的其中一种方式
常见信号
-
信号一共有61个信号,32,33,0号信号是不存在的,而1-31的信号称为普通信号,32-64为实时信号,我们只学习普通信号:
-
关于信号的详细信息我们可以用man手册查看:man 7 signal
-
每个信号的编号都是被宏定义好的:
/* Signals. */ #define SIGHUP 1 /* Hangup (POSIX). */ #define SIGINT 2 /* Interrupt (ANSI). */ #define SIGQUIT 3 /* Quit (POSIX). */ #define SIGILL 4 /* Illegal instruction (ANSI). */ #define SIGTRAP 5 /* Trace trap (POSIX). */ #define SIGABRT 6 /* Abort (ANSI). */ #define SIGIOT 6 /* IOT trap (4.2 BSD). */ #define SIGBUS 7 /* BUS error (4.2 BSD). */ #define SIGFPE 8 /* Floating-point exception (ANSI). */ #define SIGKILL 9 /* Kill, unblockable (POSIX). */ #define SIGUSR1 10 /* User-defined signal 1 (POSIX). */ #define SIGSEGV 11 /* Segmentation violation (ANSI). */ #define SIGUSR2 12 /* User-defined signal 2 (POSIX). */ #define SIGPIPE 13 /* Broken pipe (POSIX). */ #define SIGALRM 14 /* Alarm clock (POSIX). */ #define SIGTERM 15 /* Termination (ANSI). */ #define SIGSTKFLT 16 /* Stack fault. */ #define SIGCLD SIGCHLD /* Same as SIGCHLD (System V). */ #define SIGCHLD 17 /* Child status has changed (POSIX). */ #define SIGCONT 18 /* Continue (POSIX). */ #define SIGSTOP 19 /* Stop, unblockable (POSIX). */ #define SIGTSTP 20 /* Keyboard stop (POSIX). */ #define SIGTTIN 21 /* Background read from tty (POSIX). */ #define SIGTTOU 22 /* Background write to tty (POSIX). */ #define SIGURG 23 /* Urgent condition on socket (4.2 BSD). */ #define SIGXCPU 24 /* CPU limit exceeded (4.2 BSD). */ #define SIGXFSZ 25 /* File size limit exceeded (4.2 BSD). */ #define SIGVTALRM 26 /* Virtual alarm clock (4.2 BSD). */ #define SIGPROF 27 /* Profiling alarm clock (4.2 BSD). */ #define SIGWINCH 28 /* Window size change (4.3 BSD, Sun). */ #define SIGPOLL SIGIO /* Pollable event occurred (System V). */ #define SIGIO 29 /* I/O now possible (4.2 BSD). */ #define SIGPWR 30 /* Power failure restart (System V). */ #define SIGSYS 31 /* Bad system call. */ #define SIGUNUSED 31
-
其中,Ctrl+c 组合键对应的就是2号信号SIGINT,它所对应的Action行为是Term行为:Terminate终止动作
注意
- Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
- Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。
- 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。
如何理解组合键变信号?
- 键盘的工作方式是通过中断方式进行的,因此操作系统能读取并解析组合键,解析完毕后会查找进程列表中在前台运行的进程,并把对于的信号写入进程!
初始信号捕捉
我们前面提到过,信号被写入进程后,进程对此信号的处理有三种方式,我们先来简单介绍一下自定义捕捉方式
介绍signal函数
-
功能:能够捕捉指定的信号编号为signum的信号,然后将此信号的处理方式使用我们自定义的方式handler
-
其中:sighandler_t是一个函数指针,以一个的函数指针作为另一个函数的参数并在函数内使用函数指针调用对应指向的函数,此函数称为回调函数,自定义捕捉时,我们实现自定义函数,并将函数作为参数传给signal,通过回调的方式,来修改对应信号的捕捉方法。返回值是自定义捕捉前的函数的地址。
通过信号捕捉的方式,验证Ctrl+c组合键是SIGINT信号
-
#include <iostream> #include <unistd.h> #include <signal.h> using namespace std; void cathSig(int signum) { cout<<"i catch you! SIGINT!"<<endl; } int main() { signal(SIGINT,cathSig); while(1) { cout<<"my pid:"<<getpid()<<endl; sleep(1); } }
我们可以观察到,按下Ctrl-c后进程并不会像之前一样退出,而是执行了我们自己的代码!得以验证以下两个结论:
- Ctrl-c对应的信号为SIGINT信号
- Ctrl-c对应的信号处理方式为终止进程,自定义捕捉方式后,原先的处理方法就被舍弃了
注意
- signal函数一般写在最前面,原理类似于我们要先买票,才能对信号进行自定义捕捉,后面讲阻塞信号时会解释原理!
Core Dump
我们先来比较两个信号
-
这两个信号都是从键盘敲组合键来向进程发送信号,其中:
- Ctrl-c 对应的是2号SIGINT
- Ctrl-\ 对应的是3号信号SIGQUIT
-
他们两个的作用都能够用来终止进程,但不同的是:
2号对应的行为是Term:Terminate
3号对应的行为是Core:Core Dump
-
简单验证:
什么是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 -c1024
使用命令ulimit命令查看并修改相关资源配置
显示0时,代表核心转储不被允许产生,我们需要自行开启:
此时已显示core文件允许的最大内存为1024K,但仅仅在当前会话生效,退出会话后就失效了
- ulimit命令改变了Shell进程的Resource Limit,test进程的PCB由Shell进程复制而来,所以也具 有和Shell进程相同的Resource Limit值,这样就可以产生Core Dump了。
验证Ctrl-\ 发送的SIGQUIT能够产生core文件的功能
-
先写一个死循环程序:
int main() { while(1) { printf("my pid:%d\n",getpid()); sleep(1); } }
运行并用Ctrl-c命令终止后发现并没有产生任何文件:
-
再次运行并使用Ctrl-\ 命令终止:
这次我们发现产生了core.27213文件,并且后缀就是以进程的pid来命名的,代表此文件就是此进程出现某种异常时,由os将此进程在内核中相关的核心数据转存到磁盘中
核心转储有什么用?
- 主要是用来进行调试,有了核心转储的数据,调试时会方便很多
利用核心转储进行调试
-
除了SIGQUIT信号的行为是core,还有其他很多命令也有core行为,下面我们使用SIGFPE信号来验证使用核心转储调试时是否会方便
FPE:Floating point exception指的是浮点数错误,一般指除0错误
-
首先编写程序,并且编译时,带上-g选项,生成debug文件
int main() { while (1) { printf("my pid:%d\n", getpid()); sleep(1); int a=100; a/=0; printf("run here...\n"); } }
-
重新运行程序,此时也随之生成了core dump文件
-
启用gdb调试程序,输入core-file core.pid(core文件名)
此时我们能发现,在core dump文件的帮助下,能自动帮我们定位在哪一行代码收到了什么信号,比如这里,我们在29行除0错误处收到了8号信号,而8号信号就是对应的SIGFPE信号
core dump标记位
- 其实我们在进程间控制时就已经和Core Dump有过一面之缘了,我们来看这张图:
在进程控制一篇提到过,子进程退出时,父进程可以通过进程等待的方式,收集子进程的退出信息,以防子进程变成僵尸进程,其中进程等待可以使用wait和waitpid系统调用
其中我们也提到过status参数是一个输出型参数,其中如果子进程是被信号所杀的话,此参数的低七位会被填入终止信号的编号,而第八位则是显示是否有生成core dump文件,我们也可以通过代码来验证一下:
用子进程验证core dump标记位
-
int main() { pid_t id = fork(); if (id == 0) { // child sleep(1); int a = 100; a /= 0; exit(0); } int status = 0; waitpid(id, &status, 0); cout << "father:" << getpid()<<" " << "child:" << id <<" " << "exit sig:"