目录
[核心转储(Core Dump):](#核心转储(Core Dump):)
信号概述
信号的概念:
在日常生活中,信号是表示消息的物理量 ,信号是运载消息的工具 ,是消息的载体。
在Linux中,信号同样有上述特征,同时,信号还是Linux系统提供的让一个进程给其他进程发送异步信息的一种方式,属于软中端。
所谓异步即双方不需要共同的时钟,也就是接收方不知道发送方什么时候发送信号 ,接收方在接收信号之前一直在做自己的事 ,两者是并发运行的。
进程看待信号的方式:
1.能识别并处理信号。
2.收到信号时,在做其他更重要的事情,不能及时处理时,可以暂存信号(通过位图)。
3.收到信号时,可以不立即处理,等到合适的时候再处理。
4.信号是随机产生的,无法准确预料,因此信号是异步发送的。
查看信号:
可以使用 kill -l 查看所有信号:
- 每个信号都有一个编号和宏定义的名字 ,即使用信号名称实际上还是使用信号编号。如 3号新号的名成的定义为 #define SIGQUIT 3。
- 没有0号、32号、33号信号,编号34以上的都是实时信号,本文不讨论实时信号。
- 这些信号的产生条件和默认处理动作可以使用 man 7 signal 指令查看。
信号的处理方式:
对信号的处理动作有以下三种:
- 忽略此信号,即不做任何处理,使用 signal(信号编号/信号名称,SIG_IGN)。
- 执行信号的默认处理动作。
- 执行自定义的信号的处理动作,即捕捉,使用signal(信号编号/信号名称,函数指针)。
以2号信号为例,2号信号可以直接使用 kill + -2 + 进程pid 给相应进程发送信号,也可以通过键盘输入 Ctrl+c 触发 。2号信号的默认处理动作是终止进程。
cpp
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
using namespace std;
int main()
{
int count=10;
while (count)
{
count--;
cout<<"count:"<<count<<endl;
sleep(1);
}
cout<<"进程退出......"<<endl;
return 0;
}
可以发现,程序运行起来后,给程序发送2号信号后,程序直接退出了。 这是执行默认处理动作。
cpp
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
using namespace std;
int main()
{
signal(2, SIG_IGN);//忽略2号信号
int count=10;
while (count)
{
count--;
cout<<"count:"<<count<<endl;
sleep(1);
}
cout<<"进程退出......"<<endl;
return 0;
}
可以发现,程序运行起来后,给进程发送2号信号后,程序没有任何处理动作。这是忽略此信号。
cpp
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
using namespace std;
void task(int sig)
{
cout << "I am " << getpid() <<",signal num:"<< sig << endl;
}
int main()
{
signal(2, task);//自定义2号信号处理动作
int count=10;
while (count)
{
count--;
cout<<"count:"<<count<<endl;
sleep(1);
}
cout<<"进程退出......"<<endl;
return 0;
}
可以发现,程序运行起来后,给进程发送2号新号,进程会执行自定义的task指向的函数方法。这是捕捉信号。
信号的产生
通过kill指令产生信号:
可直接使用 kill + -信号编号/信号名称 + 进程pid 给相应进程发送相应信号。要注意信号编号/信号名称前边的 " - "不能省略。
cpp
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
using namespace std;
void task(int sig)
{
cout << "I am " << getpid() <<",signal num:"<< sig << endl;
}
int main()
{
signal(2, task);//自定义2号信号处理动作
signal(3, task);//自定义3号信号处理动作
signal(4, task);//自定义4号信号处理动作
int count=10;
while (count)
{
count--;
cout<<"pid:"<<getpid()<<", count:"<<count<<endl;
sleep(1);
}
cout<<"进程退出......"<<endl;
return 0;
}
通过终端按键产生信号:
当通过键盘输入 Ctrl + c 的组合键时,系统会把它解释为2号信号,再发送给进程;
当通过键盘输入 Ctrl + \ 的组合键时,系统会把它解释为3号信号,再发送给进程;
当通过键盘输入 Ctrl + z 的组合键时,系统会把它解释为19号信号,再发送给进程。
cpp
#include <iostream>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void task(int sig)
{
cout << "Get a signal , signal num:"<< sig << endl;
}
int main()
{
signal(2, task);//自定义2号信号处理动作
signal(3, task);//自定义3号信号处理动作
signal(19, task);//自定义19号信号处理动作
int count=10;
while (count)
{
count--;
cout<<"pid:"<<getpid()<<", count:"<<count<<endl;
sleep(2);
}
cout<<"end......"<<endl;
return 0;
}
观察运行结果可以发现,19号信号无法被自定义捕捉。
通过系统调用产生信号:
给任意进程发送任意信号:
cpp
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
参数:
- pid :要发送信号的目标进程。
- sig :要发送的信号编号/信号名字。
返回值:
- 成功返回 0 ;失败返回 -1,并设置错误码。
cpp
#include <iostream>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
int main(int argc,char* argv[])//自定义实现kill指令
{
if(argc<3)
{
cout << "Usage: " << argv[0] << " -signumber pid" << endl;
return 1;
}
int sig=stoi(argv[1]+1);//把命令行第二个参数转换为整数,跳过 " - "
int pid=stoi(argv[2]);//取出进程pid
int n=kill(pid,sig);
if(n==0)
{
cout<<"mykill success,pid:"<<pid<<endl;
}
else
{
cout<<"mykill falied,errno:"<<errno<<",errno message:"<<strerror(errno)<<endl;
}
return 0;
}
给自己发送任意信号:
cpp
#include <signal.h>
int raise(int sig);
参数:
- sig :要发送的信号编号/信号名称。
返回值 :
- 成功返回 0 ;失败返回 非0。
cpp
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void task(int sig)
{
cout << "Get a signal , signal num:"<< sig << endl;
}
int main()
{
signal(2,task);
int count=10;
while(count)
{
if(count%3==0)
{
raise(2);//给自己发送2号信号
}
else
{
cout<<"pid:"<<getpid()<<" ,count:"<<count<<endl;
}
count--;
sleep(1);
}
cout<<"end......"<<endl;
return 0;
}
上述代码中,当count为3的倍数时,调用一次raise函数。
给自己发送固定信号:
cpp
#include <stdlib.h>
void abort(void);
- 该函数无参也无返回值,给当前进程发送固定信号---6号信号。
- 6号信号会终止程序。
cpp
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void task(int sig)
{
cout << "Get a signal , signal num:"<< sig << endl;
}
int main()
{
signal(6,task);
int count=10;
while(count)
{
if(count==5)
{
abort();
}
else
{
cout<<"pid:"<<getpid()<<" ,count:"<<count<<endl;
}
count--;
sleep(1);
}
cout<<"end......"<<endl;
return 0;
}
上述代码运行5秒后调用abort函数。
可以发现6号信号即使被捕捉自定义了方法,执行完自定义方法后,还是会终止程序。
通过软件条件产生信号:
在进程间通信一文中有说到管道的四种情况之一,读端关闭后,写端再继续写已经没有意义了,会直接被操作系统终止,这是因为读端关闭后,操作系统给写端进程发送了13号信号(SIGPIPE),直接杀掉了进程。这里就不在赘述,相关验证代码在进程间通信一文中有,有兴趣请自行查看。
这里主要介绍alarm函数和14号信号(SIGALRM)。
设定闹钟:
cpp
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
参数:
- second:second秒,即second秒之后给进程发送 14 号信号。
返回值:
- 返回最近一次设定的闹钟的剩余秒数 或者 0。
注意:
- 若second为0,表示取消以前设定的闹钟,返回值仍然是以前设定的闹钟的剩余秒数,或者0。
- 一般情况下,定义多个闹钟,只会有响一次(最近一次设定的那一个),除非通过自定义捕捉嵌套定义,才能响多次。
cpp
#include <iostream>
#include <unistd.h>
int main()
{
int a=0,b=0,c=0;
a=alarm(3);
sleep(1);
b=alarm(10);
sleep(1);
c=alarm(5);
int count=0;
while(1)
{
cout<<"count:"<<count<<" a="<<a<<" b="<<b<<" c="<<c<<endl;
count++;
sleep(1);
}
return 0;
}
上述代码中定义了三个闹钟,分别定时3秒、10秒、5秒,每定义一个闹钟程序暂停1秒钟,a、b、c分别记录三个闹钟的返回值,有一个count负责计时,每秒打印一次数据。
观察结果可以发现,后边设定的闹钟会把前边设定的闹钟覆盖掉 ,只有最后一次设定的5秒的闹钟是有效的,并且alarm函数的返回值是前一个设定的闹钟的剩余秒数,若前边没有设定闹钟,alarm函数返回值为0。
cpp
int main()
{
alarm(4);
sleep(1);
alarm(5);
sleep(1);
alarm(2);
sleep(1);
alarm(0);
int count=10;
while(count)
{
cout<<"count:"<<count<<endl;
count--;
sleep(1);
}
cout<<"end......"<<endl;
return 0;
}
上述代码中,设定了四个闹钟,分别定时4秒、5秒、2秒、0秒,每设定一个程序员暂停1秒钟,有一个count负责计时。
观察可以发现,最后设定的0秒的闹钟把以前设定的闹钟都取消了。
通过硬件异常产生信号:
硬件异常通常会被硬件以某种方式检测到并通知内核,然后内核向当前进程发送适当的信号。
例如当前进程执行了除以0的运算 ,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址 ,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
cpp
void Divide(int sig)
{
cout << "Get a signal " << sig << " ,this signal is divided by 0 anomalies" << endl;
}
void Wildptr(int sig)
{
cout << "Get a signal " << sig << " ,this signal is a field pointer anomaly" << endl;
}
int main()
{
signal(SIGFPE, Divide);
signal(SIGSEGV, Wildptr);
int a = 0;
int b = 10 / a;
int *ptr = NULL;
*ptr = 10;
sleep(1);
cout << "end......" << endl;
return 0;
}
上述代码中,对SIGFPE信号和SIGSEGV信号进行了自定义捕捉,想要达到的目的是执行自定义的两个函数,然后退出程序,实际结果是怎样的呢?
实际结果却是一直执行除以零错误的捕捉,这是因为我们自定义捕捉了SIGFPE信号,但是并没有让程序终止,那么SIGFEP信号就会一直产生,进而就会一直捕捉SIGFEP信号。
总结:
信号的默认处理动作:
信号产生后,默认处理动作有Term、Ign、Core、Step、Cont:
这五种默认处理动作的作用:
- Trem:终止进程。
- Ign:忽略这个信号。
- Core:终止进程并进行核心转储(Core Dump)。
- Step:暂停进程。
- Cont:让暂停的进程继续执行。
大部分信号的默认处理动作是Trem和Core:
Trem和Core的区别:
- term:普通的终止进程 ,不会做别的事情。
- core:除了终止进程,还会进行核心转储(core dump),生成core文件,core文件中存储的是程序出错的相关信息,有助于找出问题所在。
- 核心转储功能默认是关闭的(可使用 ulimit + -a 查看相关信息),需要手动开启 ulimit + -c + num , 其中num代表core文件的大小,当num为0时 ,即代表关闭核心转储功能。
核心转储(Core Dump):
当一个进程要异常终止 时,可以选择把进程的用户空间内存数据全部保存到磁盘上 ,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做事后调试 (Post-mortem Debug )。一个进程允许产生多大的core文件取决于进程的Resource Limit (这个信息保存在进程PCB中)。默认是不允许产生core文件的 ,因为core文件中可能包含用户密码等敏感信息,不安全 。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件,ulimit + -c + num ,其中num代表生成core文件的大小,当num为0时 ,即代表关闭核心转储功能。
- 是否进场核心转储,会在进程的退出信息中标识。
正常退出的进程的退出信息的低16位中,低 8 位都为0 (这也是为什么没有0号信号的原因),表示正常退出,高 8 位标记退出状态。
被信号杀掉的进程的退出信息的低16位中,低 7 位标记退出码 ,第 8 位标记是否进行核心转储 ,高 8 位未用。
信号的保存
相关概念:
- 实际执行信号的处理动作叫做 信号递达 。
- 信号从产生到递达之间的状态叫做 信号未决 ,即保存信号。
- 进程可以选择阻塞(屏蔽)某一个信号。
- 被阻塞的信号产生后将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达动作。
注意:
- 这里的阻塞和前边说的忽略是不同的,被阻塞的信号不会被递达 ,而忽略是信号递达后可选择的处理动作 。类比到日常收发消息,阻塞----收不到消息,忽略----已读不回。
内核结构:
信号未决,需要被标识,这个标识都是通过位图实现的(pending)。这个位图是通过一个整型数据实现的,每一个比特位的位置表示信号编号 ,比特位的内容表示是否收到该位置对应的信号。
与信号未决类似,信号阻塞也是通过一个位图标识的(block)。这个位图是通过一个整型数据实现的,每一个比特位的位置表示信号编号 ,比特位的内容表示是否阻塞该位置对应的信号。
内核结构:
- block即阻塞位图。
- pending即信号未决位图。
- handler是一个函数指针数组,其中存储的是对应信号的处理动作。
信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
- 上图中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
- SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
- SIGQUIT信号未产生过,一旦产生SIGQUIT信号,它将被阻塞,它的处理动作是用户自定义函数sighandler。
当一个信号被阻塞时,一旦产生这个信号,它将被保存在pending位图 中,等待进程解除对该信号的阻塞,如果在进程解除对某信号的阻塞之前这种信号产生过多次 ,由于pending位图 是通过一个整型实现的,因此每个比特位只能标记是否产生了该信号 ,不能标记产生了几次该信号 。因此,常规信号在递达之前产生多次只计一次,而实时信号 又有所不同**,** 在递达之前产生多次可以依次放在一个队列里。
sigset_t:
每个信号只有一个比特位的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储 ,sigset_t称为信号集 ,它是操作系统提供的一个数据类型 。这个类型可以表示每个信号的"有效"或"无效"状态,在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞 ,而在未决信号集中"有效"和"无效"的含义是该信号是否处于未决状态 。阻塞信号集 也叫做当前进程的信号屏蔽字。
信号集操作函数:
sigset_t类型是操作系统提供的数据类型,对于每种信号用一个比特位表示"有效"或"无效"状态,至于这个类型内部是如何存储数据的,则依赖于系统实现,用户是不必关心的,用户只能调用以下函数来操作sigset_ t变量,而不能对它的内部数据做任何解释,比如用printf函数直接打印sigset_t变量是没有意义的。
cpp
#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指向的信号集,使其中所有信号的对应比特位的数据清零,表示该信号集不包含任何有效信号。成功返回0,出错返回-1。
- sigfillset:用于初始化set指向的信号集,使其中所有信号的对应比特位置1,表示该信号集的有效信号包括系统支持的所有信号。成功返回0,出错返回-1。
- sigaddset:向set指向的信号集中添加signo号有效信号。成功返回0,出错返回-1。
- sigdelset:向set指向的信号集中删除signo号有效信号。成功返回0,出错返回-1。
- sigismember:用于判断set指向的信号集的有效信号中是否包含signo号信号。若包含返回1,不包含返回0,出错返回-1。
注意:
- 在使用sigset_t类型的变量之前 ,一定要 调用sigemptyset或者sigfillset做初始化操作,保证信号集处于确定的状态。
- sigset_t只是系统提供的数据类型,与int、char等类似,单纯的对sigset_t类型的数据进行操作 ,并不会影响内核中的数据,想要修改内核数据,还要借助系统调用函数sigprocmask。
修改阻塞信号集:
cpp
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
参数:
how:有三个参数可选
|-------------|-----------------------------------------------------------------------------------------|
| SIG_BLOCK | 把在set指向的信号集中存在,而在当前阻塞信号集中没有的有效信号加入到阻塞信号集中,形成新的阻塞信号集。即新的阻塞信号集是当前的阻塞信号集和set指向的信号集的并集。 |
| SIG_UNBLOCK | 把在set指向的信号集中存在的有效信号,从阻塞信号集中移除,即解除对应信号的阻塞,允许移除未被阻塞的信号。 |
| SIG_SETMASK | 把阻塞信号集设置为set指向的信号集。 |set:输入性参数,如果为非空,则根据how参数对进程的信号屏蔽字(阻塞信号集)进行 操作。
oset:输出型参数,如果为非空,则读取进程的当前信号屏蔽字,通过oset传出。
返回值:
- 成功返回 0 ;失败返回 -1 ,并设置错误信息。
那么,能不能把所有信号都阻塞掉,创建一个"无敌"进程呢?上代码测试:
cpp
void PrintSig(sigset_t &pending)//打印信号集
{
cout << "pending bitmap: ";
for (int signo = 31; signo > 0; signo--)
{
if (sigismember(&pending, signo))//判断信号是否在信号集中
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
int main()
{
sigset_t Set,OSet;
int a=sigemptyset(&Set);//初始化Set信号集全为0
assert(a==0);
a=sigemptyset(&OSet);//初始化OSet信号集全为0
assert(a==0);
for(int i=1;i<32;i++)
{
a=sigaddset(&Set,i);//把i号信号添加进Set中
assert(a==0);
}
a=sigprocmask(SIG_SETMASK,&Set,&OSet);//把Set设置为进程的屏蔽字
assert(a==0);
cout<<"pid:"<<getpid()<<endl;
while(1)
{
sigset_t pending;
a=sigemptyset(&pending);//初始化pending信号集全为0
assert(a==0);
a=sigpending(&pending);//获取未决信号集
assert(a==0);
PrintSig(pending);//输出未决信号集
sleep(1);
}
return 0;
}
上述代码,把31个信号都加入到了阻塞信号集中,运行程序观察:
可以发现当向进程发送9号信号时,进程依然被终止了。
接下来从10号信号开始继续:
可以发现当向进程发送19号信号时,还是执行了19号信号的默认处理动作。
接下来从20号信号开始:
可以发现,向进程发送20号信号,会把18号信号的屏蔽解除。
再倒着来一遍看看:
可以发现,向进程发送18号信号会把20、21、22号信号的屏蔽解除。
总结:
- 9、19号信号无法被屏蔽,18、20号信号会把特定信号的屏蔽解除
获取未决信号集:
cpp
#include <signal.h>
int sigpending(sigset_t *set);
参数:
- set:输出型参数,若为非空,则读取当前进程的未决信号集,通过set传出。
返回值:
- 成功返回 0 ;失败返回 -1 ,并设置错误信息。
测试:
有了上述基础,我们就可以对阻塞信号集进行修改了,并且还可以获取当前进程的未决信号集。
下面写一段代码进行测试:
cpp
#include <iostream>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void task(int sig)
{
cout << "Get a signal , signal num:" << sig << endl;
}
void PrintSig(sigset_t &pending)//打印信号集
{
cout << "bitmap: ";
for (int signo = 31; signo > 0; signo--)
{
if (sigismember(&pending, signo))//判断信号是否在信号集中
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
void task(int sig)
{
cout << "Get a signal , signal num:" << sig << endl;
}
void PrintSig(sigset_t &pending)//打印信号集
{
cout << "bitmap: ";
for (int signo = 31; signo > 0; signo--)
{
if (sigismember(&pending, signo))//判断信号是否在信号集中
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
int main()
{
signal(2,task);//自定义2号信号处理动作
sigset_t Set,OSet;
int a=sigemptyset(&Set);//初始化Set信号集全为0
assert(a==0);
a=sigemptyset(&OSet);//初始化OSet信号集全为0
assert(a==0);
cout<<"Set:"<<endl;
PrintSig(Set);//由于sigset_t是系统提供的类型,无法直接打印输出,只能通过自定义函数打印输出
cout<<"OSet:"<<endl;
PrintSig(OSet);
a=sigaddset(&Set,2);//把二号信号添加进Set中
assert(a==0);
cout<<"Add signal number 2 to the Set:"<<endl;
PrintSig(Set);
a=sigprocmask(SIG_SETMASK,&Set,&OSet);//把Set设置为进程的屏蔽字
assert(a==0);
cout<<"block 2 signal success"<<endl;
cout<<"pid:"<<getpid()<<endl;
int count=0;
while(1)
{
sigset_t pending;
a=sigemptyset(&pending);//初始化pending信号集全为0
assert(a==0);
a=sigpending(&pending);//获取未决信号集
assert(a==0);
PrintSig(pending);//输出未决信号集
count++;
if(count==10)//解除对2号信号的屏蔽
{
cout<<"解除2号信号的屏蔽"<<endl;
a=sigprocmask(SIG_UNBLOCK,&Set,&OSet);
assert(a==0);
}
sleep(1);
}
return 0;
}
上述代码中, 先是自定义2号信号的处理动作,然后将2号信号设置进进程的信号屏蔽字,10秒后再解除对2号信号的屏蔽,期间通过键盘 Ctrl+c 不断给进程发送2号信号,观察现象:
可以发现,未决信号集中,只有一个比特位标记是否收到2号信号,即使发送多次2号信号,在未决信号集中也只有一个1,表示有一个2号信号没有处理。并且当解除对2号信号的屏蔽后 ,会立即执行2号信号的处理动作,后续再发送2号信号,将直接处理。
这里还有个小问题,当使用sigprocmask解除对某个信号的屏蔽时,是在sigprocmask返回之前,pending位图就清零了 ,还是sigprocmask返回之后,pending位图才清零?再来看下边一段代码:
cpp
#include <iostream>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <cassert>
using namespace std;
void PrintSig(sigset_t &pending)//打印信号集
{
cout << "bitmap: ";
for (int signo = 31; signo > 0; signo--)
{
if (sigismember(&pending, signo))//判断信号是否在信号集中
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
void task(int sig)
{
sigset_t pending;
int n =sigemptyset(&pending);
assert(n==0);
n = sigpending(&pending);
assert(n == 0);
// 打印pending位图中的信息
cout << "递达中...: "<<endl;
PrintSig(pending);
cout << sig << " 号信号被递达处理..." << endl;
}
int main()
{
signal(2,task);//自定义2号信号处理动作
sigset_t Set,OSet;
int a=sigemptyset(&Set);//初始化Set信号集全为0
assert(a==0);
a=sigemptyset(&OSet);//初始化OSet信号集全为0
assert(a==0);
a=sigaddset(&Set,2);//把2号信号添加进Set中
assert(a==0);
cout<<"Add signal number 2 to the Set:"<<endl;
PrintSig(Set);
a=sigprocmask(SIG_SETMASK,&Set,&OSet);//把Set设置为进程的屏蔽字
assert(a==0);
cout<<"block 2 signal success"<<endl;
cout<<"pid:"<<getpid()<<endl;
int count=0;
while(1)
{
sigset_t pending;
a=sigemptyset(&pending);//初始化pending信号集全为0
assert(a==0);
a=sigpending(&pending);//获取未决信号集
assert(a==0);
PrintSig(pending);//输出未决信号集
count++;
if(count==10)//解除对2号信号的屏蔽
{
cout<<"解除2号信号的屏蔽"<<endl;
a=sigprocmask(SIG_UNBLOCK,&Set,&OSet);
cout<<"解除屏蔽成功"<<endl;
assert(a==0);
}
sleep(1);
}
return 0;
}
可以发现,在递达信号的时候,pending位图就已经被清零了,即在sigprocmask返回之前,pending位图就已经清零了。
信号的捕捉
信号的检测与处理:
- 信号技术是通过软件的方式,模拟硬件中断。
- 当程序中存在系统调用或者有中断、异常时,进程就会进入内核态去处理,当进程从内核态切换到用户态 时,信号会被检测并处理 。因为进程调度、时间片的存在,使得程序中即使没有系统调用、中断、异常,进程也会存在从内核态到用户态的切换。
信号的捕捉过程:
在信号处理(捕捉)过程中,会出现4次用户态和内核态之间的转换。
以2号信号为例:
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。 当前正在执行main函数,这时发生中断/异常/系统调用切换到内核态。 在处理完毕后要返回用户态的main函数之前检查到有2号信号递达。由于2号信号的自定义处理动作的代码在用户空间,内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
进程地址空间:
- 操作系统也是一个软件,它是机器开机后第一个加载进机器的。
- 每个进程在创建时,会把操作系统相关数据通过内核级页表加载到自己进程地址空间的[3,4]GB的空间中,当进程执行系统调用或访问系统数据时,都是在进程自己的地址空间中完成的。
- 进程无论如何切换,总能找到操作系统、访问系统数据,本质就是去进程自己的地址空间的[3,4]GB空间中访问数据。(此时操作系统会进行身份识别,只有在内核态才能访问系统数据。)
- 操作系统是一个死循环的程序,不断地接受外部的硬件中断,不断被刺激响应。
信号捕捉函数:
除了前边说的signal函数可以捕捉信号外,sigaction函数也可以捕捉信号。
cpp
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
参数:
- signo:指定信号的编号。
- act:一个sigaction类型的结构体指针,若act非空,则根据act修改该信号的处理动作。
- oact:一个sigaction类型的结构体指针,若oact非空,则通过oact传出该信号原来的处理动作。
返回值:
- 成功返回0;失败返回 -1,并设置错误码。
- sigaction结构体:
cpp
struct sigaction
{
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
- sa_sigaction和sa_restorer是与实时信号相关的,这里不做处理。
- sa_handler:将sa_handler赋值为SIG_IGN表示忽略信号;赋值为SIG_DFL表示执行系统默认动作;赋值为一个函数指针表示用自定义函数捕捉信号,该函数返回值为void,可以带一个int型参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,只不过不是被main函数调用,而是被系统所调用。
- sa_flags:默认设为0.
- sa_mask:一个信号集。在调用信号处理函数时,除了当前信号被自动屏蔽之外,若还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号。
cpp
void handler(int sig)
{
cout<<"get a signal , signal number:"<<sig<<endl;
}
int main()
{
struct sigaction act,oact;
act.sa_flags=0;//默认设为0
act.sa_handler=handler;//自定义处理动作
int n=sigaction(2,&act,&oact);
assert(n==0);
while(1)
{
sleep(1);
}
return 0;
}
上述代码中,只自定义捕捉了2号信号。没有额外屏蔽其他信号。
可以发现成功捕捉了2号信号。
可重入函数
若某个函数被多个执行流重复进入,导致出现了问题,这样的函数叫做不可重入函数。反之则叫做可重入函数。
平常使用的大部分库函数都是不可重入的。最常见的就是链表的插入函数。
若某一个函数满足以下条件之一,则是不可重入的:
- 调用了malloc、free。因为malloc是调用全局链表实现的。
- 调用了标准I/O库函数的。标准I/O库中的很多实现都是以不可重入的方式使用全局数据结构。
volatile
- volatile作用主要是保持内存的可见性。在读取volatile修饰的数据时,必须去内存中读取。
有如下一段代码:
cpp
#include<stdio.h>
#include<signal.h>
int g_flag = 0;
void changeflag(int signo)
{
g_flag++;
printf("g_flag:%d\n", g_flag);
}
int main()
{
signal(2, changeflag);
while(!g_flag); // 故意写成这个样子, 编译器默认会对代码进行自动优化
printf("process quit normal\n");
return 0;
}
使用gcc编译时,在最后边加上 -O3 强制以最高优化等级优化代码,再运行:
可以发现,本应该发送一次2号信号就结束的程序,却一直没有结束。这是因为g_flag是一个全局变量,强制优化编译时,编译器会把g_flag在寄存器上保存一份 ,在使用时,直接使用寄存器上的数据,没有去内存中读取数据,导致虽然内存中的数据改变了,但是程序不读取,就会一直陷入死循环。
给g_flag加上volatile修饰,再测试:
cpp
#include<stdio.h>
#include<signal.h>
volatile int g_flag = 0;
//int g_flag = 0;
void changeflag(int signo)
{
//(void)signo;
g_flag++;
printf("g_flag:%d\n", g_flag);
}
int main()
{
signal(2, changeflag);
while(!g_flag); // 故意写成这个样子, 编译器默认会对代码进行自动优化
printf("process quit normal\n");
return 0;
}
可以发现,即使使用最高优化等级优化代码编译,程序还是会正常退出。
SIGCHLD信号
通常使用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一下,太过复杂。
其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通过信号通知父进程,父进程在信号处理函数中调用wait清理子进程即可。
cpp
void CleanupChild(int signo)
{
if (signo == SIGCHLD)
{
pid_t rid = waitpid(-1, nullptr, 0);
if (rid > 0)
{
cout << "wait child success: " << rid << endl;
}
}
cout << "wait sub process done" << endl;
}
int main()
{
signal(SIGCHLD, CleanupChild);//自定义SIGCHLD信号处理动作
pid_t id = fork();
if (id == 0)
{
// child
int cnt = 5;
while (cnt--)
{
cout << "I am child process: " << getpid() << endl;
sleep(1);
}
cout << "child process died" << endl;
exit(0);
}
// father
while (true)
sleep(1);
}
可以发现,自定义捕捉函数确实可以等待回收子进程。