信号
信号概念
通知进程发生了某件事情导致了软件中断----会打断当前的阻塞操作,去处理事情
信号的种类
信号有不同的种类,每个信号都对应了不同的事件
通过命令行kill -l
可以查看信号的种类
在图中可以看到这些都是信号的种类,总共有62个种信号
1-31---->每个都有各自对应的事件 非可靠信号/非实时信号
34-64---->后续添加的信号 可靠信号/非可靠信号
信号的生命周期:产生----->注册(注册在进程当中)----->注销(在进程中注销)----->处理
信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。
信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。
同步和异步
我自己也是在写的过程中才发觉不了解同步与异步,所以也补充一下
同步:就是发出一个功能调用时,在没有得到结果之前,该调用就不返回或继续执行后续操作
异步:当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作。当这个调用完成后,一般通过状态、通知和回调来通知调用者。对于异步调用,调用的返回并不受调用者控制
信号的产生
硬件产生:ctrl+c,ctrl+|,ctrl+z。通过键盘强行关闭,比如在一个无限循环中,使用键盘组合按键将该进程强行停掉。
软件产生:kill -signo pid 命令 产生一个信号
比如利用while写上一个死循环程序
查询到进程的pid,利用软件产生的方法杀死这个进程
当杀死之后就可以看到程序终止
int kill(pid_t pid,int sig)
给指定进程发送指定信号
int raise(int sig)
给调用进程发送信号
这两个函数都是成功返回0,错误返回-1。
void abort(void)
给调用进程发送SIGABRT
就像exit函数一样,abort函数总是会成功的,所以没有返回值。
unsigned int alarm(unsigned int sconds);
sconds秒之后给调用进程发送SIGALRM信号
返回值上一个定时器剩余的时间或0,参数为0,取消定时器,返回剩余的时间
可以结合 sleep() 来使用
信号的注册
信号的记录在pcb中。在操作系统中有一个结构体叫sigset_t{}
,它用于表示阻塞和未决的数据类型。sigset称为信号集,表示每个信号的“有效”或“无效”状态。
如图,在pcb中专门有一个pending结构体存储当前收到的信号,还有一个结构体block结构体专门存储现在都有哪些信号要被阻塞。handler是用户自定义信号的处理方式
信号注册
通过修改pending未决信号集合(位图)中对应信号位+添加信号sigqueue节点
sigqueue是信号发送函数
int sigqueue(pid_t pid, int sig, const union sigval val)
调用成功返回 0;否则,返回 -1。
sigqueue()是比较新的发送信号系统调用,主要是针对实时信号提出的(当然也支持前32种),支持信号带有参数,与函数sigaction()配合使用。
sigqueue的第一个参数是指定接收信号的进程ID,第二个参数确定即将发送的信号,第三个参数是一个联合数据结构union sigval,指定了信号传递的参数,即通常所说的4字节值。
非可靠信号注册:判断信号集合位图相应位是否为1;若为0,为信号组织sigqueue节点添加到链表中,并且pending位图置1;若为1(信号已经注册过还没有被处理),则什么都不做(等于丢弃)。
判断是否有相同的未决信号,若有,则什么也不做(事件丢失);否则,修改位图(sigset_t),添加节点
可靠信号注册:不管信号是否为1,阻止节点,添加到sigqueue链表中,并且位图置1(信号不会丢弃)
判断是否有相同未决信号,若没有,修改位图(sigset_t),添加到节点,否则直接添加节点
可靠信号的注册跟上图的模式是相似的,只是不管是否为1,都阻止节点
信号的注销
删除信号的sigqueue节点,并且修改pending位图。
非可靠信号的注销:因为非可靠信号的信号节点只有一个,因此删除节点,位图直接置0。
直接删除节点,修改位图(非可靠信号只会注册一次)
可靠信号的注销:因为可靠信号的信号节点可能会有多个,若还有相同信号节点,则位图依然置1,否则置0
删除节点,判断是否还有相同节点。若有,则位图依然置1,否则修改位图置0
也可以这样理解,只要存在相同的信号节点,那么位图永远置1。只要没有相同的,则置为0
信号的处理
信号的处理有三种方式:
默认处理方式
操作系统(内核)既定的处理方式----SIG_DFL,可能是以下的某种类型:
Treminate:进程被终止(杀死)
Dump:进程被终止(杀死),如果可能,创建包含进程执行上下文的核心转储文件(core dump)
Ignore:信号被忽略
Stop:进程被停止,即把进程置为TASK_STOPPED状态
Continue:如果进程被禁止,就把它设置为TASK_RUNNING状态
忽略处理方式
这与默认中的忽略是不同的,该处理方式处理了信号,但是什么都没有做。
自定义处理方式
通过调用相应的信号处理函数捕捉信号。
注意!!
注意!!
注意!!
有两个信号SIGKILL -9 SIGSTOP -19 无法被阻塞,无法自定义,无法被忽略
信号的捕捉初识
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signum: 信号编号
handler: 函数指针
SIG_DFL 默认处理方式
SIG_IGN 忽略处理方式
使用handler函数替换signum信号的处理方式
我们利用man手册来了解该函数的用处
函数的作用是:从三种方式中选择一种接收信号sig的方式。如果func的值是SIG_DFL,则将对该信号进行默认处理。如果func的值为SIG_IGN,则忽略该信号。否则,应用程序应确保func指向信号发生时要调用的函数。由于信号而调用这样一个函数,或者(递归地)调用该调用的任何其他函数(标准库中的函数除外),称为“信号处理程序”。
#inlcude <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
void sigcb(int signo){
printf("---------\n");
}
int main(){
signal(SIGINT, sigcb);
while(1){
printf("hello ~~~"\n);
sleep(2);
}
}
通过调用signal函数来改变信号的处理方式。平时使用ctrl+c
将强制退出软件,但是当这次使用将不在执行
SIGINT: 程序中止信号,在用户键入INTR字符(通常是Ctrl+C)时发出。
此时信号的处理将SIGINT改变成打印一段---------,所以最后退出用了ctrl+|
才得以退出
信号自定义处理方式的流程
用户态切换到内核态运行,完毕后准备从内核态切换到用户态运行的时候,去处理信号。若信号为默认/忽略处理,则在内核中直接完成,但是如果是自定义处理方式,则需要返回用户态执行信号回调函数,完毕后回到内核态,没有信号了则回到程序主流程
还有一个处理函数
int sigaction(int sig, const struct sigaction *restrict act,struct sigaction *restrict oact)
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);
};
函数的作用是:允许调用过程检查指定与特定信号相关联的操作
使用act动作替换原有的处理动作,并且将原有处理动作拷贝到oldact中。不过这个需要等一下再说
信号的阻塞
信号的一些其他概念
信号递达:实际执行信号的处理动作
信号未决:信号从产生到递达之间的状态
信号阻塞概念
暂时阻止信号被递达(处理)。信号依然可以注册,只是暂时不处理,解除阻塞之后才会处理
信号阻塞的过程
在pcb的block-信号阻塞集合中(程序初始化的时候)标记要阻塞的信号,到来之后暂不处理,将blocked位图集合中对应的位-置1,表示阻塞这个信号,直到被解除阻塞(从block集合中移除)
信号阻塞的接口
sigprocmask:阻塞/解除阻塞信号
sigprocmask()用于获取 (与/或) 更改调用线程的信号掩码。信号掩码是一组信号,它的传递目前被调用方阻塞
SET_BLOCK 将信号添加到阻塞集合中 block = block | set
SET_UNBLOCK 将信号从阻塞集合中移出 block = block & ~set
SET_SETMASK 被阻塞的信号集被设置为参数集。block = set
sigemptyset:清空信号集合
sigfillset:向集合中添加所有信号
sigaddset:向集合中添加指定的信号
sigismember:判断信号是否在集合中
sigdelset:从集合中移除执行信号
sigpending:后获取未决信号
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
struct sigaction act, oldact;
void sigcb(int signo){
printf("revc signo:%d\n", signo);
sigaction(signo, &oldact, NULL);
}
int main(){
act.sa_handler = sigcb;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(SIGINT, &act, &oldact);
while(1){
printf("---------\n");
sleep(2);
}
return 0;
}
接着之前的另一个处理信号的方法,先将sigaction中参数结构的成员都设置完,之后将信号集合中的信号全部清空。act动作是handler自定义函数,这时候替换了ctrl+c
操作。在sigcb函数中再次调用sigaction,此时act动作将继续执行SIGINT。执行结果如上图
可重入和不可重入函数
竞态条件
在并发编程中,这种由于不恰当的执行时序而出现不正确的结果是一种非常严重的情况。因为运行时序而造成的数据竞争,导致数据的二义性。这样的情况,我们称为竞态条件。
函数的重入和不可重入
一个函数是否可以在多个运行时序中重复调用而不会出现任何问题,在一个函数中是否进行了对全局数据的非原子性操作。
**重入函数:**在多个时序的运行中重复调用,不会造成异常影响。比如在某个函数的调用中,还没有返回时就再次进入该函数,就称为重入。
**不可重入函数:**不能再多个时序中重复调用,比如malloc和free函数。如果随意的调用则会发生内存泄漏。
SIGCHLD信号
子进程退出,通知父进程的信号。
在僵尸进程中,我们之前设置的是通过wait/waitpid函数去设置怎么避免僵尸进程产生,但是这样父进程就必须一直等待,无法做其他的事情了。所以这时候采用信号机制,非阻塞地查询是否有子进程结束等待清理。
在子进程终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait函数清理子进程即可。
用户需要循环非阻塞处理子进程退出
sigcb(){
while(waitpid(-1, NULL, WONOHANG) > 0);
}
因为SIGCHLD信号是一个非可靠信号,大量子进程同时退出的情况下,有可能丢失事件,因此要在一次调用中,把能处理掉的僵尸进程都处理掉。
代码测试
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <wait.h>
#include <unistd.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;
}