信号概念的引入
生活中的例子,比如:交通信号灯 、警报 、手机来电等,都是一种信号,而且信号的产生有三个特点 :
1> 随机性
2>信号没发出,但知道其作用
3>记住了信号了特征
由此,我们可以得出,对于进程而言信号的特性类似
1>信号的产生是异步的(不知道什么时候会发出信号)
2>即使没收到信号,但提前知道该怎么处理
3>提前记住了信号(什么信号发出,该怎么处理)
信号概念
信号是UNIX和Linux系统响应某些条件而产生的一个事件,接收到该信号的进程会相应地采取一些行动。信号通常是由于一个错误产生发出的,同时还可以作为进程间通信或修改行为的一种方式,由一个进程发送给另一个进程。
信号的产生叫生成,接收到信号叫捕获。
用 kill -l 命令可以察看系统定义的信号列表:
信号在头文件 <signal.h> 中定义,信号都以SIG开头,常用的信号如下:
信号名称 | 说明 |
---|---|
STGALRM | 由alarm函数设置的定时器产生 |
SIGHUP | 由一个处于非连接状态的终端发送给控制进程,或者由控制进程在自身结束时发送给每个前台进程 |
SIGINT | 一般由从终端敲入的Ctrl+C组合键或预先设置好的中断字符产生 |
SIGKILL | 因为这个信号不能被捕获或忽略,所以一般在shell中用它来强制终止异常进程 |
SIGPIPE | 如果在向管道写数据时没有与之对应的读进程,就会产生这个信号 |
STGTERM | 作为一个请求被发送,要求进程结束运行。UNIX在关机时用这个信号要求系统服务停正运行。它是 kill 命令默认发送的信号 |
STGUSR1, STGUSR2 | 进程之间可以用这个信号进行通信,例如让进程报告状态信息等 |
信号处理常⻅三种⽅式 :
1> 执⾏该信号的默认处理动作
2> 忽略此信号
3> 自定义处理(提供⼀个信号处理函数,要求内核在处理该信号时切换到⽤户态执⾏这个处理函数,这种⽅式称为捕捉(Catch)⼀个信号)
信号产生
1> 通过终端按键产⽣信号
2> 调⽤系统函数向进程发信号
3> 由软件条件产⽣信号
4> 硬件异常产生信号
阻塞信号
相关概念
- 实际执⾏信号的处理动作称为信号递达(Delivery)
- 信号从产⽣到递达之间的状态,称为信号未决(Pending)。
- 进程可以选择阻塞 (Block )某个信号。
- 被阻塞的信号产⽣时将保持在未决状态,直到进程解除对此信号的阻塞,才执⾏递达的动作.
- 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,⽽忽略是在递达之后可选的⼀种处理动作
信号在内核中表示
每个信号都有两个标志位分别表⽰阻塞(block)和未决(pending),还有⼀个函数指针数组表⽰处理动作。
信号产⽣时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
【解析上图】
- SIGHUP 信号未阻塞也未产⽣过,当它递达时执⾏默认处理动作
- SIGINT 信号产⽣过,但正在被阻塞,所以暂时不能递达(虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞 )
- SIGQUIT 信号未产⽣过,⼀旦产⽣ SIGQUIT 信号将被阻塞,它的处理动作是⽤户⾃定义函数 sighandler
- 如果在进程解除对某信号的阻塞之前这种信号产⽣过多次,将如何处理? POSIX.1允许系统递送该信号⼀次或多次 :Linux 中常规信号在递达之前产⽣多次只计⼀次,⽽实时信号在递达之前产⽣多次可以依次放在⼀个队列⾥。
sigset_t
- 每个信号只有⼀个 bit 的未决标志,“⾮0即1”,不记录该信号产⽣了多少次,阻塞标志也是这样表⽰的。未决和阻塞标志可以⽤相同的数据类型 sigset_t 来存储, sigset_t 称为信号集,这个类型可以表⽰每个信号的“有效”或“⽆效”状态
- 未决信号集中“有效”和“⽆效”的含义是该信号是否处于未决状态
- 阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞
- 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask)
信号集操作函数
sigsett类型对于每种信号⽤⼀个bit表⽰“有效”或“⽆效”状态,⾄于这个类型内部如何存储这些bit则依赖于
系统实现,从使⽤者的⾓度是不必关⼼的,使⽤者只能调⽤以下函数来操作sigset t变量,⽽不应该对它的内
部数据做任何解释,⽐如⽤printf直接打印sigset_t变量是没有意义的
#include <signal.h>
int sigemptyset(sigset_t *set); //初始化
int sigfillset(sigset_t *set); //初始化
int sigaddset (sigset_t *set, int signo); //添加信号
int sigdelset(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可以读取或更改进程的信号屏蔽字(阻塞信号集)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
如果oset是⾮空指针,则读取进程的当前信号屏蔽字通过oset参数传出
如果set是⾮空指针,则 更改进程的信号屏蔽字,参数how指⽰如何更改。如果oset和set都是⾮空指针,则先将原来的信号 屏蔽字备份到oset⾥,然后根据set和how参数更改信号屏蔽字
假设当前的信号屏蔽字为mask,下表说明了how参数的可选值
参数名 | 具体含义 |
---|---|
SIG_BLOCK | set包含了我们希望添加到当前信号屏蔽字的信号,相当SIG_BLOCK于mask=masklset |
SIG_UNBLOCK | set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set |
SIG_SETMASK | 设置当前信号屏蔽字为set所指向的值,相当于mask=set |
sigpending
#include <signal.h>
int sigpending (slgset_t *set) //输出型参数
读取当前进程的未决信号集,通过set参数传出。调⽤成功则返回0,出错则返回-1。
捕捉信号
定义: 如果信号的处理动作是⽤户⾃定义函数,在信号递达时就调⽤这个函数,这称为捕捉信号。
信号的捕捉视图:
-操作系统会在适当的时候给进程发送信号,什么时候是最适当呢?
在上图中,在内核态到用户态的时候会对信号进行处理,同时进行检测,如果这时候发现异常,操作系统就会发送信号
-sighandler和main函数是使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程
- sigaction
原型
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
功能 : sigaction函数可以读取和修改与指定信号相关联的处理动作。
返回值 : 调⽤成功则返回0,出错则返回- 1。
参数 :
signo —>指定信号的编号
若act指针⾮空,则根据act修改该信号的处理动作
若oact指针⾮空,则通过oact传出该信号原来的处理动作
act和oact指向sigaction结构体
将sahandler赋值为常数SIGIGN传给sigaction表⽰忽略信号,赋值为常数
SIG_DFL表⽰执⾏系统默认动作,赋值为⼀个函数指针,表⽰⽤⾃定义函数捕捉信号,或者说向内核注册了⼀个信号处理函 数,该函数返回值为 void ,可以带⼀个 int 参数,通过参数可以得知当前信号的编号,这样就可以⽤同⼀个函数处理多种信号。显然,这是⼀个回调函数,不是被main函数调⽤,⽽是被系统所调⽤。
- pause
原型
#include <unistd.h>
int pause(void);
功能 : pause函数使调⽤进程挂起直到有信号递达
如果信号的处理动作是终⽌进程,则进程终⽌,pause函数没有机会返回
如果信号的处理动作是忽略,则进程继续处于挂起状态,pause不返回
如果信号的处理动作是捕捉,则调⽤了信号处理函数之后pause返回-1,errno设置为EINTR, 所以pause只有出错的返回值
可重⼊函数
- 若一个函数被不同的控制流程调用,有可能在第一次调用还没有返回的时候就再次进入了该函数,这称为重入
- 如果该函数访问一个全局链表,有可能因为重入而造成错乱,这样称为不可重入
- 如果一个函数只访问自己的局部变量或参数,称为可重函数
注:如果一个函数符合以下条件之一,则不可重入:
1.调用了malloc和free,因为malloc也是用全局链表来管理堆的。
2.调用了标准I/O库函数,I/O库中好多的实现都是以不可重入的方式使用全局数据结构。
volatite : 保证内存的可见性,始终从内存中查看。
SIGCHID信号
⼦进程在终⽌时会给⽗进程发 SIGCHLD 信号,该信号的默认处理动作是忽略⽗进程可以⾃定义 SIGCHLD 信号 SIGCHLD 信号的处理函数,这样⽗进程只需专⼼处理⾃⼰的⼯作,不必关⼼⼦进程了,⼦进程终⽌时会通知⽗进程,⽗进程在信号处理函数中调⽤ wait 清理⼦进程即可。
Linux 下,想不产⽣僵⼫进程还有另外⼀种办法:
⽗进程调⽤ sigaction 将 SIGCHLD 的处理动作置为 SIG_IGN ,这样 fork 出来的⼦进程在终⽌时会⾃动清理掉,不会产⽣僵⼫进程,也不会通知⽗进程。
测试代码:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void handler(int sig)
{
pid_t id;
while( (id = waitpid(-1, NULL, WNOHANG)) > 0){
printf("wait child success: %d\n", id);
}
printf("child is quit! %d\n", getpid());
}
int main()
{
signal(SIGCHLD, handler);
pid_t cid;
if((cid = fork()) == 0){//child
printf("child : %d\n", getpid());
sleep(3);
exit(1);
}
while(1){
printf("father proc is doing some thing!\n");
sleep(1);
}
return 0;
}
结果说明: