【Linux】进程间通信之信号篇

今天我们来看进程间通信中唯一的异步通信机制-----> 信号
我们之前看过信号量,信号量的本质是一个计数器;千万不要跟今天的信号搞混,Linux中的信号是向进程异步发送的事件通知,通知进程有事件(硬件异常、程序执行异常、外部发出信号)发生。
进程间可以相互发送信号,内核也可能在内部发送信号。 当信号产生时,内核向进程发送信号(在进程所在的进程表项的信号域设置对应于该信号的位图的比特位为0或者为1) ,由于每个信号只保存为一位,因此不能对给定类型的信号进行排队。。内核处理一个进程收到的信号的时机是在一个进程从内核态返回到用户态时,当一个进程在内核态运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理,进程只有处理完信号才会返回用户态,进程在用户态下不会有未处理完的信号。内核为每个进程维护一个(未处理)的信号队列,信号产生后首先被放入到未决队列中,如果进程选择阻塞信号,那么如果某个进程发生多次,未决信号发生多次,未决队列中进仅保留相同的信号(不可靠的信号)中的一个,而可靠信号则会被保留。
kill -l 命令查看系统定义的信号列表,每个信号都有一个编号和一个宏定义名称,这些宏定义在signal.h中
这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明,命令:man 7 signal
信号产生的方式:
1、用户在终端按下某些键时,终端驱动程序会发送信号给前台进程,例如Ctrl-C产生SIGINT信号,Ctrl-\产生SIGQUIT信号,Ctrl-Z产生SIGTSTP信号(可使前台进程停止)。
2、硬件异常产生信号,这些条件由硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,,MMU会产⽣生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
3、一个进程调用kill(2)函数可以发送信号给另一个进程。 可以用kill(1)命令发送信号给某个进程,kill(1)命令也是调用kill(2)函数实现的,如果不明确指定信号则发送SIGTERM信号,该信号的默认处理动作是终止进程。当内核检测到某种软件条件发生时也可以通过信号通知进程,例如闹钟超时产生SIGALRM信号,向读端已关闭的管道写数据时产生SIGPIPE信号。 如果不想按默认动作处理信号,用户程序可以调用sigaction(2)函数告诉内核如何处理某种信号。
4、由软件条件产生
处理信号的方式(简单记忆:忽略、默认、自定义)
1、忽略此信号;
2、执行默认处理动作;
3、提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉一个信号;
信号的产生
(1)通过终端按键产生信号
(2)通过系统函数向进程发送信号
(3)由软件条件产生信号
接下来,我们就来一一看一下信号是怎么产生的emm~~~
1、通过终端按键产生信号
我们前面也说了Ctrl-C产生SIGINT信号,Ctrl-\产生SIGQUIT信号,Ctrl-Z产生SIGTSTP信号(可使前台进程停止);
SIGINT的默认处理动作是终⽌止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump,接下来我们来看一下~
什么是Core Dump?
当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。
进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。
一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。
下面我们来模拟一下Core Dump,并用gdb调试Core Dump错误
ulimit命令改变了Shell进程的Resource Limit,test进程的PCB由Shell进程复制而来,所以也具有和Shell进程相同的Resource Limit值,这样就可以产生Core Dump了。
2、调用系统函数向进程发信号
首先我们体验一下kill命令发送信号
【注】指定发送某种信号的kill命令可以有好多种写法,上面的命令还可以携程kill -SIGSEGV 4568或者kill -11 4568,11是SIGSEGV的编号。以前我们遇到过的段错误都是内存非法访问造成的,而我嗯这个程序本身没错,给她发送SIGSEGV也能产生c段错误。
那我们就该了解kill的底层实现~
kill命令是调用kill函数实现的,kill函数可以给一个指定的进程发送指定的信号;
#include<signal.h>
int kill(pid_t pid,int signo);
//成功返回0,失败返回-1
raise函数可以给当前进程发送指定的信号(自己给自己发信号);
#include<signal.h>
int raise(int signo);
//成功返回0,失败返回-1
abort函数使当前进程接收到信号而异常终止;
#include<stdio.h>
voif abort(void);
//就像exit函数一样,abort函数总是会成功的,所以没有返回值
3、由软件条件产生信号
之前我们在【管道篇】有介绍过SIGPIPE信号,它是一种由软件产生的信号(具体了解请戳 点击打开链接
今天我们来介绍SIGALRM信号和alarm函数~
alarm函数
#include<unistd.h>
unsigned int alarm(unsigned int seconds);
功能:可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号,该信号的默认处理方式时终止当前进程;
参数:设定闹钟响的时间,用秒数描述,alarm(0)-->表示取消以前设定的闹钟
返回值:为0或者以前设定的闹钟时间还剩下的秒数。例如某个人要小水一会,原设定闹钟30分钟之后响,结果睡到20分钟的时候被人吵醒,醒来之后发现还想再睡,于是重新设定闹钟为20分钟之后响,那么之前设定的闹钟时间剩下还有10分钟。如果alarm(0),函数的返回值仍然是以前设定的时间所余下的时间。
阻塞信号
我们先来看一组概念~
信号递达:实际执行信号的处理动作
信号未决:信号从产生到递达之间的状态
信号阻塞:进程可以选择阻塞某个信号,被阻塞的信号产生时将保持在未决状态,直到进程解除阻塞,才能执行递达动作
注意:信号阻塞和信号忽略不一样---->只要信号被阻塞就不会递达,忽略则是在信号递达之后的一种处理动作
信号在内核中的示意图
从图中可以看到
(1)每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作;
(2)信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
(3)如果在进程解除对某个信号的阻塞之前这种信号产生过多次,将如何让处理呢?
---->POSIX.1:允许系统递送该信号依次或者多次
----->Linux:常规信号在递达之前产生多次只记一次;实时信号在递达之前产生多次可以放在一个队列中
(4)每个信号只有一个bit的未决标志,非0即1,阻塞标志同理;因此未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为 信号集 ,这个类型可以表示每个信号的“有效”或者“无效”状态,在阻塞信号集中“有效”和“无效”的含义是信号是否被阻塞,在未决状态的“有效”和“无效”表示信号是否处于未决状态。其中阻塞信号集也叫做信号屏蔽字(Signal Mask)
下来我们就来看一下信号集操作函数
(1)先来看一组函数
#include<signal.h>//头文件
int sigemptyset(sigset_t *set);//初始化信号集 成功返回0 出错返回-1
int sigfillset(sigset_t *set);//初始化信号集 成功返回0 出错返回-1
int sigaddset(sigset)t *set,int signo);//添加某种有效信号 成功返回0 出错返回-1
int sigdelset(sigset_tt *set,int signo);//删除某种有效信号 成功返回0 出错返回-1
int sigismember(const sigset_t *set,int signo);//用来测试参数signo代表的信号是否已加入至参数set信号集里。若已在返回1,不在返回-1
sigemptyset函数和sigfillset函数的区别
(2)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可选下面值
(3)sigpending函数
功能:读取当前进程的未决信号集
函数原型:
#include<signal.h>
int sigpending(sigset_t *set);
//通过set参数传出当前进程的未决信号集
//调用成功返回0 出错返回-1
【代码验证】
【运行结果】
信号的捕捉
【图解】
什么是捕捉信号?--->如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,被称为捕捉信号
如图,信号处理函数的代码是在用户空间的,处理过程比较复杂(速记,正无穷符号)。
例如:用户程序注册了SIGQUIT信号的处理函数sighandler,这里发生四次用户到内核之间的切换(速记:正无穷中间画条线偏下,与线的交点)
第一次:当前正在执行main函数,这是发生中断或异常切换到内核态;
第二次:在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达,内核决定返回用户态后执行sighandler函数,(而不是恢复main函数的上下文继续执行);
第三次:sighandler函数返回后自动执行特殊的系统调用sigreturn在此进入内核态;
第四次:这时候没有新的信号要递达,这次再返回用户态恢复main函数的上下文继续执行。
sigaction函数
功能:读取和修改与制定信号相关联的处理动作
函数原型
#include<signal.h>
int sigaction(int signo,const struct sigaction *act,struct sigaction *oldact);
//成功返回0,失败返回-1
参数:signo是指定信号的编号;若act指针非空,则根据act修改该信号的处理动作;若oldact指针非空,则通过oldact传出该信号原来的处理动作;act和oldact指向sigaction结构体
struct sigaction类型用来描述对信号的处理,定义如下:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
在这个结构体中。。。。。。[了解即可]
成员sa_handler是一个函数指针,其含义与signal函数中的信号处理函数相似
成员sa_sigaction则是另一个信号处理函数,他有三个参数,可以获得关于信号的更详细的信息。
当sa_flags成员的值包含了SA_SIGINFO标志是,系统将使用sa_sigaction函数作为信号处理函数,否则使用sa_handler作为信号处理函数。在某些系统中,sa_handler和sa_sigacion被放在联合体中,因此使用时不要同时设置;
sa_mask成员用来指定处理函数执行期间需要被屏蔽的信号,特别是当某个信号被处理时,它自身会被自动放入进程的信号掩码,因此在信号处理函数执行期间这个信号不会再度发生
sa_flags成员用来指定信号处理的行为,它可以是一个值的“”按位或“组合”
SA_RESTART:使被信号打断的系统调用自动重新发起
SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会受到SIGCHLD信号(后面详细介绍)
SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到SIGCHID信号,这是子进程如果退出也不会成为僵尸进程
SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号
SA_RESETHAND:信号处理之后重新设置为默认的处理方式
SA_SIGINFO:使用sa_sigaction成员而不是sa_handler作为信号处理函数
re_restorer成员是一个已经废弃的数据域,不要使用
【注】将sa_sigaction赋值为常数SIGIGN传给sigaction表示忽略信号,赋值为SIG_DEL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数的返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。(这个函数是一个回调函数,不是被main函数调用,而是系统所调用)
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号在此产生,那么它会被阻塞到当前进程处理结束为止
pause函数
#include<unistd.h>
int pause(void);
功能:使调用进程挂起直到有信号递达,如果信号的处理动作是终止进程,则进程终止,pause函数没有机会返回;如果信号的处理动作是忽略,则进程继续处于挂起状态,pause不返回;如果信号的处理动作是捕捉,则调用了信号处理函数之后pause返回-1,而热闹哦设置为EINTR,所以pause只有出错的返回值(EINTR表示被信号中断)
可重入函数
什么是可重入函数?
可重入函数主要用于多任务环境中,简单点理解就是可以被中断的函数,也就是说,在这个函数的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;
什么是不可重入函数?
在实时系统中,经常会出现多个任务调用同一个函数的情况。如果这个函数被设计成为不可重入函数,那么不同人物调用这个函数可能修改其他任务用到的数据,从而导致不可预料的结果。
满足下列条件的函数多数是不可重入的
(1)函数体内使用了静态的数据结构
(2)函数体内调用了malloc() 或者free()函数
(3)函数体内调用了标准I/O函数
SIGCHID信号
我们之前用wait和waitpid函数清理僵尸进程,附近策划过呢可以阻塞等待子进程结束,也可以非阻塞的查询是否有子进程结束等待清理(也就是轮询方式),采用了第一种方式,父进程阻塞了就不能处理自己的工作了,采用第二种,父进程在处理自己的工作的同时还要时不时的轮询一下。
其实,子进程在终止时会给父进程发SIGCHILD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHILD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可
那么SIGCHILD信号产生的条件总结如下:
(1)子进程终止时
(2)子进程收到SIGSTOP信号停止的时候
(3)子进程处在停止状态,接收到SIGCONT后唤醒




  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值