进程间通信IPC(二)信号

目录

 

1 信号的概念

特点:

信号机制:

信号特质:

信号产生方式:

信号状态:

信号的默认处理方式:

信号的四要素:

常见信号:

2 阻塞信号集与未决信号集

3 信号的产生

按键产生

硬件异常产生

命令与系统调用产生

kill函数

raise函数

abort函数

软件条件产生/时钟产生信号

alarm函数

4 信号集的函数

信号集处理函数

阻塞信号集函数

未决信号集函数

5 打印未决信号集

6 信号捕捉

sigaction函数

sigaction捕捉信号案例

信号捕捉特性

7 内核实现捕捉过程

8 用SIGCHLD信号回收子进程

第一版代码:

第二版代码:

第三版代码:


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捕捉信号案例

信号捕捉特性

信号屏蔽字中的信号会阻塞,阻塞会导致该信号一直处于未决状态,直到解除阻塞才会处理该信号,处理信号可以用默认处理动作,也可以用自定义的动作函数,当使用自定义动作处理函数的时候,就称为信号的捕捉。注意屏蔽并不是忽略,忽略只是解除屏蔽之后一种可能的处理动作。

  1. 捕捉信号时候,如果自定义的动作处理函数处理时间很长,在函数执行期间不使用PCB中的信号屏蔽字,而是使用信号捕捉函数sigaction中的sa_mask来指定动作处理函数执行期间的屏蔽字。
  2. x信号的捕捉函数执行期间,x信号自动被屏蔽,也就是说,如果x信号的捕捉函数正在执行,又来了一个x信号,则该信号阻塞,当当前捕捉函数执行完毕会再执行一次捕捉。
  3. 阻塞的常规信号不支持排队,产生多次信号只记录一次。但是32个实时信号支持排队。

 

 

捕捉函数处理期间屏蔽两种信号,一个是捕捉的信号本身,一个是sa_mask指定的信号。

 

7 内核实现捕捉过程

主函数执行过程因为中断、异常或系统调用进入内核

内核先处理当前进程中可以递送的信号(在PCB中)

如果是默认处理动作,直接在内核中完成

如果用户定义了捕捉函数,那么由内核态回到用户态,执行自定义动作函数,处理完成返回内核

内核再返回主函数中上次中断的位置继续向下执行

8 用SIGCHLD信号回收子进程

子进程暂停或者终止的时候会发送这个信号

默认处理动作是忽略

我们可以通过捕捉SIGCHLD信号来回收子进程,就不用wait在那里等待回收了

 

案例:用SIGCHLD回收多个子进程

第一版代码:

因为子进程分别sleep了i秒,所以能依次得到回收,如果子进程都没有sleep呢?可能发生在捕获函数执行期间很多个子进程同时终止,当捕捉第一个SIGCHLD信号时候,捕捉函数执行期间其它SIGCHLD信号是自动屏蔽的,而且阻塞信号没有排队机制。

 

如下图,会发生回收不全的问题,当然,至少能回收到两个子进程。也就是说,如果只有两个子进程,是一定能被回收的,因此即使在一个被处理的时候一个被屏蔽,当第一个处理完成第二个就会解除屏蔽随后被处理。(这里没有考虑一种极端的情况,即父进程捕捉信号之前两个进程都结束了,也就是说sigaction函数还没执行两个子进程就终止了,此时可能会用自动处理动作,把信号忽略掉,这样就会都变成僵尸进程)

第二版代码:

 

解决上一版代码中的问题,如果在一个捕捉函数执行期间又有其它子进程终止

可以在自定义动作函数中设置只要收到一个信号,就一直不退出动作函数,而是一个调用waitpid回收子进程,直到没有子进程可回收,这样在动作函数执行期间终止的子进程都能被回收到。

第三版代码:

如果sigaction捕捉函数执行之前,子进程就已经全部终止,则sigaction还没有捕捉到信号,信号就被默认处理动作给忽略了,因此造成僵尸进程的存在。

解决方法,其实只要捕捉函数能捕捉到一个信号就足够了,这样在catch_sig函数中就能回收所有的子进程。

 

所以只需要在子进程死之前把信号给屏蔽掉,然后等执行到捕捉函数之后再解除屏蔽来处理,这样就不会被忽略导致捕捉函数没有捕捉到。

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值