【Linux】:信号的保存和信号处理
朋友们、伙计们,我们又见面了,本期来给大家带来信号的保存和信号处理相关代码和知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!
C 语 言 专 栏:C语言:从入门到精通
数据结构专栏:数据结构
个 人 主 页 :stackY、
C + + 专 栏 :C++
Linux 专 栏 :Linux
目录
1. 信号的保存
1.1 信号相关概念
1.2 信号的保存
1.3 处理位图的接口
2. 信号的处理
2.1 状态的切换
2.2 信号的处理
2.3 sigaction函数
3. 信号的其他补充
3.1 可重入函数
3.2 SIGCHLD信号
1. 信号的保存
1.1 信号相关概念
- 实际执行信号的处理动作称为信号递达(Delivery)。
信号递达的方式有三种:
- ① 信号的默认处理
- ② 信号的忽略
- ③ 信号的自定义捕捉
当我们自定义捕捉信号的时候使用的signal接口就是对指定信号进行捕捉,然后去执行我们自定义的方法,下面再来介绍一下两种用法:
- ① signal(signo, SIG_DFL):对指定信号恢复默认操作;
- ② signal(signo, SIG_IGN):对指定信号进行忽略(忽略也算做对信号进行处理)。
- 信号从产生到递达之间的状态,称为信号未决(Pending)。
- 进程可以选择阻塞 (Block )某个信号。
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
- 注意:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
1.2 信号的保存
当信号产生时,我们不一定要立即对信号进行递达,而是在合适的时候进行递达,那么在信号未决时期,我们要有能力将信号保存,所以在进程PCB中会存在三张位图表,用于保存信号:
信号屏蔽字(block表):比特位的位置表示信号的编号、比特位的内容表示是否对特定信号进行屏蔽(阻塞)。
未决位图表(pending表):比特位的位置表示信号编号、比特位的内容表示特定的信号时候被递达。
handler表(函数指针数组):比特位的位置表示信号编号、比特位的内容是一个函数指针,指向该信号的处理方法。
注意:常规信号在递达之前产生多次只记一次!
1.3 处理位图的接口
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量:
#include <signal.h> int sigemptyset(sigset_t *set); int sigfillset(sigset_t *set); int sigaddset (sigset_t *set, int signo); int sigdelset(sigset_t *set, int signo); int sigismember(const sigset_t *set, int signo);
- 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。
- 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。
- 注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
- 函数sigaddset用于向指定的信号集添加某种信号。
- 函数sigdelset用于删除指定信号集中的某种信号。
- 函数sigismember用于判断指定信号在指定信号集是否存在。
对block表进行操作:
#include <signal.h> int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数:
① set:将要设置的新的信号屏蔽字
② oldset:获取旧的信号屏蔽字
③ how:修改block表的选项
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。
SIG_BLOCK set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask = mask l set SIG_UNBLOCK set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask = mask & ~set
SIG_SETMASK 设置当前信号屏蔽字为set所指向的值,相当于
mask = set
对pengding表操作:#include <signal.h> int sigpending(sigset_t *set);
参数:
① set:获取当前进程的信号未决表
返回值:
成功返回0,出错返回-1
接下来通过这些接口我们可以实现一个动态的打印pending表的一个代码:
#include <iostream> #include <unistd.h> #include <signal.h> #include <sys/types.h> using namespace std; // 打印pending表 void PrintPending(const sigset_t &pending) { for (int signo = 31; signo > 0; signo--) { if (sigismember(&pending, signo)) { std::cout << "1"; } else { std::cout << "0"; } } std::cout << "\n"; } int main() { // 1. 屏蔽2号信号 sigset_t set, oset; // 1.1 初始化信号集 sigemptyset(&set); sigemptyset(&oset); // 1.2 添加信号 sigaddset(&set, 2); // 1.3 修改信号集 sigprocmask(SIG_BLOCK, &set, &oset); // 2. 让进程不断获取当前进程的pending int cnt = 0; sigset_t pending; while (true) { // 2.1 获取pending表 sigpending(&pending); // 2.2 打印 PrintPending(pending); sleep(1); cnt++; if (cnt == 10) { std::cout << "解除对2号信号的屏蔽, 2号信号准备递达" << std::endl; // 2.3 恢复原pending表 sigprocmask(SIG_SETMASK, &oset, nullptr); } } return 0; }
2. 信号的处理
2.1 状态的切换
进程会在合适的时候处理信号,那么这个合适的时候是指什么时候呢?
进程从内核态返回到用户态时,进行信号的检测和处理。
- 用户态:一种受控的状态,能够访问的资源是有限的。
- 内核态:操作系统的工作状态,能访问大部分的系统资源,并且可以让用户以操作系统的身份访问内核空间。
- ① 用户是无法直接访问OS底层资源,只能通过系统调用间接访问,所以用户调用系统调用,必然包含了身份的变化;
- ② 进程要被调度首先得加载到内存然后通过页表映射到物理内存,那么操作系统也是需要被映射到物理内存的;
- ③ 用户空间由用户级页表映射到物理内存;
- ④ 内核空间由内核级页表映射到物理内存;
- 所以在调用系统调用时访问OS直接在进程地址空间内进行跳转,就如同函数调用一样,调用系统调用接口也是在进程地址空间内进行的。
① 操作系统的代码、系统调用、数据结构、数据在整个系统中只有一份,所以内核级页表只需要有一张即可;
② 如果有多个进程,只需将内核空间通过内核级页表映射到物理内存,尽管有多个进程,使用的也是同一份系统调用接口;
③ 无论进程如何调度,CPU都可以直接找到操作系统!
④ 我们进程所有代码的执行,都可以在自己的进程地址空间内通过跳转的方式,进行调用和返回。
那么如何区分内核态和用户态呢?
CPU内存在的寄存器CS寄存器,CS寄存器用来保存代码段的,其中有两个比特位01表示内核态(1)、11表示用户态(3);切换用户的状态其实就是修改CS寄存器中对应的比特位。
CPU内部还存在一些CR寄存器:
CR3寄存器用于保存当前运行进程的用户级页表的物理地址;
CR1寄存器用于保存上一次引发缺页中断的虚拟地址。
2.2 信号的处理
用户在调用系统调用之后,在要完成调用任务时,会从用户态切换至内核态完成对应的任务,此时并不是直接切换回用户态,而是先要检测信号,如果有需要处理的信号,根据对信号的处理方法,如果是默认动作、忽略就直接处理,如果是用户自定义方法,那么此时不能在内核态处理,而是要返回用户态去执行用户自定义方法,在执行完之后,不能直接跳转到用户代码处,而是要再次返回内核态,再从内核态返回进入内核态的用户代码处。
简化的图就是一个♾️
在信号捕捉中,一共会涉及到4次状态的切换!
上述情况是只有一个信号需要被处理,那么如果存在多个需要处理的信号,那么在处理完一个信号之后会轮训式的检测需要处理的信号,在所有信号处理完之后再切换为用户态。
2.3 sigaction函数
该函数是一个检测信号并改变处理动作的函数:
参数:
signum:要改变的信号的编号;
sigaction是一个结构体:
其中我们只需要关注sa_handler和sa_mask
sa_handler是要指定的处理动作;
sa_mask是要额外屏蔽的信号集。
act:表示要改变的新的处理方法;
oldact:表示被改变之前的处理方法。
Linux是不允许同一个信号已经在被处理的过程中,再次进行嵌套处理的,所以当某一个信号在被处理的过程中,内核会自动将该信号加入到信号屏蔽字中,当处理的函数返回之后,会对该信号进行恢复,除了当前处理的信号被屏蔽外,我们也可以通过sa_mask(信号集)添加一些额外的信号进行屏蔽。
代码演示:
#include <iostream> #include <unistd.h> #include <signal.h> void Print(const sigset_t &pending); void handler(int signo) { std::cout << "get a sig: " << signo << std::endl; sleep(1); while (true) { sigset_t pending; sigpending(&pending); Print(pending); sleep(1); } } // 打印pending表 void Print(const sigset_t &pending) { for (int signo = 31; signo > 0; signo--) { if (sigismember(&pending, signo)) { std::cout << "1"; } else { std::cout << "0"; } } std::cout << std::endl; } int main() { std::cout << "pid: " << getpid() << std::endl; struct sigaction act, oact; // 自定义处理方法 act.sa_handler = handler; // 初始化信号集 sigemptyset(&act.sa_mask); // 添加3号信号 sigaddset(&act.sa_mask, 3); // 自定义捕捉2号信号 sigaction(2, &act, &oact); while (1) sleep(1); return 0; }
3. 信号的其他补充
3.1 可重入函数
将函数和信号结合起来研究:
在链表阶段我们实现了一个头插的函数接口,头插的阶段分为两步,做完第一步的时候由于某些硬件中断使进程回到了内核态,再次返回用户态时需要进行信号的检测与处理,如果此时的信号自定义方法中也调用了头插的函数,在做完头插的两步之后又重新返回用户态的代码处继续向下执行,那么在自定义方法中插入的头节点就会被丢失掉,此时这个头插的这个函数就是一个不可重入函数。
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。
如果一个函数符合以下条件之一则是不可重入的 :
- 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
- 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
3.2 SIGCHLD信号
在进程等待的章节说到过,子进程退出时父进程必须进行等待(waitpid()),否则会造成僵尸问题,并且我们有时还需要知道子进程的退出信息,另外在子进程退出的时候回向父进程发送SIGCHLD信号。
#include <iostream> #include <unistd.h> #include <signal.h> void handler(int signo) { std::cout << "get a sig: " << signo << std::endl; } int main() { std::cout << "pid: " << getpid() << std::endl; // 自定义捕捉信号 signal(SIGCHLD, handler); pid_t id = fork(); if (id == 0) { std::cout << "child is running" << std::endl; sleep(5); exit(10); } while (true) sleep(1); return 0; }
父进程自定义捕捉SIGCHLD信号,子进程在运行5秒后退出,可以看到果然子进程给父进程发送了SIGCHLD信号。
所以我们就可以基于信号来对子进程进行回收等待了:
#include <iostream> #include <unistd.h> #include <signal.h> #include <sys/types.h> #include <sys/wait.h> void handler(int signo) { std::cout << "get a sig: " << signo << std::endl; // 等待任意进程 waitpid(-1, nullptr, 0); } int main() { std::cout << "pid: " << getpid() << std::endl; // 自定义捕捉信号 signal(SIGCHLD, handler); pid_t id = fork(); if (id == 0) { std::cout << "child is running" << std::endl; sleep(5); exit(10); } while (true) sleep(1); return 0; }
Linux支持手动忽略SIGCHLD,如果对其进行忽略,那么所有的子进程都不要父进程进行等待了,子进程会在终止时自动的清理。
#include <iostream> #include <unistd.h> #include <signal.h> int main() { std::cout << "pid: " << getpid() << std::endl; // 手动忽略SIGCHLD signal(SIGCHLD, SIG_IGN); pid_t id = fork(); if (id == 0) { std::cout << "child is running" << std::endl; sleep(5); exit(10); } return 0; }
朋友们、伙计们,美好的时光总是短暂的,我们本期的的分享就到此结束,欲知后事如何,请听下回分解~,最后看完别忘了留下你们弥足珍贵的三连喔,感谢大家的支持!