进程信号一
1. Linux信号的基本概念
1.1 生活角度的信号
这个生活的故事,其中包含了信号的方方面面,只要好好理解这个过程,就能够融会贯通到我们接下来的信号理解中。
- 你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”
- 当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”。
- 在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取”
- 当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1.执行默认动作(幸福的打开快递,使用商品)2. 执行自定义动作(快递是零食,你要送给你你的女朋友)3.忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)
- 快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话
1.2 注意
在一个bash当中,只允许一个前台进程
- Ctrl-C产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
- Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像Ctrl-C这种控制键产生的信号。
- 前台进程在运行过程中用户随时可能按下Ctrl-C而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。
1.3 用kill -l命令
可以察看系统定义的信号列表,其中前1-31是普通信号, 34-64为实时信号
- 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义#define SIGINT 2
- 编号34以上的是实时信号,只讨论编号34以下的信号,不讨论实时信号。
1.4 信号处理常见方式
- 忽略此信号。
- 执行该信号的默认处理动作。
- 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。
2. 信号产生的一般方式
信号产生以后,不是立即被处理的。 所以信号在产生时和信号处理时之间是有一个时间窗口的。
2.1 通过终端按键产生信号
通过键盘产生。
Core Dump(核心转储)
操作系统在你这个进程要崩溃之前,把重要的信息给你存储一份,然后放到磁盘上。简单点说:这个就好比喻windows下当你出现程序崩溃的时候给你谈出来的窗口,只不过这个是在Linux下的窗口。
但是在云服务器上是默认关闭的,可以通过ulimit -a
来进行查看资源的上限,那么为什么会关闭这个core file size呢,那是应为你所生成的core.pid文件是占有一定的磁盘大小的,如果你一直不停的出错,那么这种方式将会消耗掉大量的磁盘资源。是否要生成core dump文件在waitpid哪里有一个statue的参数,其中最后一个字节的最有一个bite位为core dump标志位,如果为1则表示需要生成,如果为0表示不需要生成。
2.2 调用系统函数向进程发信号
#include <signal.h>
int kill(pid_t pid, int signo);
第一个参数代表你要给那个进程发信号,第二个参数发多少号信号
int raise(int signo);
这两个函数都是成功返回0,错误返回-1。
kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。
#include<stdio.h>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
void handler(int signo)
{
printf("catch a signal : %d\n",signo);
}
int main(int argc,char *argv[])
{
signal(2,handler); //这个handler是一个函数指针,指向的是一个函数,这个函数就是对于这个信号的处理方法
//这里需要加强强制转换和转换的概念
//atoi可以把字符串转化为整型
//argv[0] 代表的是可执行程序
if(argc == 3)
{
kill(atoi(argv[1]),atoi(argv[2]));
}
return 0;
}
raise函数可以给当前进程发送指定的信号(自己给自己发信号)。但是进过下面的代码发现,还是进程直接被杀掉了。所以9号信号不允许捕捉(不能够自定义)。
#include<stdio.h>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
void handler(int signo)
{
printf("catch a signal : %d\n",signo);
}
int main(int argc,char *argv[])
{
signal(9,handler); //这个handler是一个函数指针,指向的是一个函数,这个函数就是对于这个信号的处理方法
while(1)
{
sleep(1);
raise(9);
}
return 0;
}
abort函数使当前进程接收到信号而异常终止。
#include <stdlib.h>
void abort(void);
就像exit函数一样,abort函数总是会成功的,所以没有返回值。
#include<stdio.h>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
void handler(int signo)
{
printf("catch a signal : %d\n",signo);
}
int main(int argc,char *argv[])
{
signal(6,handler); //这个handler是一个函数指针,指向的是一个函数,这个函数就是对于这个信号的处理方法
while(1)
{
sleep(1);
abort();
}
return 0;
}
2.3 由软件条件产生信号
这个条件在发生时没有什么硬件条件发生,也不是我们主动调用系统调用,而是或许时间片到了而触发的。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号,该信号的默认处理动作是终止当前进程。
#include<stdio.h>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
void handler(int signo)
{
printf("catch a signal : %d\n",signo);
}
int main(int argc,char *argv[])
{
alarm(1);
int count = 0;
while(1)
{
printf("%d\n",count++);
}
}
2.4 硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
指针访问异常时
int main(int argc,char *argv[])
{
printf("begin...\n");
int *p = NULL;
*p = 100;
printf("end...\n");
return 0;
}
会报错Segmentation fault
除0操作时,会报错Floating point exception
int main(int argc,char *argv[])
{
printf("begin...\n");
int i = 1/0;
printf("end...\n");
return 0;
}
由此可以确认,我们在C/C++当中除零,内存越界等异常,在系统层面上,是被当成信号处理的。
所有的信号,都必须经过OS的手发出,上面的四种情况都是信号触发的条件,但是最终都要交给OS,来帮我们杀死进程,因为进程是由操作系统所管理的。
在每个PCB中都有一个信号位图,只需要将对应的bite位置1就可以触发相应的那个信号。这个就是信号的保存方法