进程信号(Linux)
Posted 雨轩(爵丶迹)
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了进程信号(Linux)相关的知识,希望对你有一定的参考价值。
Linux进程信号
信号入门
1、生活角度的信号
- 你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”
- 当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”。
- 在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取”
- 当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:
1. 执行默认动作(幸福的打开快递,使用商品)
2. 执行自定义动作(快递是零食,你要送给你你的女朋友)
3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏) - 快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话
2、技术应用角度的信号
先看下面的代码,一眼就看出来是死循环。
1 #include <stdio.h>
2 int main()
3
4 while(1)
5 printf("I am a process, I am waiting signal!\\n");
6 sleep(1);
7
8
我们直接Ctrl+C终止这个进程
我们知道可以终止这个进程,但是为什么能终止?
我们输入Ctrl+C,在Shell下启动一个前台进程 ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号(Ctrl+C是2号信号),2号信号发送给目标前台进程。前台进程因为收到2号信号,进而引起进程退出。
上面是系统默认的处理方式,输入Ctrl+C就会终止掉进程,就跟你默默的打开快递一样,没有做任何思考(进程就是你,操作系统就是快递员,信号就是快递)。
那么我们是否可以自定义进行处理?当然是可以的,这里先介绍一个函数:signal函数
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signal()将信号符号设置为处理程序,处理程序可以是SIGIGN、SIG-DFL,也可以是程序员定义的函数(“信号处理程序”)的地址。
signum:信号编号
如果配置设置为SIG_IGN,则忽略该信号。
如果配置设置为SIG_DFL,则发生与信号关联的默认动作。
如果配置设置为一个功能,然后首先要么配置重置到SIG_DFL,或sianal被阻塞,然后处理程序是用signum参数调用。如果对处理程序的调用导致信号被阻塞,那么信号在从处理程序返回时被解除阻塞。
handler:对应自处理方法
我们写来用signal函数自定义捕抓这个信号,signal函数对2号信号进行捕捉,如果我们捕抓到了2号信号,证明Ctrl+C确实是收到了2号信号
1 #include <stdio.h>
2 #include <signal.h>
3 void handler(int signo)
4
5 printf("this signo is %d\\n",signo);
6
7 int main()
8
9 signal(2, handler);
10 while(1)
11 printf("I am a process, I am waiting signal!\\n");
12 sleep(1);
13
14
当该进程收到2号信号时,我们在输入Ctrl+C就不会终止进程,而是执行我们对应的自定义方法,捕抓到了该信号,并打印出来。由此证明,Ctrl+C确实是收到了2号信号。
这里要退出这个进程可以使用Ctrl+\\(Ctrl+\\对应3号信号SIGQUIT,代表退出信号) 。
3、注意
- Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
- Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。
- 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。
4、信号概念
信号是进程之间事件异步通知的一种方式,属于软中断。
5、用kill -l命令可以察看系统定义的信号列表
下面是每个信号编号对应的信号。
其中1-31号信号是普通信号,34-64号是实时信号。每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义#define SIGINT 2(Ctrl+C对应的信号)
我们可以通过命令,找出对应信号编号宏定义的路径,在查看其宏定义。
[dy@VM-12-10-centos rumen]$ sudo find / | grep 'signum.h'
/usr/include/bits/signum.h
下面是信号对应的宏定义
6、信号是如何发送的以及如何记录?
- 信号如何发送:进程收到信号,本质是进程内对应结构体里面的信号位图被修改了,只有OS有这个资格去修改,所以是OS直接去修改了目标进程task_struct中的信号位图。例外信号发送的方式有多种,但是只有OS有资格发送(掌管生杀大权)。
- 信号如何记录:当一个进程接受到信号后,该信号记录在进程对应的task_struct(PCB)中,PCB本质就是一个结构体变量,我们可以用32位的位图来记录一个信号是否产生。
其中比特位的位置代表信号编号,比特位的内容就代表是否收到信号,比如第6个比特位是1就表明收到了6号信号。
7、信号处理常见方式概览
可选的处理动作有以下三种:
- 忽略此信号。(忽略并不是不处理,比如你拿了快递丢到一边,继续玩手机)
- 执行该信号的默认处理动作。
- 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。
可以通过命令man 7 signal查看各个信号默认的处理动作
注意:SIGKILL和SIGSTOP命令是不能捕捉的,捕捉了,系统就不能杀掉一个进程和停止一个进程了。
产生信号
1、通过终端按键产生信号
刚开始,我们就已经展示了Ctrl+C能终止一个进程。其实Ctrl+\\也可以终止掉一个进程。我们继续拿第一次的代码来展示。
1 #include <stdio.h>
2 int main()
3
4 while(1)
5 printf("I am a process, I am waiting signal!\\n");
6 sleep(1);
7
8
终止掉了该进程。
那么Ctrl+C和Ctrl+\\ 有什么区别?
1、Ctrl+C对应2号信号(SIGINT),SIGINT的默认处理动作是终止进程。
2、Ctrl+\\ 对应3号信号(SIGQUIT),SIGQUIT的默认处理动作是终止进程并且Core Dump。
这个两个命令都是终止进程,但是SIGQUIT多出了一个操作为Core Dump(核心转储)。
Core Dump
那么什么是核心转储(Core Dump)?我们先来看一下这个两个命令对应的信号的默认处理方式。
从上面这张图,我们看到两个都是终止进程,但是对应的行为Action是不一样的,一个对应Term,一个对应Core,我们在来看看这两个的区别。
看完这两个的区别之后,我们在来看核心转储的概念。
- 首先解释什么是Core Dump。当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部 保存到磁盘上,文件名通常是core,这叫做CoreDump。
- 进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortemDebug(事后调试)。
- 一个进程允许产生多大的core文件取决于进程的Resource Limit(资源限制)(这个信息保存在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K:
$ ulimit -c 1024
了解概念之后我们继续往下走,刚刚我们知道一个进程默认是不允许产生core文件的,即核心转储被关闭了,我们可以通过使用ulimit -a
命令查看当前资源限制的设定。
可以看到,core file size的大小为0,因为核心转储被关闭了。我们可以通过命令ulimit -c size
来修改其大小,这就代表着核心转储被打开了。
注意:ulimit命令改变了Shell进程的Resource Limit,test进程的PCB由Shell进程复制而来,所以也具有和Shell进程相同的Resource Limit值,这样就可以产生Core Dump了
我们在运行刚开始的文件,输入Ctrl+\\ 就会显示Core Dump,并且会产生一个Core文件(保存进程的用户空间内存数据),core文件后面还带有一个进程编号pid
我们在对应的程序里面获取对应的pid,来验证一下是否是对应的进程编号。
1 #include <stdio.h>
2 #include <unistd.h>
3 int main()
4
5 while(1)
6 printf("pid:%d\\n",getpid());
7 printf("I am a process, I am waiting signal!\\n");
8 sleep(1);
9
10
我们这里可以看到是一样的,说明就是对应的进程编号
上面我们的程序异常终止了,我们肯定会去想为什么终止,这时候就需要去调试代码。
核心转储作用:核心转储的目的就是为了在调试时,方便问题的定位。
我们写一段代码来展示:
#include <stdio.h>
#include <unistd.h>
int main()
printf("hello world\\n");
sleep(3);
int a = 1/0;
return 0;
除0错误,生成了一个core文件。
我们通过命令gdb
对当前的可执行程序进行调试,然后直接使用core-file core
文件命令加载core文件,即可判断出该程序在终止时收到了8号信号,并且定位到了错误的代码处,显示行数。
这样的操作就叫事后调试,以后我们的程序崩溃了,就可以打开核心转储,gdb
调试即可找到对应报错的行数。
我们之前学进程等待的时候,其中的waitpid
函数和wait
函数:
pid_t waitpid(pid_t pid, int *status, int options);
pid_t wait(int*status);
status是一个输出型参数,用于获取子进程的退出状态。status的不同比特位所代表的信息不同(可以当位图理解,0表示没有,1表示有),具体如下图。
如果进程是正常退出,高八位表示其退出状态,如果进程是异常终止,则低七位表示该进程收到对应的终止信号,第八位是core dump标志,表示该进程是否进行了核心转储。
我们编写一个进程等待的程序,在子进程中进行一个非法访问,比如野指针异常、除0、异常终止等。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
pid_t pid = fork();
if(pid < 0)
perror("fork fail");
return 1;
else if(pid == 0)
printf("i am child\\n");
int a = 1/0;
else
int status = 0;
int ret = wait(&status);
printf("exitCode:%d, core dump:%d, exitSignal:%d\\n",(status>>8)&0xFF,(status>>7)&1, status & 0x7F);
return 0;
通过下面的运行结果,我们可以看到core dump
标志位为1,即第八位为1,说明对应子进程发生了核心转储(为0则表示没有发生核心转储)。
子进程没有发生核心转储,core dump
标志位为0。
所以我们现在知道了core dump
标志位的作用:表示一个进程崩溃的时候,是否进行了core dump。
组合键
通过终端按键产生信号,并不只有Ctrl+C、Ctrl+\\,还有其他的组合键。我们这里写一个程序,将1-31号信号进行捕获,并将收到信号后的默认处理动作改为我们自定义的动作。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int signal)
printf("catch signal:%d\\n", signal);
int main()
int signo;
for (signo = 1; signo <= 31; signo++)
signal(signo, handler);
while (1)
sleep(1);
return 0;
通过运行结果,我们可以看到Ctrl+C、Ctrl+\\、Ctrl+Z都被捕获了,然后执行我们的自定义动作,需要注意的是,我们去杀死这个程序时,执行kill pid
,我们可以捕抓到对应的信号,但是kill -9 pid
我们是捕获不到的,刚开始就说过,如果9号信号捕获了,那么一个进程就永远都杀不死了,即便是操作系统本身。
2、 调用系统函数向进程发信号
我们先在后台执行一个死循环程序,然后执行kill -SIGSEGV pid
命令发送SIGSEGV信号给该进程。
指定发送某种信号的kill命令可以有多种写法,上面的命令还可以写成 kill -SIGSEGV 19707
或 kill -11 19707,11是信号SIGSEGV的编号。以往遇 到的段错误都是由非法内存访问产生的,而这个程序本身没错,给它发SIGSEGV也能产生段错误。
kill命令:调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。
#include <signal.h>
int kill(pid_t pid, int signo);
成功返回0,错误返回-1
模拟kill命令
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
void Usage(char* proc)
printf("Usage: %s pid signo\\n", proc);
int main(int argc, char* argv[])
if(argc != 3)
Usage(argv[0]);
return 1;
argv是命令行参数指针数组
pid_t pid = atoi(argv[1]);
int signo = atoi(argv[2]);
kill(pid, signo);
return 0;
我们开起一个后台进程
[dy@VM-12-10-centos SignalGeneration]$ sleep 1000 &
[1] 30379
然后我们通过自己编写的代码,通过程序名 进程pid 信号编号
杀死该后台进程。
raise函数:raise函数可以给当前进程发送指定的信号(自己给自己发信号)
int raise(int signo);
成功返回0,错误返回-1
通过程序,我们来看一下raise的使用:
1 #include <stdio.h>
2 #include <signal.h>
3 #include <unistd.h>
4 void handler(int signo)
5
6 printf("this signo is %d\\n",signo);
7
8 int main()
9
10 signal(2, handler);
11 while(1)
12
13 sleep(1);
14 raise(2);
15
16
每隔一秒给自己发送一个2号信号
当然,9号信号不能自己给自己发,会直接终止掉进程的,至于原因,这里就不在冗余。
abort函数:使当前进程接收到信号而异常终止。
#include <stdlib.h>
void abort(void);
就像exit函数一样,abort函数总是会成功的,所以没有返回值
通过程序,我们来看一下abort的使用:
1 #include <stdio.h>
2 #include <signal.h>
3 #include <stdlib.h>
4 #include <unistd.h>
5 void handler(int signo)
6
7 printf("this signo is %d\\n",signo);
8
9 int main()
10
11 signal(6, handler);
12 while(1)
13
14 sleep(1);
15 abort();
16
17
发送一个指定信号(SIGABRT:6)就异常终止。
abort函数的作用是异常终止进程,而abort本质是通过向当前进程发送SIGABRT信号而终止进程的,但使用abort函数终止进程总是成功的。
exit函数的作用是正常终止进程,使用exit函数终止进程可能会失败,
3、由软件条件产生信号
进行通信中的SIGPIPE信号:
- SIGPIPE信号是一种由软件条件产生的信号,当进程在使用管道进行通信时,读端进程将读端关闭,而写端进程还在一直向管道写入数据,那么此时写端进程就会收到SIGPIPE信号进而被操作系统终止。
我们来看例外的一种软件条件产生的信号,SIGALRM信号:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm 函数可以设定一个闹钟
也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号
该信号的默认处理动作是终止当前进程
alarm函数的返回值:
若调用alarm函数前,进程已经设置了闹钟,则返回上一个闹钟时间的剩余时间,并且本次闹钟的设置会覆盖上一次闹钟的设置。
如果调用alarm函数前,进程没有设置闹钟,则返回值为0。
比如,我们可以看看在自己的终端上面,一秒钟可以将一个变量累加到多少。
1 #include <stdio.h>
2 #include <unistd.h>
3 int main()
4
5 int count = 0;
6 alarm(1);
7 while(1)
8
9 count++;
10 printf("count = %d\\n",count);
11
12
在我自己的服务器上,可以加到24000左右。
其实,count累加的值远远大于上面的运行结果。由于我们每进行一次累加就进行了一次打印操作,而与外设之间的IO操作所需的时间要比累加操作的时间更长。再加上自己网络传输等耗时,所以累加的比较少。
我们可以让count一直累加,然后捕获SIGALRM信号,最后在输出count看看是多少。
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <signal.h>
4 #include <stdlib.h>
5 int count = 0;
6 void handler(int sig)
7
8 printf("catch a signal:%d\\n", sig);
9 printf("count = %d\\n",count);
10 exit(-1);
11
12 int main()
13
14 signal(SIGALRM, handler);
15 alarm(1);
16 while(1)
17
18 count++;
19
20 return 0;
21
由此证明:与计算机单纯的计算相比较,计算机与外设进行IO时的速度是非常慢的。
4、硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
模拟一下野指针异常
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <signal.h>
4 #include <stdlib.h>
5 int main()
6
7 int *p;
8 *p = 100;
9 return 0;
10
通过运行结果我们可以得知,程序异常了,操作系统是如何识别到的?
首先,学进程的时候我们就知道,访问一个变量,需要经过页表的映射,虚拟地址转换成物理地址,最终找到对应的数据位置。
总结:
1、其中页表属于一种软件映射关系,而实际上在从虚拟地址到物理地址映射的时候还有一个硬件叫做MMU,它是一种负责处理CPU的内存访问请求的计算机硬件,因此映射工作不是由CPU做的,而是由MMU做的,但现在MMU已经集成到CPU当中了。
2、当需要进行虚拟地址到物理地址的映射时,我们先将页表的左侧的虚拟地址导给MMU,然后MMU会计算出对应的物理地址,我们再通过这个物理地址进行相应的访问。
3、而MMU既然是硬件单元,那么它当然也有相应的状态信息,当我们要访问不属于我们的虚拟地址时,MMU在进行虚拟地址到物理地址的转换时就会出现错误,然后将对应的错误写入到自己的状态信息当中,这时硬件上面的信息也会立马被操作系统识别到,进而将对应进程发送SIGSEGV信号。
所以C/C++程序崩
以上是关于进程信号(Linux)的主要内容,如果未能解决你的问题,请参考以下文章