进程信号

在学习进程信号的相关知识之前,我们先来看一看操作系统定义的信号列表中有多少种信号

使用kill -l命令来查看,如下图:


注意:

1.一眼看过去,好像有64种信号,但其实只有62种信号!!!(其中1~31是普通信号,也是我们在这里要重点学习的信号;34~64是实时信号);

2.每一种信号都有其对应的编号和宏定义,我们可以在signal.h中找到;

3.使用命令man 7 signal对于每一个信号在什么条件下产生,各自的默认处理动作是什么,都有详细的说明。(如下图)


以上是有关信号的一点基本知识的铺垫,下面我们一起来学习信号!

一、产生信号

1.通过终端按键产生信号

(1)SIGINT信号的默认处理动作是终止进程;

(2)SIGQUIT信号的默认处理动作是终止进程并且Core Dump(核心转储)。

这里我们将Core Dump的相关知识总结一下:

(1)Core Dump:当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上(文件名通常为core);

(2)Post-mortem Debug(事后调试):进程异常终止通常是因为有Bug,比如非法内存访问导致的段错误,事后可以用调试器检查core文件以查清楚错误原因;

(3)一个进程允许产生多大的core文件取决于进程的Resource Limit(该信息保存在PCB中);

(4)默认是不允许产生core文件的,因为core文件中可能包含用户的密码等敏感信息,这样做不安全。(但是在开发调试阶段可以用ulimit命令改变这个限制,允许产生core 文件)。

操作如下:

首先,用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K:$ ulimit -c 1024


然后写一个死循环:


接着前台运行这个程序,终端键入Ctrl-\  (Ctrl-C不行)


到这里我们就深刻的体会到了ulimit命令改变了Shell进程的Resource Limit,fun进程的PCB由Shell进程复制而来,所以也具有和Shell进程相同的Resource Limit值,这样就可以产生Core Dump了。

使用core文件:


2.调用系统函数向进程发信号

首先在后台执行死循环程序,然后用kill命令发送SIGSEGV信号;


注意:

(1)5076是fun进程的id;

(2)发送命令也可以写成kill -11 5076(11是该信号的编号);

(3)多按一次回车才会显示Segmentation fault,是因为5076进程终止掉之前已经回到了Shell提示符等待用户输入下一条命令,Shell不希望Segmentation fault信息和用户的输入交错在一起,所以等用户输入命令之后才显示。

下面我们学习几个发送信号的函数:

#include<signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
//kill命令是调用kill函数实现的
//kill函数可以给一个指定的进程发送指定的信号
//raise函数可以给当前进程发送指定的信号(自己给自己发信号)
//这两个函数都是成功返回0,错误返回-1
#include<stdlib.h>
void abort(void);
//abort函数使当前进程接收到信号而异常终止
//abort函数总是成功的,因此没有返回值

3.由软件条件产生信号

有三种软件条件产生信号的方式:SIGPIPE\alarm\SIGALRM

#include<unistd.h>
unsigned int alarm(unsigned int seconds);
//调用alarm函数可以设置一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号,该信号的默认处理动作是终止当前进程

该函数的返回值是0或者之前设置的闹钟剩下的秒数。

下面我们来做一个小游戏,代码和运行结果如下:



这个程序的作用是一秒之内不停地数数,1秒中到了就会被SIGALRM信号终止。

当产生信号后,接下来很容易想到的就是信号该如何处理,这里我们给出常见的三种处理方式:

(1)忽略此信号;

(2)执行该信号的默认处理动作;

(3)提供一个信号处理函数,要求内核在处理该信号时切换回用户态的处理函数称为捕捉一个信号。

二、阻塞信号

1.一些信号的常见概念

(1)信号递达(Delivery):实际执行信号的处理动作;

(2)信号未决(Pending):信号从产生到递达之间的状态;

(3)阻塞(Block):进程可以选择阻塞某个信号;

(4)被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才能执行递达动作;

(5)阻塞和忽略是不同的,一旦信号被阻塞就不会递达,而忽略是在递达之后的一种处理动作。

2.在内核中的表示


3.几点说明:

(1)每一个信号都有两个标志位,分别为未决(Peding)和阻塞(Block),还有一个函数指针(handler)即:处理动作。

(2)信号产生时,内核在PCB(进程控制块)中设置该信号的未决标志,直到信号产生才会消失。

(3)每一个信号都由一个bit位的未决标志,即:不是零就是一。记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞可以用相同的数据类型sigset_t来存储,其中sigset_t称为信号集。这个类型可以表示每个信号的“有效”和“无效”状态,(在阻塞中表示该信号是否被阻塞,在未决信号集中表示该信号啊是否处于未决状态)。

4.信号集操作函数

(1)

#include<signal.h>
int sigemptyset(sigset_t *set);
//函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号
int sigfillset(sigset_t *set);
//函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统所支持的所有信号
int sigaddset(sigset_t *set);
//信号集中添加某种有效信号
int sigdelset(sigset_t *set);
//信号集中删除某种有效信号
int sigismember(const sigset_t *set, int signo);//布尔类型,用于判断一个信号集的有效信号中是否包含某种信号,包含为1
注意:这几个函数都是成功返回0,出错返回-1.

(2)sigprocmask(可以读取或者更改进程的信号屏蔽字(阻塞信号集))

#include<srdio.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
//如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。
//如果oset是空指针,则更改进程的信号屏蔽字,参数how指示如何更改。
//如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。

假设当前的信号屏蔽字为mask,下表说明了how参数的可选值


如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。

(3)sigpending

#include<signal.h>
sigpending
//读取当前进程的未决信号集,通过set参数传出。
//调用成功返回0,出错返回-1


当键入ctrl+c(SIGINT)之后,该信号被text阻塞,所以一直处于未决状态,不被处理

三、信号捕捉

1.我们通过下面一张图看一看信号信号在内核是如何实现信号哦的捕捉


2.sigaction

#include<signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact)

(1)sigaction函数可以读取和修改与指定信号有关联的处理动作;

(2)调用成功则返回0,出错返回-1;

(3)signo是指定信号的编号;

(4)若act指针非空,则根据act修改该信号的处理动作;若oact指针非空,则通过oact传出该信号原来的处理动作;

(5)act和oact指向sigaction的结构体。

3.pause

#include<unistd.h>
int pause(void);

(1)pause函数使调用进程挂去直到有信号递达;

(2)如果信号的处理动作是终止进程,则进程终止,pause函数没有机会返回;如果信号的处理动作是忽略,则进程继续处于挂起状态,pause不返回;如果信号的处理动作是捕捉,则调用了信号的处理函数之后pause返回-1,,

(3)errno设置为EINTR,因此pause只有出错的返回值。

4.我们用alarm和pause来实现mysleep


四、可重入函数


像上图这样

(1)重入:insert函数被不同的控制流调用,有可能在第一次调用还没有返回时就再次进入该函数;

(2)不可重入函数:insert函数访问一个全局链表,有可能因为重入而造成错乱;

(3)可重入函数:如果一个函数只访问自己的局部变量或参数。

如果一个函数符合以下条件之一则是不可重入函数:

(1)调用了malloc或free,因为malloc也是用全局链表来管理堆的;

(2)调用了标准I/O库函数。标准库函数的很多实现都以不可重入的方式使用全局数据结构。

五、volatile

对于程序中存在多个执行流程访问同一全局变量的情况,volatile限定符是必要的,此外,程序虽然只有单一的执行流,下面的情况之一也要加该关键字:

(1)变量的内存单元中的数据不需要写操作就可以自己发生变化,每次读上来的值都可能不一样;

(2)即使多次向变量的内存单元中写数据,只写不读。

六、竞态条件与sigsuspend函数

1.竞态条件:由于异步事件在任何时候都有可能发生(这里的异步事件指出现更高优先级的进程),如果我们写程序时考虑不周密,就有可能由于时序问题导致错误。

2.sigsuspend函数

(1)功能:“解除信号屏蔽”和“挂起等待信号”这两步合并成一个原子操作

(2)

#include<signal.h>
int sigsuspend(const sigset_t *sigmask);

该函数没有成功返回值,返回-1,errno设置为EINTR

七、SIGCHLD信号

SIGCHLD信号的处理函数为,父进程只需要专心处理自己的工作,不必关心子进程,子进程终止时会通知父进程,父进程在信号处理函数中调用wait获得子进程的退出状态并且打印。

编写一个这样的程序,如下:


猜你喜欢

转载自blog.csdn.net/cherrydreamsover/article/details/79827152
今日推荐