0


精讲Linux-进程信号

进程信号

初始信号

生活角度中的信号

  • 当我们在网上买东西,再等待不同商品快递的到来。但即便快递没有到来,我们也知道快递来临时,我们该怎么处理快递。也就是我们能“识别快递”
  • 当快递到了,但是我们正在忙其他的事情,这段时间内我们没有取快递,但是我们是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”
  • 在收到短信时,再到你拿到快递期间,是有一个时间窗口的,在这段时间,我们并没有拿到快递,但是我们知道有一个快递已经来了。本质上是我们“记住了有一个快递要去取”
  • 当我们有时间时,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种: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. 忽略此信号。
  2. 执行该信号的默认处理动作。
  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 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。

本篇文章到这就已结束了。

标签: linux 运维 服务器

本文转载自: https://blog.csdn.net/weixin_45599288/article/details/122279207
版权归原作者 _End丶断弦 所有, 如有侵权,请联系我们删除。

“精讲Linux-进程信号”的评论:

还没有评论