Linux进程信号(五)之捕捉信号
文章目录
- 捕捉信号
- 信号是什么时候被处理的?
- 内核如何实现信号的捕捉
- sigaction
- 代码应用
- 可重入函数
- volatile
- SIGCHLD信号
捕捉信号
信号是什么时候被处理的?
内核态:允许进程访问操作系统的代码和数据!
用户态:只能访问(进程)自己的代码和数据!
当进程从内核态返回到用户态的时候,进行信号的检测和处理!
返回之前,还处于内核态,而且这时候更重要的事已经做完了!
CPU在跑代码时,不仅仅跑用户自己写的代码,还跑库函数的代码和操作系统的代码!
调用系统调用,除了调用相应的函数,还需要切换身份:用户身份变成内核身份(或者反过来)。
不允许以内核态的身份来访问用户代码,
防止用户用这种方式用操作系统的权限做非法操作。
身份切换就是将ECS寄存器的低两位比特位改变0->3/3->0
内核如何实现信号的捕捉
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。
由于信号处理函数的代码是在用户空间的,处理过程比较复杂,
举例如下:
用户程序注册了SIGQUIT
信号的处理函数sighandler
。
当前正在执行main函数,这时发生中断或异常切换到内核态。
在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT
递达。
内核决定返回用户态后不是恢复main
函数的上下文继续执行,
而是执行sighandler
函数,sighandler
和main
函数使用不同的堆栈空间,
它们之间不存在调用和被调用的关系,是 两个独立的控制流程。
sighandler
函数返回后自动执行特殊的系统调用sigreturn
再次进入内核态。
如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
sigaction
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
sigaction函数可以读取和修改与指定信号相关联的处理动作。
调用成功则返回0,出错则返回-1。
signo是指定信号的编号。
若act指针非空,则根据act修改该信号的处理动作。(输入型参数)
若oact指针非空,则通过oact传出该信号原来的处理动作。(输出型参数)
act
和oact
指向sigaction
结构体:
将sa_handler
赋值为常数SIG_IGN
传给sigaction
表示忽略信号,
赋值为常数SIG_DFL
表示执行系统默认动作,
赋值为一个函数指针表示用自定义函数捕捉信号,
或者说向内核注册了一个信号处理函数,
该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,
这样就可以用同一个函数处理多种信号。
显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。
代码应用
使用sa_handler
#include<iostream>
#include<unistd.h>
#include<cstdlib>
#include<signal.h>
#include<cstring>using namespace std;void handler(int signo)
{cout<<"catch a signo: "<<signo<<endl;
}int main()
{struct sigaction act,oact;memset(&act,0,sizeof(act));memset(&oact,0,sizeof(oact));act.sa_handler=handler;sigaction(2,&act,&oact);while(1){cout<<"I am a process: "<<getpid()<<endl;sleep(1);}return 0;
}
问题1:什么时候将捕捉的信号的pending
位图由1->0
的呢?
思路:在handler信号捕捉执行完之前打印pending位图。
如果pending位图是 0000 0000 0000 0000 0000 0000 0000 0000
那么说明位图的改变是在捕捉信号之前
否则位图的改变就是在捕捉信号之后
#include<iostream>
#include<unistd.h>
#include<cstdlib>
#include<signal.h>
#include<cstring>using namespace std;//问题1:pending位图,什么时候由1->0
//思路:在handler信号捕捉执行完之前打印pending位图。
//如果pending位图是 0000 0000 0000 0000 0000 0000 0000 0000
//那么说明位图的改变是在捕捉信号之前
//否则位图的改变就是在捕捉信号之后
void PrintPending()
{sigset_t set;sigemptyset(&set);sigpending(&set);for(int i=31;i>=1;i--){if(sigismember(&set,i)){cout<<"1";}else{cout<<"0";}}cout<<endl;
}void handler(int signo)
{PrintPending();cout<<"catch a signo: "<<signo<<endl;
}int main()
{struct sigaction act,oact;memset(&act,0,sizeof(act));memset(&oact,0,sizeof(oact));act.sa_handler=handler;sigaction(2,&act,&oact);while(1){cout<<"I am a process: "<<getpid()<<endl;sleep(1);}return 0;
}
结论:在处理信号之前,pending
位图就已经由1->0
了。
问题二:信号被处理的时候,对应的信号也会被添加到block表中,防止信号被嵌套捕捉!
思路:
在信号捕捉(处理)方法里面写一个死循环,
那么就一直是在捕捉信号的过程,
再来一个2号信号,因为2号信号的block
位图是1,所以只能阻塞(不能递达),
所以2号信号的pending
由0->1
结论:
当某个信号的处理函数被调用时,
内核自动将当前信号加入进程的信号屏蔽字,
防止该信号被嵌套捕捉,
当信号处理函数返回时自动恢复原来的信号屏蔽字,
这样就保证了在处理某个信号时,
如果这种信号再次产生,
那么它会被阻塞到当前处理结束为止。
在阻塞期间,该信号发送了n次,但是最终也只会被记录一次。
使用sa_mask
如果在调用信号处理函数时,
除了当前信号被自动屏蔽之外,
还希望自动屏蔽另外一些信号,
则用sa_mask
字段说明这些需要额外屏蔽的信号,
当信号处理函数返回时自动恢复原来的信号屏蔽字。
对比实验:
没有手动屏蔽任何信号
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>
#include <cstring>using namespace std;void PrintPending()
{sigset_t set;sigemptyset(&set);sigpending(&set);for (int i = 31; i >= 1; i--){if (sigismember(&set, i)){cout << "1";}else{cout << "0";}}cout << endl;
}void handler(int signo)
{cout << "catch a signo: " << signo << endl;while (1){PrintPending();sleep(1);}//该循环就是在处理信号
}int main()
{struct sigaction act, oact;memset(&act, 0, sizeof(act));memset(&oact, 0, sizeof(oact));// sigemptyset(&act.sa_mask);// sigaddset(&act.sa_mask,1);// sigaddset(&act.sa_mask,3);// sigaddset(&act.sa_mask,4);act.sa_handler = handler;//SIG_DFL SIG_IGNsigaction(2, &act, &oact);while (1){cout << "I am a process: " << getpid() << endl;sleep(1);}return 0;
}
手动屏蔽1,3,4号信号
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,1);
sigaddset(&act.sa_mask,3);
sigaddset(&act.sa_mask,4);
sa_flags
字段包含一些选项,本章的代码都把sa_flags
设为0,
sa_sigaction
是实时信号的处理函数。
可重入函数
main
函数调用insert
函数向一个链表head
中插入节点node1
,
插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,
再次回用户态之前检查到有信号待处理,于是切换到sighandler
函数,
sighandler
也调用insert
函数向同一个链表head
中插入节点node2
,
插入操作的两步都做完之后从sighandler
返回内核态,
再次回到用户态就从main
函数调用的insert
函数中继续往下执行,
先前做第一步之后被打断,现在继续做完第二步。
结果是,main
函数和sighandler
先后向链表中插入两个节点,
而最后只有一个节点真正插入链表中了。
像上例这样,insert
函数被不同的控制流程调用,、
有可能在第一次调用还没返回时就再次进入该函数,这称为:重入,
insert
函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为:不可重入函数,
反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant
) 函数。
如果一个函数符合以下条件之一则是不可重入的:
调用了malloc
或free
,因为malloc
也是用全局链表来管理堆的。
调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
volatile
保存内存可见性!!
该关键字在C当中我们已经有所涉猎,今天我们站在信号的角度重新理解一下
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>
#include <cstring>using namespace std;
int flag=0;void handler(int signo)
{cout<<"catch a sig: "<<signo<<endl;flag=1;
}int main()
{signal(2,handler);while(!flag);cout<<"process normal quit!"<<endl;return 0;
}
标准情况下,键入 CTRL-C
,2号信号被捕捉,执行自定义动作,
修改 flag=1
, while
条件不满足,退出循环,进程退出。
因为main
函数和handler
函数是两个不同的执行流,
flag
的值在main函数中没有修改flag
,main
函数只对flag
进行了逻辑判断(检测),
算数运算/逻辑运算都是在cpu
内部进行的,
所以在优化条件下,flag
变量可能直接被优化到寄存器中。
优化
mysignal:mysignal.ccg++ -o $@ $^ -O3 -std=c++11
.PHONY:clean
clean:rm -rf mysignal
优化情况下,键入 CTRL-C
,2号信号被捕捉,执行自定义动作,
修改 flag=1 ,但是 while 条件依旧满足,进程继续运行!
但是很明显flag肯定已经被修改了,但是为何循环依旧执行?
很明显, while 循环检查的flag,
并不是内存中最新的flag,这就存在了数据二异性的问题。
while 检测的flag其实已经因为优化,被放在了CPU寄存器当中。
如何解决呢?很明显需要 volatile
volatile int flag=0;
volatile
作用:防止过度优化,保持内存的可见性,
告知编译器,被该关键字修饰的变量,不允许被优化,
对该变量的任何操作,都必须在真实的内存中进行操作。
SIGCHLD信号
之前讲过用wait
和waitpid
函数清理僵尸进程,
父进程可以阻塞等待子进程结束,
也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。
采用第一种方式,父进程阻塞了就不能处理自己的工作了;
采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。
其实,子进程在终止时会给父进程发SIGCHLD
信号,
该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD
信号的处理函数,
这样父进程只需专心处理自己的工作,不必关心子进程了,
子进程终止时会通知父进程,父进程在信号处理函数中调用wait
清理子进程即可。
验证:在子进程退出时,父进程会收到17号信号
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>
#include <cstring>using namespace std;void handler(int signo)
{cout<<"I am "<<getpid()<<" catch a signal: "<<signo<<endl;
}int main()
{signal(17,handler);pid_t id=fork();if(id==0){while(1){cout<<"I am child , pid: "<<getpid()<<" , ppid: "<<getppid()<<endl;sleep(1);break;}cout<<"child quit!!!!!!!!!!"<<endl;exit(1);}while(1){cout<<"I am father , pid: "<<getpid()<<endl;sleep(1);}return 0;
}
父进程在进行等待的时候,我们可以采用基于信号的方式异步等待。
等待的好处:
1.获取子进程的退出状态,释放子进程的僵尸。
虽然我们不知道父子进程谁先运行,但是我们知道父进程一定最后退出!
还是得调用 wait/waitpid
这样的接口!
并且要保证父进程是一直在运行的。(防止子进程变孤儿)
请编写一个程序完成以下功能:父进程fork
出子进程,
子进程调用exit
终止,父进程自定义SIGCHLD
信号的处理函数,
在其中调用wait/waitpid
等待子进程退出。
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>
#include <cstring>
#include<sys/wait.h>
#include<sys/types.h>using namespace std;void handler(int signo)
{sleep(3);pid_t rid=waitpid(-1,nullptr,0);cout<<"I am "<<getpid()<<" catch a signal: "<<signo<<" ,child quit: "<<rid<<endl;
}int main()
{signal(17,handler);pid_t id=fork();if(id==0){while(1){cout<<"I am child , pid: "<<getpid()<<" , ppid: "<<getppid()<<endl;sleep(5);break;}cout<<"child quit!!!!!!!!!!"<<endl;exit(1);}while(1){cout<<"I am father , pid: "<<getpid()<<endl;sleep(1);}return 0;
}
如果我们有5个子进程,
同时退出?
那么在捕捉方法里设置一个循环,不断等待子进程退出即可。
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>
#include <cstring>
#include <sys/wait.h>
#include <sys/types.h>using namespace std;void handler(int signo)
{sleep(3);pid_t rid;while ((rid = waitpid(-1, nullptr, WNOHANG)) > 0){cout << "I am " << getpid() << " catch a signal: " << signo << " ,child quit: " << rid << endl;}
}int main()
{signal(17, handler);for (int i = 0; i < 5; i++){pid_t id = fork();if (id == 0){while (1){cout << "I am child , pid: " << getpid() << " , ppid: " << getppid() << endl;sleep(5);break;}cout << "child quit!!!!!!!!!!" << endl;exit(1);}}while (1){cout << "I am father , pid: " << getpid() << endl;sleep(1);}return 0;
}
退出一半?
在等待的时候,使用非阻塞等待,就不会一直卡在没有退出的子进程上了。
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>
#include <cstring>
#include<ctime>
#include <sys/wait.h>
#include <sys/types.h>using namespace std;void handler(int signo)
{sleep(3);pid_t rid;while ((rid = waitpid(-1, nullptr, WNOHANG)) > 0){cout << "I am " << getpid() << " catch a signal: " << signo << " ,child quit: " << rid << endl;}
}int main()
{srand(time(nullptr));signal(17, handler);for (int i = 0; i < 5; i++){pid_t id = fork();if (id == 0){while (1){cout << "I am child , pid: " << getpid() << " , ppid: " << getppid() << endl;sleep(5);break;}cout << "child quit!!!!!!!!!!" << endl;exit(1);}sleep(rand()%5+3);}while (1){cout << "I am father , pid: " << getpid() << endl;sleep(1);}return 0;
}
事实上,由于UNIX
的历史原因,要想不产生僵尸进程还有另外一种办法:
父进程调 用sigaction
将SIGCHLD的处理动作置为SIG_IGN
,
这样fork出来的子进程在终止时会自动清理掉,
不会产生僵尸进程,也不会通知父进程。
系统默认的忽略动作和用户用sigaction
函数自定义的忽略
通常是没有区别的,但这是一个特例。
此方法对于Linux可用,但不保证在其它UNIX系统上都可用。
请编写程序验证这样做不会产生僵尸进程。
测试代码
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>
#include <cstring>
#include <ctime>
#include <sys/wait.h>
#include <sys/types.h>using namespace std;int main()
{signal(17, SIG_IGN);// SIG_DFL -> IGNfor (int i = 0; i < 5; i++){pid_t id = fork();if (id == 0){while (1){cout << "I am child , pid: " << getpid() << " , ppid: " << getppid() << endl;sleep(3);break;}cout << "child quit!!!!!!!!!!" << endl;exit(1);}sleep(1);}while (1){cout << "I am father , pid: " << getpid() << endl;sleep(1);}return 0;
}