Linux之进程信号
1. 信号概念
信号是进程之前事件异步通知的一种方式,属于软中断。
2. 信号的种类
2.1 信号个数
在Linux操作系统中,用kill -l命令可以查看系统定义的信号列表,查看结果如下:
在Linux操作系统中,总共有62个信号,其中1-31号信号有可能丢失,叫==非可靠信号==;32、33,信号不存在;34-64号信号属于可靠信号,进程收到多少可靠信号就处理多少次。
2.2 信号是如何产生的
信号的产生分为硬件产生和软件产生。
- 硬件产生:
例如,Ctrl+c,产生SIGINT,也就是2号信号,为中断信号。
Ctrl+z,产生SIGSTP,终止信号,一般慎用。
Ctrl+|,产生SIGQUIT,退出信号。 - 软件产生:
例如kill函数,可以给任意进程发送,只要知道进程的pid即可。raise函数,其实是封装的kill函数,只能给自己知道的进程发送信号。这两个函数都是成功返回0,错误返回-1。
#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
3. 信号的注册
3.1 信号注册示意图:
3.2 非可靠信号的注册
- 第一次注册一个信号:
- 更改sig位图当中信号对应的比特位,将该比特位置位1。
- 给sigqueue队列中添加对应信号的节点。
- 第二次注册相同的信号(前提是之前的信号没有被处理):
- 更改sig位图,想让对应的比特位置为1。
- 如果发现sig位图当中之前比特位为1,则不再添加sigqueue节点。
3.3 可靠信号的注册
- 第一次注册一个信号
- 更改sig位图当中信号对应的比特位,将该比特位置位1。
- 给sigqueue队列中添加对应信号的节点。
- 第二次注册相同的信号(前提之前的信号没有被处理)
- 更改sig的位图,想让对应的节点的比特位置位1。
- 直接在添加对应信号的节点到sigqueue的对列当中,直白点说就是有多少可靠信号来注册就添加多少sigqueue节点。
4. 信号的注销
- 非可靠信号的注销
- 在sigqueue队列中将对应信号的节点进行出队操作。
- 将对应的比特位置位0。
- 可靠信号的注销
- 在sigqueue队列中将对应信号的节点进行出队操作。
- 需要判断sigqueue节点当中是否存在相同的节点。如果存在,则sig位图当中对应的比特位保持为1,如果不存在,则将对应的比特位置位0。
5. 信号的捕捉流程
流程图:
假设代码如下:
int main()
{
signal(2, sigcallback);
while(1)
{
sleep(1);
}
}
其中执行上述代码的流程为:
- 当收到ctrl+c,也就是2号信号的时候。
- 当程序执行sleep函数的时候,从用户态切换到内核态,执行内核的代码。
- 执行完sleep函数的逻辑后,需要调用一个函数do_signal函数,处理程序所收到的信号;当程序没有收到2号信号时,直接调用sysreturn函数返回用户态;当程序收到了2号信号,切换到用户态去执行用户自定义的函数。
- 执行完毕后,调用sigreturn函数切换回内核态,再次调用do_signal函数,重复3逻辑,知道程序收到的信号被处理完。
- 调用sysreturn函数返回用户态继续执行程序代码。
信号的阻塞
前提:在task_struct结构体当中保存了一个block位图。
注意:信号的阻塞并不是说信号不能被注册,不会影响信号更改pending位图和增加sigqueue节点。
操作系统处理信号的逻辑:
当程序从用户态切换到内核态之后,处理do_signal函数的时候,发现收到某个信号,想要处理这个信号之前,先判读block位图当中对应信号的bit位是否为1;当block当中对应bit位为1时,则不处理该信号,sigqueue当中对应的信号的节点还是存在的。当block当中对应的bit位为0时,则处理该信号。其中更改该bit位的函数如下:
int sigprocmask(int how, const sigset_t * set, sigset_t *oldset);
功能:更改sigset_t位图当中的bit位的值。
参数howh中的3个宏常量:
SIG_BLOCK:设置某个信号位阻塞状态,用修改达到目的,方法:block(new)= block (old) | set
SIG_UNBLOCK:设置某个信号位非阻塞状态,方法:block(new)= block(old)&(~set)
SIG_SETMASK:设置新的阻塞的sigset_t位图,方法:block(new)= set
其中set是要设置的新的阻塞位图;oldset是之前程序当中阻塞的位图,是出参的。
例子如下:
几点说明:
- 信号阻塞的时候,必不会干扰信号的注册;
- 同时受到多个同样的非可靠信号只会添加一次sigqueue节点,也就是说只会处理一次。
- 同时受到多个可靠信号,会添加多次sigqueue节点,每一个可靠信号都会被处理。
6. 自定义信号的处理方式
- 信号的处理方式
- SIG_DEF:默认处理方式。
- SIG_IGN:忽略处理。
SIGCHILD信号就是默认处理的方式,子进程在退出的时候,会给父进程发送一个SIGCHLD信号,而父进程对SIGCHILD信号的处理方式为忽略。
僵尸进程:子进程退出的时候,给父进程发送一个SIGCHLD信号,但是操作系统对SIGCHLD信号的处理方式为忽略处理,而导致父进程不去处理信号,从而子进程变成了僵尸进程。
3. 自定义处理–程序员自己定义处理的函数
typedef void(*sighandler_t)(int);
定义自定义处理函数,其中void表示没有返回值。
int参数值的是哪一个信号触发操作系统调用该函数。
sighandler_t signal(int signum, sighander_t handler);
signum:需要更改自定义处理函数的信号。
handler:接受一个函数的地址,将信号的处理函数更改为什么函数。
9号信号是不能被定义信号处理方式的。
int sigaction(int signum, const struct sigaction *act, struct sigaction * oldact)
自定义信号的处理流程:
- 在task_struct结构体中,有一个指向sighand_struct的结构体指针,在该结构体指针中有一个action的数组,数组当中每一个元素都是struct k_sigaction结构体,数组中每一个元素对应一个信号的处理逻辑。
- 在struct k_sigaction结构体中有一个元素是struct sigaction sa,在struct sigaction结构体当中有一个sighandler_t类型的元素,这个sighandler_t是一个函数指针类型,typedef void(*sighandler)(int),保存信号默认执行的函数。
操作系统默认对信号的处理:
当sig位图中收到一个信号的时候,意味着sig位图当中的某一个比特位被置位1,系统处理该信号的时候,就会从PCB当中寻找sighang_struct这个结构体的指针,从而找到sa_handler,进而操作系统内核去调用sa_handler保存的函数地址,完成信号功能。