赞
踩
目录
世上有两种耀眼的光芒,一种是正在升起的太阳,一种是正在努力学习编程的你!一个爱学编程的人。各位看官,我衷心的希望这篇博客能对你们有所帮助,同时也希望各位看官能对我的文章给与点评,希望我们能够携手共同促进进步,在编程的道路上越走越远!
提示:以下是本篇文章正文内容,下面案例可供参考
信号:Linux系统提供得一种,向指定进程发送特定事件的方式,做识别和处理。信号的产生是异步的。
我们把1~31号的信号叫为普通信号,将34~64的信号叫为实时信号,我们只考虑1~31号的普通信号。
- typedef void(*sighandler_t)(int);
- // 返回值为void,参数为int的函数指针类型
- sighandler_t signal(int signum,sighandler_t handler);
- // 捕捉对应的信号,进程收信号,会执行handler函数对应的动作
- // handler:对指定信号进行捕捉的方法
- void handler(int sig)
- {
- std::cout << "get a sig: " << sig << std::endl;
- }
- int main()
- {
- signal(2, handler);
- while (true)
- {
- std::cout << "hello bit, pid: " << getpid() << std::endl;
- sleep(1);
- }
- }
对信号的自定义捕捉,我们只要捕捉一次,后续一直有效。
我们用signal()函数捕捉2号信号,那么2号信号一直不产生呢?
可不可以对更多的信号进行捕捉?
2)SIGINT默认是什么动作呢?
2) SIGINT是什么呢?
ctrl + \ ---> 3)SIGQUIT (终止信号)
- man 2 kill
- int kill(pid_t pid,int sig);
- // 向指定的进信发送指定的信号
process.cc
- void handler(int sig)
- {
- std::cout << "get a sig: " << sig << std::endl;
- }
- int main()
- {
- signal(2, handler);
- while (true)
- {
- std::cout << "hello bit, pid: " << getpid() << std::endl;
- sleep(1);
- }
- }
testsig.cc
- // ./mykill 2 1234
- int main(int argc, char *argv[])
- {
- // 对命令行参数作处理
- if(argc != 3)
- {
- std::cerr << "Usage: " << argv[0] << " signum pid" << std::endl;
- return 1;
- }
- pid_t pid = std::stoi(argv[2]);
- int signum = std::stoi(argv[1]);
- kill(pid, signum);
- }
打开两个终端,一个终端执行myprocess可执行程序,另一个终端执行mykill可执行程序(./mykill 2 pid(myprocess进程的pid))。
- man 7 signal
- // 查看信号的默认动作
如何理解信号的发送和保存?
通过kill命令,向指定的进程发送指定的信号。
比如:ctrl + c ---> 2)SIGINT、ctrl + \ ----> 3)SIGQUIT
- man 2 kill
- int kill(pid_t pid,int sig);
- // 向指定的进信发送指定的信号
- man raise
- int raise(int sig);
- // 谁调用raise()函数,就给谁发一个指定的信号
- int main()
- {
- while (true)
- {
- sleep(1);
- std::cout << "hello bit, pid: " << getpid() << std::endl;
- raise(3);
- // 本来就是每隔1秒给本进程发送一个3号信号,进程直接终止
- }
- }
- // 被signal()函数捕捉之后,进程便不会终止了,而是会执行hnadler()函数动作
有一种特殊的情况:如果我把所有的信号都捕捉了?
- void handler(int sig)
- {
- std::cout << "get a sig: " << sig << std::endl;
- }
- int main()
- {
- // 将31种信号全部捕捉,换成handler()函数的方法
- for(int i = 1; i <= 31; i++)
- signal(i, handler);
- while (true)
- {
- sleep(1);
- std::cout << "hello bit, pid: " << getpid() << std::endl;
- }
- }
如果我们把所有的信号都捕捉了,比如:2、3信号默认是执行终止进程的操作的,但是我们让signal()函数对信号进行捕捉,那么信号原来的默认动作就变为handler()函数的动作了,但是此时handler()函数什么都不做,那么再次发送终止信号(kill -2 pid等),进程是退不出去的,那就出问题了。系统也考虑到了这一点,就让9号信号不允许自定义捕捉,9号信号可以杀掉进程。
- man abort
- // 一般终止一个进程时,用man()函数中的return;
- // exit()正常终止;
- // 也可以使用abort()函数把进程异常的终止掉。
- void handler(int sig)
- {
- std::cout << "get a sig: " << sig << std::endl;
- }
- int main()
- {
- int cnt = 0;
- signal(SIGABRT, handler);
- while (true)
- {
- sleep(1);
- std::cout << "hello bit, pid: " << getpid() << std::endl;
- abort();
- // abort()函数异常终止进程,尽管用signal()函数捕捉SIGABRT信号,该进程依然要被终止
- }
- }
我们之前所学的管道,把读端关闭,写端一直在进行,OS会给写端的进程发送 13)SIGPIPE的信号,软件条件触发了信号的产生。
我们再来讲解一个alarm()系统调用的接口。
- unsigned int alarm(unsigned int seconds);
- // 过seconds秒之后,会给进程发送一个 14) SIGALRM 的信号
- void handler(int sig)
- {
- alarm(1);// 每隔1秒就再次触发一次闹钟(实现了闹钟循环)
- std::cout << "cnt: " << cnt << " get a sig: " << sig << std::endl;
- // std::cout << "get a sig: " << sig << std::endl;
- //exit(1);
- }
- int main()
- {
- signal(SIGALRM, handler);
- alarm(1); // 设定1S后的闹钟 -- 1S --- SIGALRM(终止的意思)
- // sleep(4);
- // 在我们所在的代码中闹钟只能设置一次,再次设置闹钟,就是对上一次设置的闹钟进行重置
- // int n = alarm(2); // alarm(0): 表示取消闹钟, n:返回上一个闹钟的剩余时间
- // std::cout << "n : " << n << std::endl;
- // sleep(10);
- while (true)
- {
- std::cout << "cnt: " << cnt << std::endl;// 边计算边打印,只能打印八万多次
- cnt++;// 只做++,是一个纯内存级的数据递增,1秒过后,打印显示的结果,cnt++之后有五亿左右次
- sleep(1);
- }
- }
- void handler(int sig)
- {
- std::cout << "get a sig: " << sig << std::endl;
- }
- int main()
- {
- // 程序为什么会崩溃???非法访问、操作(?), 导致OS给进程发送信号啦!! --- 为什么
- // signal(SIGSEGV, handler);
- // signal(SIGFPE, handler);
- // 崩溃了为什么会退出?默认是终止进程
- // 可以不退出吗?可以,捕捉了异常, 推荐终止进程(为什么?) --- 为什么?
- // int *p = nullptr;
- // *p = 100; // 11) SIGSEGV
- int a = 10;
- a /= 0; // 8) SIGFPE
- while (true)
- {
- std::cout << "hello bit, pid: " << getpid() << std::endl;
- sleep(1);
- }
- }
程序为什么会崩溃???非法访问、操作(?), 导致OS给进程发送信号啦!!
程序崩溃,OS为什么会给目标进程发送信号,OS怎么知道进程崩溃了?
我们来拿下面的代码看一下:
- int a = 10;
- 10 /= 0; // 除零操作
算数运算和逻辑运算一般是CPU来进行运算,
一般进程级别的数据运算都是交给CPU,CPU中有许多的寄存器,有一个状态寄存器eflag,CPU对数据进行运算,数据是来源于用户的,所以就注定了有些运算是正确的,有些运算是错误的,那么CPU如何得知运算是正常的还是异常的呢?
CPU里面有状态寄存器(eflag),eflag里面有溢出标记位(比特位),为0表示没有溢出,为1表示溢出了。CPU在运算10/0的除法运算会被转换成加法,CPU内的加法器会一直做累加,累加到一定程度后,会发生数据溢出,所以溢出标记位被置为1。如果CPU将10/0运算结束后,溢出标记位为0,没有溢出,那么10/0的运算是可信的,所以会把计算结果再写回内存;反之,溢出了,那么CPU会标定出10/0的运算出错了。
因为OS是软硬件资源的管理者,eflag状态寄存器中有溢出标记位,有错误信息,OS要随时处理这种硬件的问题,因此OS会向目标进程发送信号,发送信号就是修改结构体对象的变量中8号信号所对应的位图,将0~1。
崩溃了为什么会退出?
默认是终止进程。
可以不退出吗?可以,捕捉了异常,但是终端会一直无限死循环的打印异常信号对应的编号,为什么?
我们再来看下面的代码:
- int *p = nullptr;
- *p = 100;
- // 野指针的访问
- // 11) SIGSEGV
程序当中出现的异常,最终都会体现在硬件上,而硬件上的问题最终都会被操作系统识别到。
默认在云服务器上,向我们的系统当中形成的debug文件,功能默认是关闭的,怎么查看它是被关闭的呢?
- ulimit -a
- // 查看OS对于普通用户能使用资源对应的一些限制
打开被关闭的core文件:
block:是单位 -c:是core文件的选项
ulimit -c 10240(大小)
再次执行文件时,会发现错误信号后面多了一个(core dumped标记位),而且在当前目录下形成了一个core文件一旦把core file size文件打开了,就允许我们在服务器上进行core dumped。
core文件是什么呢?
当一个进程在运行时,出现了异常(除零/野指针等),进程其实没有退出,OS会把当前进程出异常的整个内存级的核心的代码和数据给我们dump(转储)到当前进程的工作目录上(磁盘),形成一个core文件,这个功能叫核心转储(core dump)。
core dump标志位只有一个比特位,为0表示没有核心转储;为1表示有核心转储。
服务器为什么要默认关闭核心存储?
在一些老的ubuntu版本中,用core动作来终止异常的程序,并打开core文件,那么每一次执行程序时,都会发生核心转储,都会生成core.pid的文件,举一个例子:一般程序挂掉了之后,程序就得立马重启,那么此时程序一重启,就会报错,core动作终止程序,如果重启了一晚上,那么生成了不知道多少个core.pid的文件,内存会被耗干,机器可能都会开不起来了,所以一般OS会将core file size文件关闭的,不过现在新的版本有改进,就算是将core file size文件打开了,也只会核心转储到一个core文件,文件不会再加pid为后缀,只要发生一次核心转储,就会将core文件更新一下。
如何打开Linux的core功能呢?
通过 ulimit -a 查看到 core file size 文件的大小的选项,用 ulimit -c 10240(用一个合适的大小) 打开core文件大小的选项,从而打开了Linux中的core功能,开启了Linux的core dump功能。
为什么有core的功能(就是core文件)?
想通过core定位到进程为什么退出,以及执行到哪行代码退出的。
什么是core的功能(core文件)?
将进程在内存运行中的核心数据(与调试有关)转储到磁盘中,以core、core.pid文件的形式保存下来,通过这两个文件可以定位到进程为什么退出,以及执行到哪行代码退出的。
core的功能(core文件)有什么用呢?
协助我们进行调试。
我们的笔记本电脑或者是台式电脑,我们将电源关闭之后,停两天之后,再次打开,会发现我们电脑上的时间依旧是正确的,为什么呢?
我们将电脑的电源断开之后,我们电脑的主板上,还有一个小拇指盖大小的纽扣电池的,这颗电池是一直维护电脑的时间,给主板供电,会一直统计时间,利用时间戳来进行时间的比对。
时间戳:从1970年的1月1日开始,时间呈线性递增的。
如何区分键盘输入的是字符,还是命令?
键盘驱动和OS进行联合解释的。
OS怎么知道键盘在输入数据了?
通过一种硬件中断的技术。
OS不是主动去键盘中读取数据的,而是被动读取的。
通过读取键盘数据的方法可以读取到键盘的数据,之后让OS来进行判定,是字符的话,就拷贝到文件对应的文件级缓冲区中;是控制命令的话,比如:ctrl + c 解释成为2号信号,OS把2号信号给对应的进程,让该进程终止。
- pending位图
- 0000 0000 0000 0000 0000 0000 0000 000 0
比特位的位置,表示信号编号,比特位的内容,是否收到指定的信号。
- block位图
- 0000 0000 0000 0000 0000 0000 0000 000 0
比特位的位置,表示信号的编号,比特位的内容,是否阻塞该信号。
如果一个信号被阻塞(屏蔽),则该信号永远不会被递达处理,除非解除阻塞。
阻塞一个信号,和是否收到了指定的信号,有关系吗?没关系。
每个进程在自己的PCB内,都维护了三张表:block表、pending表、方法表。
当我们收到了一个指定的信号之后,我们会根据信号的编号,直接去索引函数指针表,就可以找到对应信号的处理方法。
信号在内核中的表示示意图
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印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);
- // 有没有涉及到将数据设置进内核中呢?没有!!!
- // sigset_t是一个OS提供的数据类型(结构体类型),int double float class没有差别
- sigset_t s; // 用户栈上开辟了空间
- sigemptyset(&s);
- sigaddset(&s, 2);// 这里的函数也只是修改了s变量
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
- #include <signal.h>
- int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
- 返回值:若成功则为0,若出错则为-1
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。
第一个参how数有3个选择:
SIG_BLOCK | set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set |
SIG_UNBLOCK | set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set |
SIG_SETMASK | 设置当前信号屏蔽字为set所指向的值,相当于mask=set |
如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
- #include <signal.h>
- int sigpending(sigset_t *set);
- 读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。
- #include <iostream>
- #include <signal.h>
- #include <unistd.h>
- #include <cassert>
- #include <sys/wait.h>
-
- void PrintSig(sigset_t& pending)
- {
- std::cout << "Pending bitmap: ";
- for (int signo = 31; signo > 0; signo--)
- {
- // 判断指定的信号signo是否在指定的pending信号集中
- if (sigismember(&pending, signo))
- {
- std::cout << "1";// 在
- }
- else
- {
- std::cout << "0";// 不在
- }
- }
- std::cout << std::endl;
- }
- void handler(int signo)
- {
- sigset_t pending;
- sigemptyset(&pending);
- int n = sigpending(&pending); // 我正在处理2号信号哦!!
- assert(n == 0);
- // 3. 打印pending位图中的收到的信号
- std::cout << "递达中...: ";
- PrintSig(pending); // 0: 递达之前,pending 2号已经被清0. 1: pending 2号被清0一定是递达之后
- std::cout << signo << " 号信号被递达处理..." << std::endl;
- }
- int main()
- {
- // 对2号信号进行自定义捕捉 --- 不让进程因为2号信号而终止
- signal(2, handler);
- // 1. 屏蔽2号信号
- sigset_t block, oblock;// 设置两个信号集
- // block:新的信号集用于添加信号 oblock:回收老的信号集
- sigemptyset(&block);
- // 因为在栈区中设置的block信号集里面都是随机值,所以要先清空指定的信号集
- sigemptyset(&oblock);
- sigaddset(&block, 2);
- // 将2号信号添加到指定的block信号集中
- // 2) 和 SIGINT 是一样的 --- 添加信号的操作根本就没有设置进当前进程的PCB block位图中
- // (只是在栈区的变量block位图结构中添加了一个2号比特位为1)
- // 0. for test: 如果我屏蔽了所有信号呢???
- // for(int signo = 1; signo <= 31; signo++) // 9 和 19) SIGSTOP 信号无法被屏蔽, 18号信号会被做特殊处理
- // sigaddset(&block, signo); // SIGINT --- 根本就没有设置进当前进程的PCB block位图中
-
-
-
- // 1.1 开始屏蔽2号信号,其实就是设置进入内核中
- int n = sigprocmask(SIG_SETMASK, &block, &oblock);
- // SIG_SETMASK:重置成新的信号屏蔽集
- // 参数3:将原先在 block 位图中的信号屏蔽字放在 oblock 信号集中保存起来,未来恢复可以使用
- // 成功返回0,失败返回-1
- assert(n == 0); // debug版本是执行的,release版本是直接优化了
- // 使用断言证明sigprocmask()函数执行成功
- // (void)n; // 骗过编译器,不要告警,因为我们后面用了n,不光光是定义
-
- std::cout << "block 2 signal success" << std::endl;
- std::cout << "pid: " << getpid() << std::endl;
- int cnt = 0;
- // 刚开始获得到的pending位图中的比特位都是0,后来在block位图中添加了一个2号信号,因为2号信号被屏蔽掉了,
- // 所以pending位图中的2号比特位始终为1
- while (true)
- {
- // 2. 获取进程的pending位图
- sigset_t pending;
- sigemptyset(&pending);
- n = sigpending(&pending); // 将进程的pending位图通过输出型参数带出来
- assert(n == 0);
- // 3. 打印pending位图中收到的信号
- PrintSig(pending);
- cnt++;
- // 4. 解除对2号信号的屏蔽
- if (cnt == 20)
- {
- std::cout << "解除对2号信号的屏蔽" << std::endl;
- n = sigprocmask(SIG_UNBLOCK, &block, &oblock); // 2号信号会被立即递达, 默认处理是终止进程
- assert(n == 0);
- }
- // 我还想看到pending 2号信号 1->0 : 递达二号信号!
- sleep(1);
- }
- return 0;
- }
细节:
进程从内核态,切换回用户态的时候,信号会被检测并处理。
我们写代码的时候,会直接或间接的使用过系统调用,那么进程陷入内核,OS执行系统调用,结果给用户。
我自己写的代码里没有任何的系统调用,此时属于用户态,
但是进程是会被调度的,因为进程是有时间片的,那么当时间到了之后,进程就得把CPU资源出让出来,让OS进行一个对应的调度运行,此时出让CPU的过程,就是让进程从用户态陷入到内核态,OS执行它的调度算法,当进程在被唤醒到CPU上时,进程就从内核态切换到用户态,继续让进程运行起来,所以说即使代码中没有系统调用,进程依然有大量的内核态和用户态之间的切换。
在信号处理的过程(捕捉)中,一共会有4次状态切换(内核态和用户态)。
以前我们系统调用,都是执行OS给我们提供的方法;
今天能不能不做状态变化了,让OS执行用户的自定义方法呢?(不改变进程的内核态)
从技术角度:OS以内核态的身份,想执行你自定义写的代码,是可以做到的,OS的权限很高,OS能检测进程内核级的数据结构,内核的数据结构中的handler表中就包含了自定义方法的地址;
可以为什么不让内核态执行自定义的方法呢?
自定义的方法是用户写的,OS是不相信任何人的,直接表现就是:不让任何人直接访问内核的数据结构,只能让用户通过系统调用来访问,同时OS也不会以内核态的身份去执行你写的自定义的代码,万一用户所写的信号捕捉的方法是越权的非法操作呢?所以让用户态和内核态各管各的。
信号 --- 杀掉这个进程 --- SIG_DFL (大部分的信号都是杀掉进程的,还有一些是暂停和忽略进程)
通过信号杀掉进程,为什么不直接杀掉这个进程,还要向进程的pending位图里对应位置写一个1,在合适的时候,由进程自己来决定要不要退出?
退出一个进程时,这个进程可能在做更重要的事情,直接杀掉进程可能会导致一些未定义的错误。
OS也是软件,是第一个被加载到物理空间中的,被加载的OS是通过内核级页表映射到进程地址空间的[3,4]G的内核空间中。所以之前访问的内核数据结构都是通过内核级页表的映射关系来进行查找的。
OS内部的系统调用本质其实是一个函数指针数组。数组中的每一个元素都指向系统调用。数组的下标称为系统调用号。
执行一个系统调用:
比如:在我们的正文代码当中,调用了系统调用,将来在跳转时,拿着系统调用的编号以及函数指针数组表的起始虚拟地址,就可以跳转到OS内部,在系统调用表中索引,找到对应的系统调用,让OS执行对应的方法,执行完毕之后,再返回正文代码部分,这就完成了一个系统调用了。
OS内会有多个进程,每个进程都有自己的PCB、进程地址空间、代码、数据和用户级页表,是互不相关的;但是OS的代码和数据只有一份,所以对于不同的进程,通过进程地址空间中的内核空间可以访问同一份的内核级页表,使用同一份OS的代码和数据。
结论:
进程是有时间片的,到一定时间之后,进程将出让CPU资源,CPU怎么出让资源的呢?
找到OS,执行OS内相关的代码,把CPU里的上下文数据保存到该进程的PCB中,并把该进程状态设置为S,放到其它的队列里,再让CPU执行新的进程。
信号技术本来就是通过软件的方式,来模拟的硬件中断。
谁让OS运行起来的呢?
硬件非常高频率的,每隔非常短的时间,就给CPU发送中断 --- CPU不断地进行处理中断。
中断来了,OS就会把当前进程停下来,然后通过当前的进程地址空间,找到物理内存中OS提供的中断向量表,OS根据中断号找到执行方法,调度该方法。我们把非常高频率的给CPU发送中断,成为OS的周期时钟中断。
操作系统是一个死循环,不断的在接受外部的其它硬件中断。
CPU中的CR3寄存器指向整个当前任何一个进程对应的页表,CPU中的CS寄存器中的低两位当中有两个比特位,这两个比特位我们称之为权限标识位,两个比特位为0,表示:内核态;为3,表示:用户态。CPU加系统整体会为我们标识当前进程是处于用户态,还是内核态,有了标识,当我们执行系统调用跳转的时候,OS会对当前的用户身份进行审核,如果是0,就允许访问OS的代码和数据,反之,则不能。
写实拷贝只会发生在进程地址空间中的用户空间中的。
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:用户程序注册了SIGQUIT信号的处理函数sighandler。当前正在执行 main函数,这时发生中断或异常切换到内核态。在中断处理完毕后要返回用户态的main函数之前检查到有信号 SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler 和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。如果没有新的信号要递达,这次再返回用户态就是恢复 main函数的上下文继续执行了。
- #include <signal.h>
- int sigaction(int signo, const struct sigaction* act, struct sigaction* oact);
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask(sigaction结构体中的信号集,信号集的是类型sigset_t)字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。sa_flags字段包含一些选项,本章的代码都把sa_flags设为0,sa_sigaction是实时信号的处理函数。
如何证明某个信号的的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字?
- void Print(sigset_t &pending)
- {
- std::cout << "curr process pending: ";
- for(int sig = 31; sig >= 1; sig--)
- {
- // 判断指定的信号sig是否在指定的pending信号集中
- if(sigismember(&pending, sig)) std::cout << "1";
- else std::cout << "0";
- }
- std::cout << "\n";
- }
-
- // 自定义类型的方法
- void handler(int signo)
- {
- std::cout << "signal : " << signo << std::endl;
- // 不断获取当前进程的pending信号集合并打印
- sigset_t pending;
- sigemptyset(&pending);// 对pending位图清空,所以打印出来的都是0(原来打印出来的2号比特位为1)
- while(true)
- {
- // 获取当前进程的pending信号集
- sigpending(&pending);
- Print(pending);
- sleep(1);
- }
- }
- int main()
- {
- struct sigaction act, oact; // 创建两个结构体对象
- act.sa_handler = handler; // 你要执行的自定义类型的方法
- act.sa_flags = 0;
- sigemptyset(&act.sa_mask); // 对指定的信号集清空
- sigaddset(&act.sa_mask, 3); // sa_mask信号集:默认处理一个信号时,当前信号也会自动屏蔽
- sigaddset(&act.sa_mask, 4);
- sigaddset(&act.sa_mask, 5);
- // 在指定的信号集sa_mask中添加3、4、5号信号,在处理2号信号时,就会把2、3、4、5信号都屏蔽掉
- sigaction(2, &act, &oact); // 用act结构体中的handler()方法对2号信号进行捕捉
- // 修改的是进程当中的handler()表,拿着2号作为数组下标,
- // 直接把你对应的handler方法设置进2号下标对应的函数指针数组当中,就可以对2号信号进行自定义的捕捉
- while(true) sleep(1);
- return 0;
- }
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,如果我们处理完对应的信号,该信号默认也会从信号屏蔽字中移除。是为了不想让信号,嵌套式进行插足处理。
比如:我正在处理2号信号,2号信号的处理方法特别长,2号的处理方法里面也有系统调用,陷入内核,再检测,那么在处理2号信号期间,不断有大量的2号信号发来,呈递归式的处理,OS不允许这样的事情发生(可能会发生栈溢出);所以,在处理2号信号时,将2号信号屏蔽掉,那么在处理2号期间,就不会再有2号信号递达。这样的话,对2号信号的处理就变成了串行处理。
如果一个函数符合以下条件之一则是不可重入的:
- #include <stdio.h>
- #include <unistd.h>
- #include <signal.h>
-
- // volatile int g_flag = 0;
- int g_flag = 0; // 任何变量的定义都在内存当中的
-
- void changeflag(int signo)
- {
- // 我们不想使用signo这个变量,但是不使用编译器会报错,所以可以写成下面这个形式,用来骗过编译器。
- (void)signo;
- printf("将g_flag,从%d->%d\n", g_flag, 1);
- g_flag = 1;// 修改的是内存当中的值
- }
-
- int main()
- {
- signal(2, changeflag);
-
- // 本来g_flag为0,取反后为1;现在g_flag为1,取反后为0,可以退出循环
- // 我们读取g_flag时,不想再从内存当中读取了;
- // 我们把g_flag放在寄存器当中,每次进行while()循环判断时,就直接从寄存器中读取,不会再重复的在内存中读取数据了;
- // 所以编译器会做优化,一旦进行优化,可能会产生意料之外的变化
- while (!g_flag); // 故意写成这个样子, 编译器默认会对我们的代码进行自动优化!
- // {
- // printf("hello world\n");
- // sleep(1);
- // }
-
- printf("process quit normal\n");
- return 0;
- }
那么如何解决这种问题呢?
采用volatile关键字,volatile修饰g_flag全局变量,作用:编译器不管怎么优化,都禁止编译器把g_flag优化到寄存器里,往后检测和访问g_flag只能从内存中读取数据了,不能从CPU的寄存器中读取,这叫做保持内存的可见性。
- man gcc
- // 查看gcc中有很多的优化级别
- // 比如:gcc test.c -O1 编译器对test.c代码进行O1级别的优化
子进程退出,父进程不wait,子进程就会僵尸。子进程退出,不是默默的退出的。会在退出的时候,向父进程发送信号的,发送SIGCHLD信号,17) SIGCHLD。
如何证明?
- void handler(int signo)
- {
- std::cout << "child quit, father get a signo: " << signo << std::endl;
- }
- int main()
- {
- signal(SIGCHLD, handler);
- pid_t id = fork();
- if (id == 0)
- {
- // child
- int cnt = 5;
- while (cnt--)
- {
- std::cout << "I am child process: " << getpid() << std::endl;
- sleep(1);
- }
- std::cout << "child process died" << std::endl;
- exit(0);
- }
- // father
- while (true)
- sleep(1);
- }
用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。
其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号 的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。
编写一个程序完成以下功能:父进程fork出子进程,父进程自定义SIGCHLD信号的处理函数,在其中调用wait获得子进程的退出状态并打印。
- // 清理子进程
- void CleanupChild(int signo)
- {
- // v1版本
- if (signo == SIGCHLD)
- {
- // -1:等待任意一个子进程
- pid_t rid = waitpid(-1, nullptr, 0);
- if (rid > 0)
- {
- std::cout << "wait child success: " << rid << std::endl;
- }
- }
- std::cout << "wait sub process done" << std::endl;
- // v2版本
- if (signo == SIGCHLD)
- {
- // 回收100个子进程
- while (true)
- {
- pid_t rid = waitpid(-1, nullptr, 0); // -1 : 回收任意一个子进程
- if (rid > 0)
- {
- std::cout << "wait child success: " << rid << std::endl;
- }
- else if(rid <= 0) break;
- }
- }
- std::cout << "wait sub process done" << std::endl;
- // v3版本
- if (signo == SIGCHLD)
- {
- while (true)
- {
- // WNOHANG:非阻塞等待
- pid_t rid = waitpid(-1, nullptr, WNOHANG); // -1 : 回收任意一个子进程
- if (rid > 0)
- {
- std::cout << "wait child success: " << rid << std::endl;
- }
- else if (rid <= 0)
- break;
- }
- }
- std::cout << "wait sub process done" << std::endl;
- }
- // 假如创建了10个子进程,10个子进程统一全部退出,就意味着10个子进程都会给父进程发送SIGCHLD,
- // pending位图每次只会记录一个SIGCHLD,在处理期间,不会管其它的子进程,因此大多子进程的SIGCHLD信号都丢失了。
- int main()
- {
- // 方法一:V1版本、V2版本、V3版本
- signal(SIGCHLD, CleanupChild);
- // signal(SIGCHLD, SIG_IGN);// 方法2:SIGCHLD的处理动作置为SIG_IGN,子进程退出,系统就自动把子进程回收
- // 50个退出,50个没有
- for (int i = 0; i < 100; i++)
- {
- pid_t id = fork();
- if (id == 0)
- {
- // child
- int cnt = 5;
- while (cnt--)
- {
- std::cout << "I am child process: " << getpid() << std::endl;
- sleep(1);
- }
- std::cout << "child process died" << std::endl;
- exit(0);
- }
- }
- // father
- while (true)
- sleep(1);
- }
事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证 在其它UNIX系统上都可用。上面的方法2就是此法。
好了,本篇博客到这里就结束了,如果有更好的观点,请及时留言,我会认真观看并学习。
不积硅步,无以至千里;不积小流,无以成江海。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。