进程信号
初始信号
生活角度中的信号
- 当我们在网上买东西,再等待不同商品快递的到来。但即便快递没有到来,我们也知道快递来临时,我们该怎么处理快递。也就是我们能“识别快递”
- 当快递到了,但是我们正在忙其他的事情,这段时间内我们没有取快递,但是我们是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”
- 在收到短信时,再到你拿到快递期间,是有一个时间窗口的,在这段时间,我们并没有拿到快递,但是我们知道有一个快递已经来了。本质上是我们“记住了有一个快递要去取”
- 当我们有时间时,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1. 执行默认动作(打开快递,使用商品)2. 执行自定义动作(快递是吃的,马上干掉它)3. 忽略快 递(快递拿到后放一边,继续忙自己的事情)
技术应用角度的信号
我们写个简单的死循环代码:
1#include<iostream>2#include<unistd.h> 3 using namespace std;45 int main()6{7 while(1)8{9 cout<<"This is signal"<<endl;10 sleep(1);11}12return0;13}
为什么我们按下键盘的Ctrl+c就终止掉程序了呢?
我们按下Ctrl+c时键盘输入产生一个硬件中断,被操作系统获取,然后操作系统解释成信号(2号信号)发送给进程,进程收到2号信号后退出。我们可以用signal函数来测试进程是不是收到了2号信号。使用该函数要传入2个参数,第1个参数是信号编号,第2个参数是你要怎么处理这个信号。
确实是收到了2号信号,但是为什么没有退出呢?因为我们把它默认退出改成了打印。
Ctrl+c只能发送给前台进程。
注意:系统当中,打开一个终端一个bash中只允许有一个前台进程。
把进程放在后台运行命令:
要运行的程序名+&
前台进程和后台进程的区别
前台进程演示:
前台进程我们敲命令是没用的,可以终止进程
后台进程演示:
后台进程我们输入命令还可以执行,但是却无法用Ctrl+c干掉。
干掉后台进程可以用fg转成前台进程在干掉,或者用kill + pid干掉它。
这样就干掉了后台进程。
信号概念
信号是进程之间事件异步通知的一种方式,属于软中断。俗话说就是通知事件发生。
查看信号列表
kill-l //查看信号列表
1-31是普通信号,34-64是实时信号。
信号处理常见方式
信号处理方式有3种:
- 忽略此信号。
- 执行该信号的默认处理动作。
- 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称捕捉一个信号。
产生信号
1.通过终端产生信号
SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump,现在我们来验证一下。
Core Dump
当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部 保存到磁盘上,文件名通常是core,这叫做Core Dump,也就是核心转储功能。一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。
用命令查看资源限制:
ulimit -a
默认core文件是关闭的,为了测试我们在云服务中开启coer文件,用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K
ulimit -c 1024
10intmain()11{1213while(1)14{15 cout<<"This is signal"<<endl;16sleep(1);17}18return0;19}
ctrl+c和ctrl+\都可以终止掉进程,但我们打开core,ctrl+\产生了core dumped文件
ulimit命令改变了Shell进程的Resource Limit,test进程的PCB由Shell进程复制而来,所以也具 有和Shell进程相同的Resource Limit值,这样就可以产生Core Dump了。
10intmain()11{12// signal(2,handler);13int a =10/0;14return0;15}
我们来个整数除0,使用一下core文件
core-file core.+进程pid
使用调试时要加上-g选项,Liunx用的是release版本
核心转储功能:我们可以通过调试找到代码的问题所在。这种调试这叫做事后调试。
2.通过系统调用函数产生信号
通过kill命令向进程发送信号
kill -l +信号 +进程pid
写一个死循环试验:
kill -信号编号 +进程pid
kill命令是通过调用kill函数实现的
函数原型:
intkill(pid_t pid,int signo);
成功返回0,失败返回-1.
6 void handler(int sig)7{8 printf("catch a sig:%d\n",sig);9}1011 int main(int argc,char* argv[])12{13 if(argc ==3)14{15 kill(atoi(argv[1]),atoi(argv[2]));16}17return0;18}
我们把它放在后台睡眠,通过给它发送9号信号就干掉它了。
rasie函数
raise函数可以给当前进程发送指定的信号(自己给自己发信号)。
成功返回0,错误返回-1.
1#include<stdio.h>2#include<unistd.h>3#include<signal.h>4#include<sys/types.h>5 void handler(int sig)6{7 printf("catch a sig:%d\n",sig);8}91011 int main()12{1314 signal(3,handler);15 while(1)16{17 raise(3);18 sleep(1);19}20return0;21}
给当前进程发送3号信号。
abort函数使当前进程接收到信号而异常终止。
函数原型:
#include<stdlib.h>voidabort(void);//就像exit函数一样,abort函数总是会成功的,所以没有返回值。
1#include<stdio.h>2#include<unistd.h>3#include<signal.h>4#include<sys/types.h>5#include<stdlib.h>6 void handler(int sig)7{8 printf("catch a sig:%d\n",sig);9}101112 int main()13{1415 signal(6,handler);16 while(1)17{18 abort();19 sleep(1);20}21return0;22}
就终止掉了进程。
3. 由软件条件产生信号
SIGPIPE是一种由软件条件产生的信号,在“管道”中遇见过了。
alarm函数
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
11 int main()12{13 int count =0;14 alarm(1);15 while(1)16{17 count++;18 printf("count:%d\n",count);19}2021return0;22}
到4万多就退了,而我们的cpu运算是很快的,我们的代码是++一次打印到屏幕一次涉及到IO,效率就会很低,我们可以等+的时间到了在打印数据,定义一个全局的count++。
1#include<stdio.h>2#include<unistd.h>3#include<signal.h>4#include<sys/types.h>5#include<stdlib.h>6 int count =0;7 void handler(int sig)8{9 printf("catch a sig:%d\n",sig);10 printf("count=%d\n",count);11 exit(0);12}1314 int main()15{16 signal(SIGALRM,handler);17 alarm(1);18 while(1)19{20 count++;21}2223return0;24}
这次的count就非常大了,达到了4亿多,足以看出IO的效率很低。
4.硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。
14 int main()15{16 int *p;17 *p =10;18return0;19}
就是11号信号。
阻塞信号
信号其他相关常见概念
- 实际执行信号的处理动作称为信号递达(Delivery)
- 信号从产生到递达之间的状态,称为信号未决(Pending)。
- 进程可以选择阻塞 (Block )某个信号。
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
- 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
在内核中表示
pending和block都是位图,有32个比特位。
pending位图保存信号,有2个意思。是谁?是否?是谁表示是哪个信号,是否收到。block位图记录信号被屏蔽的信息。它们2个比特位的位置是一样的,但是比特位的内容是不一样的。handler是数组,数组内容是函数指针,自己的函数地址填入叫做自定义捕捉信号。
- 上面的图中,1号信号没有被屏蔽,没有收到1号信号,当它递达是执行默认处理动作。
- 2号信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
- 3号信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数handler
sigset_t
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。
也就是说我们可以用sigset来设置信号操作,但是不建议使用,建议使用下面的函数进行操作。
信号集操作函数
下面的一系列函数供我们来操作。
#include<signal.h>intsigemptyset(sigset_t *set);intsigfillset(sigset_t *set);intsigaddset(sigset_t *set,int signo);intsigdelset(sigset_t *set,int signo);int sigismember(const sigset_t *set,int signo);
- 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。
- 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号
- 注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
- 这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。
sigprocmask函数
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)
函数原型:
intsigprocmask(int how,const sigset_t *set, sigset_t *oset);
成功返回0,出错返回-1。
参数说明:
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。
注意:如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
sigpending
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。
intsigpending(sigset_t *set);
做一个测试:
1.利用上述的函数将2号信号屏蔽
2.向进程发送2号信号
3.此时2号信号被屏蔽,处于pending状态
4.通过sigpending函数来读取pending信号集来查看验证。
1 #include<stdio.h>2 #include<unistd.h>3 #include<signal.h>4 #include<sys/types.h>5 #include<stdlib.h>67voidprintsigset(sigset_t *set)8{9int i =1;10for(;i<32;++i)11{12if(sigismember(set,i))13{14printf("1");15}16else17{18printf("0");19}20}21printf("\n");22}2324intmain()25{26 sigset_t set,oset;27sigemptyset(&set);//初始化信号集对象28sigemptyset(&oset);29sigaddset(&set,SIGINT);//发送2号信号3031sigprocmask(SIG_BLOCK,&set,NULL);//阻塞2号信号 32 sigset_t pending;33sigemptyset(&pending);//pending位图置空 3435while(1)36{37sigpending(&pending);//获取未决信号集38printsigset(&pending);39sleep(1);40}41return0;4243}
效果如下:
一开始是没有收到任何信号的,当给它发送2号信号,第2个比特位由0变成了1。为了看到2号信号递达后pending的变化,我们可以设置一段时间后解除对2号信号的屏蔽,并且我们对2号信号进行捕捉自定义执行我们自己的动作。
效果如下:
当解除2号信号时,它执行我们自定义的动作,第2个比特位也从1变为了0.
捕捉信号
用户空间和内核空间
(在32位下)程序地址空间中有1-3GB是用户区,3-4GB是内核区。我们的进程映射的物理空间用的用户级页表,每个进程都会有自己的用户级页表。内核区用的是内核级页表映射达到物理内存,所有的进程用的是同样一张的内核页表。用户是没有权限随意的访问系统的代码和数据的。
内核态和用户态
- 用户态:用来执行系统的代码时的状态有很大的权限
- 内核态:执行普通用户的代码的状态,权限小 我们的代码是在用户态和内核态进行切换的。如下图所示:
内核如何捕捉信号
一个信号被递达:是在内核态切换到用户态是进行相关检测的。
那内核是怎么捕捉信号的呢?
为了方便好记可以画个简化的图:
就像数学中的正无穷的符号差不多,但是相交的点是在信号检测,是在内核区的。和横线的4个交点就说明进行了4的状态切换。
信号到用户自定义的函数,为什么切换到用户在执行呢?内核是由权限执行用户的代码
因为如果是非法的代码由内核来执行就会容易中病毒,因为内核具有高的权限的,所以系统进行切换到用户区执行,用户态的权限是微小的。
sigaction
还可以用sigaction函数进行信号捕捉。
函数原型:
intsigaction(int signo,conststructsigaction*act,structsigaction*oact);
参数说明:
- signo:指定信号的编号
- 若act指针非空,则根据act修改该信号的处理动作
- 若oact指针非 空,则通过oact传出该信号原来的处理动作。
- act和oact指向sigaction结构体 sigaction结构体如下:
structsigaction{void(*sa_handler)(int);void(*sa_sigaction)(int, siginfo_t *,void*);
sigset_t sa_mask;int sa_flags;void(*sa_restorer)(void);};
第2个和第5个成员是关于实时信号我们不用管。
sa_handler:收到信号,做什么动作
sa_mask:要屏蔽的信号,默认为0
sa_flags:默认设为0
来个例子测试一手:
1 #include<stdio.h>2 #include<unistd.h>3 #include<signal.h>4 #include<sys/types.h>5 #include<stdlib.h>67voidhandler(int sig)8{9printf("get a sig:%d\n",sig);10}111213intmain()14{15structsigaction act,oact;1617 act.sa_handler = handler;18 act.sa_flags =0;19sigemptyset(&act.sa_mask);2021sigaction(2,&act,&oact);22while(1)23{24printf("i am pid\n");25sleep(1);26}27return0;28}
我们按下Ctrl+c,进程收到了2号信号。由于是我们自定义处理所以它没有退出。
可重入函数
当我们插入链表时,先插入node1,刚让node1->next指向新节点时候来了一个信号,这个信号也是让我们进行插入操作。此时从用户态到内核态中处理,插入完毕后回到用户态回到main函数里继续执行插入操作。此时head从指向node2变成了了指向node1,但是node2却被丢了造成了内存泄漏问题。
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入函数。
当一个函数符合以下条件之一则是不可重入的:
- 调用了malloc或free,因为malloc也是用全局链表来管理堆的
- 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
volatile
先来看一段代码:
1#include<stdio.h>2#include<unistd.h>3#include<signal.h>4#include<sys/types.h>56 int flag=0;78 void handler(int sig)9{10 printf("flag is to 1\n");11 flag =1;12}13 int main()14{1516 signal(2,handler);17 while(!flag);18 printf("i am quit!\n");1920return0;21}
定义1个全局flag变量,不发送2号信号会在死循环,当我们按下Ctrl+c是进程退出。
我们在编译是加上-O2选项
我们按Ctrl+c但是进程却没有退出。我们加了选项把flag的值优化到了CPU的寄存器中,while循环检查的flag不是内存中最新的flag,就会出现二义性的问题。此时就要用volatile。
即使有优化,当我们按Ctrl+c时进程退出了。
volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。
本篇文章到这就已结束了。
版权归原作者 _End丶断弦 所有, 如有侵权,请联系我们删除。