4. 阻塞信号
4.1 相关概念
- 信号递达(delivery):执行信号的处理动作(默认处理:终止进程(Term,Core)、signal函数:自定义处理)
- 信号未决(pending):信号从产生到递达之间的状态(暂时保存
- 阻塞信号(block):被阻塞的信号产生时将保持在未决状态,直到进程解出对此信号
- 的阻塞才执行递达动作(阻塞和忽略是不同的,信号被阻塞就不会递达,忽略是递达后的一种处理动作(什么都不做的动作))
4.2 信号在内核中的示意图
进程维护三张表:
- pending表:位图结构;比特位的位置表示哪一个信号,比特位的内容表示是否收到信号
- block表:位图结构;比特位的位置表示哪一个信号,比特位的内容表示是否被阻塞
- handler表:函数指针数组;数组下标表示信号编号,数组下标对应的内容表示递达动作
如何理解:第一行中,block是0,pending是0,默认处理动作。第二行中block是1,pending是1,忽略来处理。第三行中block是1,pending是0,捕获信号自定义处理。
4.3 函数操作pending和block表
4.3.1 sigset_t信号集
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态**。阻塞信号集也叫做当**
前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。sigset_t来控制block和pending两个位图。
# define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int))) typedef struct { unsigned long int __val[_SIGSET_NWORDS]; } __sigset_t;
4.3.2 函数
int sigemptyset(sigset_t *set); //初始化set给出的信号集为空,并从该集合中排除所有信号
int sigfillset(sigset_t *set); //初始化set为full,包括所有信号
int sigaddset(sigset_t *set, int signum); //添加信号符号
int sigdelset(sigset_t *set, int signum); //删除信号符号
int sigismember(const sigset_t *set, int signum); //测试sgn是否是集合的成员
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); //检查和改变阻塞信号
- sigprocmask函数
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信
号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后
根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。
how参数可选值 | 功能 |
SIG_BLOCK | 添加信号屏蔽字信号,相当于mask = mask | set |
SIG_UNBLOCK | 删除信号屏蔽字信号,相当于mask = mask & ~set |
SIG_SETMASK | 设置信号屏蔽字为set所指向的值,相当于mask = set |
- 使用
#include <iostream> #include <signal.h> #include <sys/types.h> #include <unistd.h> void showNew(sigset_t* newSet) { int signalnum = 1; std::cout << "newSet:"; for(; signalnum <= 31; ++signalnum) { if(sigismember(newSet, signalnum)) std::cout << "1"; else std::cout << "0"; } std::cout << std::endl; } void showOld(sigset_t* oldSet) { int signalnum = 1; std::cout << "oldSet:"; for(; signalnum <= 31; ++signalnum) { if(sigismember(oldSet, signalnum)) std::cout << "1"; else std::cout << "0"; } std::cout << std::endl; } int main() { //栈操作-->并没有设置进进程 sigset_t newSet, oldSet; sigemptyset(&newSet); //初始化 sigemptyset(&oldSet); sigaddset(&newSet, 2); //将2号信号添加到newSet信号集 //设置进进程 sigprocmask(SIG_SETMASK, &newSet, &oldSet); //阻塞信号集被设置为参数集 int time = 0; while(true) { showNew(&newSet); //这里打印是一直不变的,因为newSet和oldSet并没有改变 showOld(&oldSet); ++time; if(time == 10) //不再屏蔽2号信号 { sigprocmask(SIG_SETMASK, &oldSet, &newSet); //把old信号集设置到进程 } sleep(1); } return 0; }
4.3.3 sigpending未决信号集
函数:int sigpending(sigset_t *set);//检测未决信号(set参数是输出型参数
#include <iostream> #include <signal.h> #include <cassert> #include <unistd.h> #include <sys/types.h> //任务:屏蔽二号信号不断获取进程pending信号集并不断打印,发送二号信号观察pending信号集变化并解出二号信号阻塞,递达处理动作是自定义动作 static void printtPending(const sigset_t &pending) { std::cout << "PID:" << getpid() << " pending: "; for(int signalnum = 1; signalnum <= 31; ++signalnum) { if(sigismember(&pending, signalnum)) std::cout << "1"; else std::cout << "0"; } std::cout << std::endl; } static void handler(int signalnum) { std::cout << "catched:" << signalnum << std::endl; } int main() { sigset_t newSet, oldSet; //初始化 sigemptyset(&newSet); sigemptyset(&oldSet); //信号集中设置2信号 sigaddset(&newSet, SIGINT); //信号屏蔽字设置进进程中 sigprocmask(SIG_BLOCK, &newSet, &oldSet); //获取进程pending信号集并打印 int count = 0; signal(SIGINT, handler); //2号信号捕捉后执行自定义动作 while(true) { sigset_t pending; sigemptyset(&pending); //获取pending信号集 int ret = sigpending(&pending); assert(ret == 0); (void)ret; //打印 printtPending(pending); //解出对2号信号的屏蔽 if(count++ == 10) { std::cout << "SIGINT signal unblocked!" << std::endl; sigprocmask(SIG_SETMASK, &oldSet, nullptr); //对2号信号解出阻塞后,默认递达动作时终止进程 } sleep(1); } return 0; }
运行结果
现象描述:给进程发送2号信号后,并没有采取信号默认处理方式而是处于信号未决状态,也就是本来没发送信号,此时发送2号信号后,屏蔽信号集中2号信号被设置,所以进程收到2号信号时被阻塞了,也就是处于未决状态没有递达,所以此时pending信号集的第2个比特位变成了1,过了10秒后信号递达先是解除2号信号阻塞,然后执行自定义处理动作,随后打印解出2号信号后的pending信号集。
5. 捕捉信号
5.1 引出
生活中,当我们和某个人说的十分重要的事的时候突然来了个电话,我们不会去立即处理,当和这个人说完事后再回电话处理。那么信号会被立即处理吗?也可能不会,但是当一个信号解除了阻塞状态时,就会立即递达。这里需要引出的问题就是,什么时候合适解出阻塞状态呢?正是进程从内核态到用户态的时候,进程会在OS指导下进行信号的检测和处理(处理三种方式:默认、忽略、自定义行为处理)。用户态是执行用户的代码进程所处的状态,内核态是执行内核的代码进程所处的状态,这句话什么意思呢?我们在linux上写代码的时候往往会调用系统调用接口,这些系统接口是Linux操作系统中的代码来封装得到的,那么当我们写代码的时候就会会执行内核中的代码。 那么再回顾地址空间:
所有进程的虚拟地址空间[0GB, 3GB]是不同的,每个进程都有自己的用户级页表
所有进程的虚拟地址空间[3GB, 4GB]是不同的,每个进程都有相同的内核级页表
OS运行的本质:都是在进程的虚拟地址空间运行
系统调用的本质:在进程自身地址空间中进行函数跳转并返回即可
OS本质?1.OS是软件,是systemd进程,只是这个进程是死循环 2. OS时钟每个很短时间给OS发送时钟中断,OS执行对应中断处理方法来检测当前进程时钟中断。进程如何被调度?时间片到了,进程对应的上下文等等保存并切换,选择合适用的进程(进程调度就是一个系统函数schedule()来完成的)
问题:既然进程中包含内核地址空间和用户地址空间,那么一个进程不就可以随意访问内核中的代码和数据吗?
这里为了解决这个问题就有了内核态和用户态的出现,怎么来识别身份的呢?CPU中有CR3寄存器,其中3表示用户态,0表示内核态,这里身份切换并不是我们用户来完成的,用户无法更改,所以,OS中的系统调用内部中会修改执行级别,这样就能进行访问内核中的代码了。
5.2 信号捕捉
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码
是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行
main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号
SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler
和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返
回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复
main函数的上下文继续执行了。信号捕捉中用户态和内核态状态转换有四次转换
5.3 sigaction
函数:int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); //检测和更改信号,如果act不为nullptr,signum被设置到act中,如果oldact不为nullptr,之前的act会被保存到oldact中
oldact是输出型参数,act是输入型参数。其中struct sigaction结构体:
struct sigaction { void (*sa_handler)(int); //sa_handler指定与signum相关联的操作,默认操作可以是SIG_DFL,忽略该信号的SIG_IGN,或者指向信号的指针处理函数。 void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; //sa_mask指定在execu‐期间应该被阻塞的信号的掩码(即,添加到调用信号处理程序的线程的信号掩码中)信号处理程序的连接。此外,触发处理程序的信号将被阻塞,除非使用了SA_NODEFER标志。 int sa_flags; //sa_flags指定一组修改信号行为的标志 void (*sa_restorer)(void); };
- 使用
include <iostream> #include <cstring> #include <csignal> #include <cassert> #include <unistd.h> static void printtPending(const sigset_t &pending) { std::cout << "PID:" << getpid() << " pending: "; for(int signalnum = 1; signalnum <= 31; ++signalnum) { if(sigismember(&pending, signalnum)) std::cout << "1"; else std::cout << "0"; } std::cout << std::endl; } //现象:2/3/4/5号信号都被block了 //第一次发送2号信号,此时2号信号正在被自定义处理,在此期间如果再发送2号信号,此时再发送的2号信号就会被暂存处于pending状态,3/4/5号信号也会暂存 //pending信号集是在执行handler之前被置零的 static void handler(int signalnum) { printf("PID:%d catched signalnum:%d\n", getpid(), signalnum); int time = 30; while(time--) { sigset_t pending; sigemptyset(&pending); sigpending(&pending); printtPending(pending); sleep(2); } } int main() { std::cout << "Process PID: " << getpid() << std::endl; struct sigaction act, oldact; memset(&act, 0, sizeof(act)); //初始化 memset(&oldact, 0, sizeof(oldact)); act.sa_handler = handler; //对2信号递达后采用自定义处理动作 act.sa_flags = 0; sigemptyset(&act.sa_mask); //初始化 sigaddset(&act.sa_mask, SIGQUIT); //3号信号屏蔽 sigaddset(&act.sa_mask, SIGILL); //4号信号屏蔽 sigaddset(&act.sa_mask, SIGTRAP); //5号信号屏蔽 int ret = sigaction(SIGINT, &act, &oldact); //检测2号信号 --> 等价于signla(SIGINT) assert(ret == 0); (void)ret; while(true) { sleep(1); } }
运行截图
6. 其他知识
6.1 可重入函数
#include <iostream> #include <signal.h> #include <unistd.h> void handler(int signalnum); typedef struct singleLinkListNode { struct singleLinkListNode* _next; int _val; singleLinkListNode(const int& val) :_val(val) { _next = nullptr; } }node; node* head = new node(0); node node1(1), node2(2); void printLink(node* phead) { node* cur = phead; while(cur) { printf("node:%d->", cur->_val); cur = cur->_next; } std::cout << "nullptr" << std::endl; } void insert(node* newnode) { newnode->_next = head; std::cout << "wait signal......." << std::endl; sleep(10); //10秒期间发送2号信号让其递达执行自定义处理动作 head = newnode; } void handler(int signalnum) { printf("PID:%d, call handler!\n", getpid()); insert(&node2); } int main() { printf("Process PID:%d\n", getpid()); signal(SIGINT, handler); insert(&node1); std::cout << "head: "; printLink(head); std::cout << "node1: "; printLink(&node1); std::cout << "node2: "; printLink(&node2); return 0; }
- 画图理解
运行结果:
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。
- 符合以下条件之一则是不可重入
- 调用了malloc或free,因为malloc也是用全局链表来管理堆的
- 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构
6.2 volatile关键字
#include <stdio.h> #include <signal.h> #include <unistd.h> int quit = 0; void handler(int signalnum) { printf("change quit form 0 to 1\n"); quit = 1; } int main() { printf("Process PID:%d\n", getpid()); signal(SIGINT, handler); while(!quit); //欺骗编译器 printf("normal exit!\n"); return 0; } //makefile valatile_keyword:valatile_keyword.cc g++ -o $@ $^ -std=c++11 .PHONY:clean clean: rm -f valatile_keyword
运行结果:
其实gcc编译器有很多优化选项:-O1、-O2、-O3、-O0 (man gcc查找):
下面换用-O1优化选项进行编译,运行截图:
- 为什么这里-O2选项优化后发送2号信号并不会终止进程呢?
首先要知道上面代码哪里优化了,其实这里while(!quit)这个语句时别优化了,如何优化呢?CPU执行运算的时候,quit初始值为0,那么0就被Load到寄存器中,此时寄存器就是0值,当发送2号信号,quit被赋值变成1,但是这里寄存器中的值并随之改变,所以一直死循环。这里就是一个内存位置不可见的问题,怎么来解决这个问题呢?告诉编译器保证每次检测都要从内存中读取数据,不要让内存数据不可见。解决方法:变量前加上volatile关键字。volatile关键字作用:保证内存可见性。
6.3 SIGCHLD信号
引出:子进程退出,父进程如何得知的呢?父进程阻塞式等待或者非阻塞式等待都需要父进程主动检测,其实子进程退出的时候会向父进程发送SIGCHLD信号,父进程收到信号采用的是忽略的处理方式。验证SIGCHLD信号:
#include <stdio.h> #include <signal.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <stdlib.h> pid_t id; void handler(int signalnum) { sleep(1); printf("catched a signal:%d, who:%d\n", signalnum, getpid()); pid_t result = waitpid(-1, nullptr, 0); //等待任意子进程 if(result > 0) { printf("wait success!, result:%d, id:%d\n", result, id); } } int main() { signal(SIGCHLD, handler); id = fork(); if(id == 0) { int time = 5; while(time--) { printf("child process, PID:%d, PPID:%d\n", getpid(), getppid()); sleep(1); } exit(1); } while(true) { sleep(1); } return 0; }
运行结果:(监控脚本:examine.sh,使用:bash examine.sh)
场景:假如如果有多个子进程同时退出呢?多个子进程同时退出会发送多个SIGCHLD信号,但是这里父进程的信号集中的SIGCHLD信号只有一个比特位来标记,所以此时就需要循环等待子进程来回收所有子进程(基于信号回收进程):
#include <stdio.h> #include <signal.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <stdlib.h> pid_t id; void handler(int signalnum) { sleep(1); printf("catched a signal:%d, who:%d\n", signalnum, getpid()); while (true) //循环回收 { pid_t result = waitpid(-1, nullptr, WNOHANG); //等待回收子进程 if (result > 0) { printf("wait success!, result:%d, id:%d\n", result, id); } else { break; } } printf("handler done!\n"); } int main() { signal(SIGCHLD, handler); for (int i = 0; i < 5; ++i) //创建5个子进程 { id = fork(); if (id == 0) { int time = 5; while (time--) { printf("child process, PID:%d, PPID:%d\n", getpid(), getppid()); sleep(1); } exit(1); } } while (true) { sleep(1); } return 0; }
优雅的处理僵尸进程,直接让操作系统回收,而不是父进程等待回收(只保证Linux下有效):
#include <stdio.h> #include <signal.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <stdlib.h> pid_t id; int main() { signal(SIGCHLD, SIG_IGN); //收到SIGCHLD信号默认处理动作为忽略 for (int i = 0; i < 5; ++i) { id = fork(); if (id == 0) { int time = 5; while (time--) { printf("child process, PID:%d, PPID:%d\n", getpid(), getppid()); sleep(1); } exit(1); } } while (true) { sleep(1); } return 0; }