目录
信号
- 信号实质是软中断,用于通知进程发生某些事件。
- 信号作用:通知事件的发生,信号产生之后第一时间也不是直接处理,而是先存储下来,再处理信号。
- 信号生命周期: 信号的产生 ---> 信号的注册 ---> 信号的阻塞 ---> 信号的注销 ---> 信号的处理
在Linux下,有62种信号:
信号分了两类:
- 1~31不可靠信号(非实时信号)
- 34~64 可靠信号(实时信号)
-
信号产生
- 通过硬件中断; // SIGINT(2号信号,Ctrl + C产生)、SIGQUIT(3号信号,Ctrl + \产生)和SIGTSTP(20号信号,Ctrl+Z产生)
测试代码:
#include <iostream>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
using namespace std;
int main(){
pid_t pid = fork();
if(pid < 0){
cerr << "fork error" << endl;
exit(0);
}
else if(pid == 0){
while(1){
sleep(1);
cout << "The Child Pid = " << getpid() << ", PPid = " << getppid() << endl;
}
}
else{
int status;
wait(&status);
cout << "exit num is = " << status << endl;
}
return 0;
}
在程序执行时,通过ctrl+[c \ z]的结果:
上面结果并未返回子进程退出信息,这是因为按键操作使得父进程也退出,且父进程没机会能打印出子进程退出信息。可以通过给子进程发送退出信号的方式来得到希望结果(在后续展示。。。)。
- 程序异常产生; // 比如当前进程访问了非法地址空间,MMU(内存管理单元)会产生异常,内核将这个异常解释为SIGSEGV信号(11号信号)发送给进程、若程序中有除0错误,也会给进程发送一个信号(SIGFPE -- 8号信号)。
测试代码:
int main(){
pid_t pid = fork();
if(pid < 0){
cerr << "fork error" << endl;
exit(0);
}
else if(pid == 0){
int n = 0;
n = 10/0;
}
else{
int status;
wait(&status);
cout << "exit num is = " << status << endl;
}
return 0;
}
- 软件条件产生:
- int kill (pid_t pid, int sig); // 向指定进程发送指定信号
- int sigqueue(pid_t pid, int sig, const union sigval value); // 给指定进程发送指定信号,同时可以携带一个参数
- int raise (int sig); // 向自身发送信号
- unsigned int alarm (unsigned int n); // 定时器,在n秒后向进程发送SIGALARM信号,在设置一个定时器,会取消上一个定时器,并返回上一个定时器剩余时间。
- 函数abort,是为了让进程异常终止的。
#include <stdlib.h> void abort (void);
测试代码:
int main(){
pid_t pid = fork();
if(pid < 0){
cerr << "fork error" << endl;
exit(0);
}
else if(pid == 0){
int count = 3;
while(count--){
sleep(1);
cout << "The Child Pid = " << getpid() << ", PPid = " << getppid() << endl;
}
raise(3); // 向自身发送3信号
abort();
}
else{
int status;
kill(pid, 9); // 向子进程发送9号信号
wait(&status);
cout << "exit num is = " << status << endl;
}
return 0;
}
调用kill函数:
调用raise()函数:
调用abort():
-
信号注册
- 信号的注册即给一个进程发送信号,就是修改这个进程pcb中关于信号的pending位图,将相应的信号位置1;
- 信号的阻塞:暂时不处理信号即阻止信号的递达,并不是不接收信号;
原理: 要阻塞一个信号那么就是将 pcb 中关于信号的block位图再修改,将相应信号位置1,以说明此信号不处理。
- 信号的递达即处理信号
- 信号未决:是一种状态,信号从注册成功到信号的递达之间的一种状态。
-
信号阻塞(屏蔽)
其他概念:
- 实际执行信号的处理动作称为信号递达(Delivery);
- 信号从产生到递达之间的状态,称为信号未决(Pending);
- 进程可以选择阻塞(Block)某个信号;
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作;
- 阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是递达之后可选的一种动作。
说明:
在pcb中有一个pending位图,其中存储当前接受到的信号,还有一个blocked位图用于存储现在都有哪些信号要被阻塞,它们之间不会相互影响。进程看到pending集合中收到那些信号,在处理这些信号之前,进程先查看一下blocked集合是否存有,如果存在了,就意味着这个信号现在将不被处理,直到解除阻塞。
#include <signal.h>
// 清空一个信号集合
int sigemptyset (sigset_t *set);
// 将所有信号都添加到set集合
int sigfillset(sigset_t *set);
// 当前pending集合(信号注册集合)中的信号取出放到set中
int sigpending(sigset_t *set);
// 添加指定的单个信号到set集合中
int sigaddset(sigset_t *set,int signum);
// 从集合中移除一个指定的信号
int sigdelset(sigset_t *set, int signum);
// 判断一个信号是否在一个集合中
int sigismember(const sigset_t *set,int signum);
// 阻塞信号/解除阻塞
int sigprocmask(int how, sigset_t *set, sigset_t *oldset);
说明:
sigset_t:用相同的数据类型sigset_t 来存储未决和阻塞标志,sigset_t称为信号集。阻塞信号集也叫做信号屏蔽字(Signal Mask)。
set: 要阻塞/解除阻塞的集合
signum:指定信号
how:
- SIG_BLOCK 阻塞集合中的信号
- SIG_UNBOCK 对集合中的信号解除阻塞
- SIG_SETMASK 将集合中的信号设置到阻塞集合中
oldset: 保存原先阻塞集合中的信号
扩充:
有两个信号不会被阻塞:SIGKILL 和 SIGSTOP
sigpending 获取未决信号
-
信号处理
sighandler_t signal (int signum, sighandler_t handler);
说明:
- signum:信号的编号
- handler:处理方式:1. SIG_IGN 忽略 2. SIG_DFL 默认
修改信号处理方式:
- 默认处理:按照操作系统中对信号事件的既定处理方式
- 忽略处理:直接将信号丢掉。
- 自定义处理:用户自定义事件的处理方式。
信号是当我们发起系统调用/程序异常/中断当前程序从用户态运行切换到内核态,去处理这些事情,处理完毕之后,要从内核态返回用户态,但是返回之前会看(在pending位图)一下是否有信号需要被处理,如果有,就处理信号(切换到用户态执行信号的自定义处理方式),处理完毕之后再次返回到内核态,判如果没有信号要处理了就调用sys_sigreturn返回用户态(我们程序之前的运行位置)。
信号捕捉:
对于默认和忽略的处理方式可以在内核态直接处理,而对于是自定义处理方式时,信号递达需要先调用自定义函数处理,这一过程叫信号捕捉。
原理:
捕捉时机:
内核态切换到用户态。
内核态:操作系统本身的状态;
用户态:受限状态,必须在操作系统搭建好的状态下运行。
实现方式:
sigaction接口(较为多用):自定义信号的处理方式,并且signal函数内部也是通过sigaction实现的。
#include <signal.h>
int sigcation(int signum, const struct sigaction *act, struct sigaction *oldact);
// signum:信号编码
// act:新的处理方式
// oldacr:保存原因处理方式
struct sigaction{
void (*sa_handler) (int); //处理函数
void (*sa_sigaction) (int, siginfo_ *, void *); // 处理函数
sigset_t sa_mask; //在处理信号的时候可以 通过这个mask 暂时阻塞一些信号,处理完毕之后会还原回去
int sa_flags; //决定了我们使用那个回调接口,并且换有其他选项信息
void (*sa_restorer) (void);
}
-
信号注销
注销就是从pending 集合中将即将要处理的信号相应位置0。
可靠信号:
对于可靠信号就是不管当前信号是否注册都要置1,且添加到链表中,所以不会弄丢信号。
注销就是删除节点,且判断是否有相同信号节点,若没有则位图置0,否则就不置0.
非可靠信号:
对应非可靠信号注册就是在pending位图相应位置1,然后添加一个sigqueue结构到链表中,如果之后有相同信号到来,而相应位是1,则不做操作,也就不会重复注册,相当于丢了。
注销就是删除链表节点,且相应位置0.
扩展补充:
- 进程信号与信号量:
信号: 实质是软中断,信号也算作是进程通信的一种方式;
信号量:实际上是等待队列的计数器,它表示是否有可用临界资源。
- 关于僵尸进程的避免:
操作系统如何通知父进程,子进程退出呢?
信号:SIGCHLD - 17号信号
在没有学习信号,为避免僵尸进程,只能让父进程一直等待子进程的退出(因为实在是不知道子进程到底什么时候退出),浪费了父进程资源。现在可以自定义SIGCHILD的处理方式,相当于提前告诉进程,当接收到这个信号时使用waitpid,这样就不用一直等了。
使用非阻塞的循环来处理SIGCHILD信号
因为SIGCHILD信号是不可靠信号,就有可能丢失,因此就有可能漏掉僵尸子进程没有处理,所以一旦收到信号就处理到不能处理为止。