目录
1 信号的概念
也是一种进程间通信方式
特点:
简单,携带信息量小,满足某个特定条件才发送
信号机制:
A进程给B进程发送信号,B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停执行去处理信号,处理完毕再继续执行。处理信号可以是忽略信号,或者捕获信号待会处理,或者马上去处理。信号是软件层面实现的中断,早期被称为“软中断”。需要注意的是,虽然说是由进程A发送信号给进程B,但所有信号,都是由内核发送,内核处理。
信号特质:
由于信号通过软件方法实现,其实现手段导致信号有很强的延时性,但对于用户来说,这个延迟时间非常短,不易察觉。
信号产生方式:
按键产生:crtl C, crtl z, crtl \
系统调用产生:kill raise abort
软件条件产生:定时器alarm
硬件异常产生:非法访问内存(段错误)、除0、内存对齐出错(总线错误),SIGPIPE(管道通信时候读端全部关闭时候产生的信号)
命令产生:kill命令等
信号状态:
产生
递达 信号到达并且处理完
未决 信号被阻塞了
注意递达与未决的异同,不管是递达还是未决,此时进程都已经接收到了信号,如果进程把该信号处理了,那么该信号的状态就变成了递达,如果虽然接收到信号但是还没处理,那么就是未决。未决并不是没有接收到信号的意思,因为信号是由内核产生的,因此可以认为信号产生之后每个进程都能接收到信号。
信号的默认处理方式:
执行默认动作
捕获 学习信号主要目的就是为了捕获信号,比如像段错误之类的硬件异常信号会导致程序异常终止,学习捕获是为了处理异常让程序继续执行
信号的四要素:
编号
事件
名称
默认处理动作 忽略 终止 终止+core 暂停 继续
常见信号:
从左到右四列分别对应了四要素中的名字,编号,默认动作,事件
常见信号31个,32到64是嵌入式时候用的实时信号
2 阻塞信号集与未决信号集
Linux内核的简称控制块PCB是一个结构体,task_struct结构体,除了包含进程id,状态,工作目录,用户id,组id,文件描述符表,还包含了信号相关的信息,即每个进程各自的阻塞信号集与未决信号集。
阻塞信号集与未决信号集本质就是两个位图,但不是直接的位图,而是用sigset_t结构体来定义出来的位图。
阻塞信号集:将某一位设置为1即将这个信号加入了阻塞信号集,当进程接收到这个信号后不会去处理它,直到解除对该信号的屏蔽才会处理。
未决信号集:
3 信号的产生
按键产生
硬件异常产生
命令与系统调用产生
kill函数
是系统API产生信号
函数原型:
int kill(pid_t pid , int sig);
如果pid > 0, 发送sig编号信号给pid进程
如果pid = 0, 发送给pid进程组内所有进程
如果pid = -1, 发送给所有有权限发送的进程(不包括init进程)
如果pid < -1, 发送给-pid进程组内所有进程
如果sig = 0,不发送信号,但是可以用来检查pid进程(组)是否存在或者当前进程有没有权限给pid进程发送信号。
例子:父进程生成五个子进程,然后让2号进程杀死父进程
raise函数
给自己发信号
#include <signal.h>
int raise(int, sig);
杀死自己例子:
abort函数
也是自己给自己发信号,跟raise不同,人raise至少还能选择一下发哪一个信号,abort是默认直接给自己发SIGABRT这个信号。
软件条件产生/时钟产生信号
alarm函数
也是一个系统api
在seconds秒后给自己发送一个信号
该信号是SIGALRM,默认动作是终止进程
返回值:上一次设定的闹钟还有多长时间触发,比如上一次我们设置了一个20秒的闹钟,然后到第7秒的时候我们等不下去了要重新设置一个,那么重新设置这次的返回值就是14,因为上一次设置的闹钟还有14秒就要出发了,当然,第一次设置的闹钟返回值一定是0。
如果传入参数为0,代表取消所有已经设置的闹钟
例子:6秒后杀死自己
setitimer函数
可以周期性发送信号
函数原型:
#include <sys/time.h>
// int getitimer(int which, struct itimerval *curr_value);
int setitimer(int which, const struct itimerval *new_value,
struct itimerval *old_value);
which有三种选择,不同选择对应不同信号:
ITIMER_REAL 自然计时法 SIGALRM
ITIMER_VIRTUAL 进程执行时间 SIGVTALRM
ITIMER_PROF 进程执行时间+调度时间 SIGPROF
new_value:
是itimerval结构体,第一个成员it_interval设置发送信号的周期,it_value设置第一次发送信号的延时。
old_value:
一般不用,直接设置为NULL,是上一次调用setitimer时候的new_value值
itimerval 结构体定义:
struct itimerval {
struct timeval it_interval; /* Interval for periodic timer */
struct timeval it_value; /* Time until next expiration */
};
timeval结构体定义:
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
其中tv_sec是秒数,tv_usec是微秒数
返回值:成功返回0,失败返回-1
例子:
先看一下用于捕获的signal函数
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
第一个参数signum是要捕获的信号编号
第二个参数是一个函数指针,用来捕获以后进行处理,该函数返回值是void,参数是int
4 信号集的函数
首先阻塞信号集和未决信号集都可以理解为位向量,但是我们不能简单直接得通过操作某一位来改变,而应该使用特定的系统调用。内核通过读取未决信号集来判断信号是否应被处理,信号屏蔽字mask(即阻塞信号集)可以影响未决信号集。
可以在应用程序中自定义set来改变mask,达到屏蔽指定信号的目的。
信号集处理函数
清空信号集(全变成0)
int sigempty(sigset_t *set);
填充信号集(全变成1)
int sigfillset(sigset_t *set);
添加某个信号到信号集
int sigaddset(sigset_t *set, int signum);
signum为信号编号
从集合中删除某个信号
int sigdelset(sigset_t *set, int signum);
以上四个函数成功返回0,失败返回1
判断是否是集合里的成员
int sigismember(const sigset_t *set, int signum);
是集合中成员返回1,不是返回0,函数出错返回-1
阻塞信号集函数
设置阻塞或者解除阻塞信号集
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how:
SIG_BLOCK 设置阻塞,应该是把set对应的位设置阻塞
SIG_UNBLOCK解除阻塞
SIG_SETMASK 把set设置为新的阻塞信号集
set:
传入的信号集
oldset:
旧的信号集,是一个传出参数,用来获取改变之前的阻塞信号集
未决信号集函数
获取未决信号集
int sigpending(sigset_t *set);
set:
传出参数,用于获取当前的未决信号集
5 打印未决信号集
6 信号捕捉
能防止进程意外死掉
在第3节信号的产生中已经使用了一个signal函数用来信号捕捉,不过由于signal这个单词常常有特定的含义,因此一般不用这个函数,而是用另外一个,sigaction
sigaction函数
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
signum: 要捕捉的信号编号(或者宏)
act: 传入的动作,不过这里不是一个直接的函数指针了,而是一个sigaction结构,结构的成员中有函数指针。结构定义在下边。
oldact: 传出参数,传出原动作
返回值:成功返回0失败-1
struct sigaction{
void (*sa_handler)(int); // 与signal函数一样的捕捉函数
void(*sa_sigaction)(int, siginfo_t *, void *); // 一般不用,有点麻烦
sigset_t sa_mask; // 执行捕捉函数期间,临时屏蔽信号集
int sa_flags; // 一般填0,这个时候用第一个函数指针,SA_SIGINFO用第二个指针
void (*sa_restorer)(void); // 无效参数
};
sigaction捕捉信号案例
信号捕捉特性
信号屏蔽字中的信号会阻塞,阻塞会导致该信号一直处于未决状态,直到解除阻塞才会处理该信号,处理信号可以用默认处理动作,也可以用自定义的动作函数,当使用自定义动作处理函数的时候,就称为信号的捕捉。注意屏蔽并不是忽略,忽略只是解除屏蔽之后一种可能的处理动作。
- 捕捉信号时候,如果自定义的动作处理函数处理时间很长,在函数执行期间不使用PCB中的信号屏蔽字,而是使用信号捕捉函数sigaction中的sa_mask来指定动作处理函数执行期间的屏蔽字。
- x信号的捕捉函数执行期间,x信号自动被屏蔽,也就是说,如果x信号的捕捉函数正在执行,又来了一个x信号,则该信号阻塞,当当前捕捉函数执行完毕会再执行一次捕捉。
- 阻塞的常规信号不支持排队,产生多次信号只记录一次。但是32个实时信号支持排队。
捕捉函数处理期间屏蔽两种信号,一个是捕捉的信号本身,一个是sa_mask指定的信号。
7 内核实现捕捉过程
主函数执行过程因为中断、异常或系统调用进入内核
内核先处理当前进程中可以递送的信号(在PCB中)
如果是默认处理动作,直接在内核中完成
如果用户定义了捕捉函数,那么由内核态回到用户态,执行自定义动作函数,处理完成返回内核
内核再返回主函数中上次中断的位置继续向下执行
8 用SIGCHLD信号回收子进程
子进程暂停或者终止的时候会发送这个信号
默认处理动作是忽略
我们可以通过捕捉SIGCHLD信号来回收子进程,就不用wait在那里等待回收了
案例:用SIGCHLD回收多个子进程
第一版代码:
因为子进程分别sleep了i秒,所以能依次得到回收,如果子进程都没有sleep呢?可能发生在捕获函数执行期间很多个子进程同时终止,当捕捉第一个SIGCHLD信号时候,捕捉函数执行期间其它SIGCHLD信号是自动屏蔽的,而且阻塞信号没有排队机制。
如下图,会发生回收不全的问题,当然,至少能回收到两个子进程。也就是说,如果只有两个子进程,是一定能被回收的,因此即使在一个被处理的时候一个被屏蔽,当第一个处理完成第二个就会解除屏蔽随后被处理。(这里没有考虑一种极端的情况,即父进程捕捉信号之前两个进程都结束了,也就是说sigaction函数还没执行两个子进程就终止了,此时可能会用自动处理动作,把信号忽略掉,这样就会都变成僵尸进程)
第二版代码:
解决上一版代码中的问题,如果在一个捕捉函数执行期间又有其它子进程终止
可以在自定义动作函数中设置只要收到一个信号,就一直不退出动作函数,而是一个调用waitpid回收子进程,直到没有子进程可回收,这样在动作函数执行期间终止的子进程都能被回收到。
第三版代码:
如果sigaction捕捉函数执行之前,子进程就已经全部终止,则sigaction还没有捕捉到信号,信号就被默认处理动作给忽略了,因此造成僵尸进程的存在。
解决方法,其实只要捕捉函数能捕捉到一个信号就足够了,这样在catch_sig函数中就能回收所有的子进程。
所以只需要在子进程死之前把信号给屏蔽掉,然后等执行到捕捉函数之后再解除屏蔽来处理,这样就不会被忽略导致捕捉函数没有捕捉到。