什么是信号?
- 信号是软件中断,信号通知进程发生了某个事件,打断进程当前正在进行的操作,去处理这个信号(应对对应的事件),这种机制为正在运行进程提供了一种处理异步事件的方法,而异步的执行方式因为不需要等待事件的完成再去执行接下来的操作,极大地提升了程序的执行效率;
信号种类
- 既然信号有那么多产生的场景,那么也就需要不同种类的信号来对事件类型加以区分,在CentOS上,我们可以使用kill -l 来查看系统所能够识别的所有信号种类:
- 这里共有62种信号,其中1~31号是继承于Unix而来的,这些信号都是(非实时信号/不可靠信号)这写信号是在Unix里已经定义好的,对应指定事件的信号,通常每个都有自己的默认动作,但这写信号不支持同类信号排队,最大的问题就是可能会丢失(信号发生了进程却一直不知道,发了多次却只收到一次)这就是它不可靠的地方,而后31种信号则是Linux更改了信号机制产生的可靠,实时信号,这些信号支持被定义,同类信号排队,并且不会丢失;
信号的处理流程
- 当进程接收一个信号时,有信号产生—信号注册—信号阻塞—信号注销—信号处理:
信号产生
终端按键产生信号
- 当你在按中断(键盘)上的某些键时会产生信号(比如你按下"Ctrl+\",就是产生了一个SIGQUIT信号);
- ctrl+c—SIGINT;
- ctrl+\—SIGQUIT;
- ctrl+z— SIGSOP
- …
硬件异常产生信号
- 通常是除数为0,无效内存引用等,(还记得Linux下我们解引用NULL时会发生的那个"Segment fault"吗?,这就是硬件异常导致进程接收到了SIGSEGV信号);
调用系统函数产生信号
- 使用kill命令在控制台向正在运行的程序发送信号:kill -signum pid;
- 使用kill和raise函数可以使进程自己给自己发送信号:
- int kill(pid_t pid, int signo);
- int raise(int signo);
int main(){
int count = 0;
int flag = 0;
scanf("%d",&flag);
while(count++!=100){
printf("hehe%d\n",count);
sleep(1);
if(count==flag){
raise(SIGQUIT);
}
}
return 0;
}
- int sigqueue(pid_t pid, int sig, const union sigval value); 主要支持带有数据的实时信号函数
- abort(); 产生SIGABRT信号使程序异常终止
由软件条件产生的信号
- SIGPIPE信号就是当管道读进程终止时,在写管道产生的;
- SIGALRM信号:使用alarm函数设置的定时器超时时,就会产生此信号,默认动作时终止调用该函数的进程
- unsigned int alarm(unsigned int seconds);
- alarm(0);执行此条语句时,如果有以前调用的的尚未超时的定时器,使其失效,返回值是剩下的时间;
信号在进程中注册
- 当一个进程接收到一个信号 ,该信号往往会先在PCB里进行注册,在task_struct里,有一个名叫pending的sigpending结构体成员(待解决信号表):
struct sigpending {
struct sigqueue *head, *tail;
sigset_t signal;
}
- 其中signal使用二进制位图的方式记录收到了那些待解决信号集(收到的置1),并且为每一个注册的信号维护一个sigqueue节点:
- 第一个第二成员指向了这个未解决信号队列的首尾;
struct sigqueue{
struct sigqueue *next; //指向下一个sigqueue节点;
siginfo_t info; //记录该信号所携带的信息
}
- 在这里可以看看可靠信号与不可靠信号的具体不同:
- 可靠/实时信号传入时,不管对应的信号集的位是不是1(有没有同类信号注册)都会维护一个sigqueue节点放入队列中等待处理;
- 而传入的信号是不可靠/非实时信号时,一旦待解决信号集里的对应位是1,发现已经有同类信号注册了了,就不会再创建sigqueue节点了,这就意味着此次信号的丢失.
- 所以不可靠信号(信号值小于SIGRTMIN的信号)在待解决信号队列中只能占有一个sigqueue节点,而之后的信号只要收到就会忽略,知道队列中的信号被处理
信号的注销
- 当进程拿到队列中已注册的信号,在真正处理它之前,要先在pcb中抹除信号存在的痕迹,方便接下来同类信号的识别,具体做法是:
- 可靠信号:在删除sigqueue节点之后,判断队列中是否还有同类信号,如果没有,将未解决信号集中(位图)对应的位置0;
- 非可靠信号:在删除sigqueue节点后,直接将未解决信号集中的对应位置0,原因见上一条粗体句;
信号的处理:信号递达(Delivery)
- 自Unix系统以来,便规定当目标信号出现时,我们可以告诉内核按照以下三种方式处理:
- 执行默认动作:还记得我们之前提到的那些1~31号信号都被系统指定代表了特定事件的发生,并有与之匹配的默认动作,这之中大部分都会默认终止当前进程;
- 忽略此信号:除了SIGKILL与SIGSTOP信号之外,其他信号都支持以这种方式进行处理,这也就意味着当信号发生时,进程并不会对此有任何反应,如果SIGKILL与SIGSTOP支持这种方式,那么我们将失去解除异常进程与停止通信的方法,这会使得进程的行为便成为定义的;
- 捕捉信号(自定义处理):这就使得我们在特定信号发生时,调用一个我们自定义的函数,以我们想的方式进行处理;
相关接口
- sighandler_t signal(int signum,sighandler_t handler)
- signum :信号编号;
- handler:信号处理方式
- SID_DFL默认处理方式
- SIG_IGN忽略处理
- typedef void (*sighandler_t)(int);用户自定义信号回调函数原型;
- int sigaction(int signum,struct sigaction *act,struct sigaction *oldact)
- signum:信号编号
- act:信号新的处理动作
- oldact:保存信号源有的处理动作
信号递达流程图:
- 当信号传入,系统因为中断切换到内核态运行,首先查看PCB中的signal位图信息,如果有待处理的信号,则将要处理的信号从队列中注销,然后从动作结构体数组handle中找到对应的动作结构体,从中取到指向处理动作函数的函数指针(这个函数指针是根据signal()/sigaction()接口已经决定好的),拿去到后如果是默认/忽略动作,在内核态完成;如果是自定义动作函数,返回用户态执行函数,然后调用sigreturn()返回内核态检查signal位图信息(看队列里是否还有其他待处理信号),如果没有了,执行sys_sigturn()返回用户态让进程继续之前的工作,如果还有,则继续在handle里拿取执行动作函数的函数指针
信号的阻塞:阻止信号被递达(Block)
- 在信号的处理过程中,我们也有可能会需要收到信号后,并不需要马上处理,而是记录下来后一会再处理,但也不希望在这个过程中信号发生丢失,Linux为这种情况提供了信号阻塞机制:在PCB中,有一个信号的阻塞合集(block),这个信号阻塞集合的作用就是标记哪些信号到来的时候可以注册但暂时不被处理;
- block(信号阻塞集)/pending(待处理信号集)/handle(信号处理动作集)的关系
相关接口
- int sigprocmask(int how,const sigset_t * set,sigset_t *oldset);
- how:对信号阻塞集合要进行的动作
- SIG_BLOCK 将set中的信号添加到阻塞集合,相当于block= block|set
- SIG_UNBLOCK 对set中的信号解除阻塞,相当于block=block &(~set)
- SIG_SETMASK 设置当前信号屏蔽值为set所指向的值,相当于block=set
- set 你想要的阻塞的信号集合
- oldset 用于保存原来的信号集合
- how:对信号阻塞集合要进行的动作
- 清空set信号集合
- int sigemptyset(sigset_t *set);
- 将所有信号添加到set集合中
- int sigfillset(sigset_t *set);
- 将signum信号添加到set集合中
- int sigaddset(sigset_t *set, int signum);
- 将signum信号从set集合中移除
- int sigdelset(sigset_t *set, int signum);
可重入函数
- 在进程捕捉信号并对其进行处理时,进程正在进行的正常指令就会被信号处理程序临时中断,当执行完信号处理程序返回时,才进行之前的操作,信号传递过来可以是任意时间的,但如果进程正在执行malloc函数,它正在为所分配的内存区维护一个链表,这时插入信号中断,就会对进程造成破坏;当函数可以在信号调用过程中保证调用安全,也就是在多个执行流中重复调用,且结果符合预期时,它就是可重入函数
- 不可重入函数的特征:
- 已知它们使用静态数据结构
- 调用malloc或这free
- 是标准输入I/O函数,这些函数大多都以不可重入的方式使用全局数据结构
- 无法在函数中对全局数据进行原子操作;