对于一个信号从接收到处理,我们一般分为三个步骤,第一步信号的产生,第二步信号的保存,第三步信号的处理。当然有时可能没有第二步,举个生活例子,比如当我们快递信息来的时候,这个信息就是信号,对于这个信号我们可能会立即处理,当然也可能不去取,等到有时间了我们再去取,在从信息来再到等到我们去取的之前,我们就是把这个信号保存起来了,即我们有个快递要去取,在取来之后我们又会对快递做处理,可能是立即打开,也可能是送人,也可能是放在一边置之不理
两个问题:1.信号是怎么产生的呢?2.对于操作系统而言,又是怎么收到的?
信号的产生分为四种:
2号信号SIGINT默认是终止进程,3号信号SIGQUIT是终止进程并产生core dump文件,但是操作系统一般默认不让它产生对应的core dump文件,因为可能造成信息泄露,但为了实验,我们可以通过相关命令可以允许它产生
默认是不产生的:Ctrl +
通过命令ulimit -c 1024允许它产生,1024是文件大小最大可以是1024K,这里自行设置
产生core文件,28745是该进程对应的进程号
取消产生core文件只需重新登陆即可
kill函数
参数:
pid:进程pid
sig:要修改几号kill指令
返回值:
成功返回0,失败返回-1并设置错误码
#include
#include
#include
using namespace std;
void handler(int sig)
{
cout << "I am signal 2" << endl;
}
int main()
{
signal(2, handler);
while(1)
{
cout << "I am a process" << endl;
sleep(1);
}
return 0;
}
这里选择替换当前进程的2号信号,由原来直接退出当前进程替换为执行handler函数
可以看到此时再次按下Ctrl + c,不是将该进程直接退出,而是执行handler函数。
对于自定义的handler函数,参数是要替换的信号编号,只不过这个参数不需要我们显示的去传,因为在底层对它进行了封装,对于signal函数,我们只需要将信号(或编号)和要替换的函数地址传过去即可
进程分为前台和后台进程,我们在命令行输入的信号,默认是发给前台进程的,前台进程只有一个,后台进程可以有多个,要想让一个进程以后台进程形式运行,只需在该可执行程序后面加上符号&即可
既然信号要执行操作可以换成我们自定义的函数,那么我们要是把1-31信号都换成我们自定义的,是不是该进程就不可以被退掉了?
不是,操作系统对9号信号和19号信号做了特殊处理,这两个信号是不可以被设置成为自定义的信号。
作用:给当前进程发送信号sig
参数:要发送的信号的编号
返回值:成功为0,失败为-1并设置错误码
void handler(int sig)
{
cout << "I am signal 2" << endl;
}
int main()
{
signal(2, handler);
int cnt = 0;
while(++cnt)
{
cout << "I am a process" << endl;
sleep(1);
if(cnt == 5)
{
raise(3);
}
}
return 0;
}
#include
#include
#include
using namespace std;
int main()
{
int cnt = 0;
while(++cnt)
{
cout << "I am a process" << endl;
sleep(1);
if(cnt == 5)
{
abort();
}
}
return 0;
}
参数:seconds秒数
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
信号产生之后,可能该信号并没有被处理,而是被保存起来了。
实际执行信号的处理动作被称为递达
注意区分两个不同的概念:信号阻塞(屏蔽)是处于未被递达的状态,信号忽略是处于已被递达但不对该信号做处理的状态。
是否接收到某信号,通过pending数组来表示,下标表示信号对应的编号,数据若为0表示没有收到该信号,若为1则表示收到了该信号。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志
是否阻塞信号通过数组block来表示,下标也是表示信号对应的编号,若为0则表示不阻塞该信号,为1表示阻塞该信号
对于信号递达要执行的相应操作,通过handler数组来存储,handler数组里面保存的是处理方法。
解释上图:
1号信号SIGHUP,pending为0表示没有收到该信号,block为0,表示不阻塞该信号,对应handler的处理方法是SIG_DFL(默认方法),因此当有信号递达时,就执行默认方法
2号信号SIGINT,pending为1表示收到了该信号,且此时处于未决状态,即还没有递达,(递达是指执行信号相应的方法)。block为表示此时该信号处于阻塞状态,并没有被递达。
3号信号SIGQUIT,pending为0表示此时没有收到该信号,block为1表示当收到该信号时,会阻塞该信号,当信号递达时执行自定义方法。
至于忽略是指当信号递达了,也不会做任何操作。
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型的每一个比特位可以表示对应信号的的“有效”或“无效”状态.
在阻塞信号中,有效和无效的含义表示的是该信号是否被阻塞,而在未决信号集里面,有效和无效的含义表示的是该信号是否是处未决状态,也就是是否被递达。
阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
为什么会有sigset_t?
因为操作系统不相信任何人,这是提供给我们的一个类型,通过它定义出来的变量来与内核中的信号集进行交互去改变内核里面的信号集,至于是怎么做到的,我们可以不用问,因为操作系统已经给我们做好了,那么我们怎么通过它定义出来的信号集变量,来修改某一个信号呢?**它定义出来的变量与普通变量不一样,因为它不向其它变量一样可以直接修改,要想去修改这个变量我们需要通过函数来改变。**为什么要这样做呢?我们确实是可以使用内置类型来代替这个位图,但是对于内置类型而言int类型只有固定的32位,如果我们的位图想要进行拓展存储的多余32个信号,就不可以了,因此操作系统给我们提供了一个封装类型。sigset_t来使用下面我们来介绍几个函数接口,用于修改自定义信号集中的某个信号。
参数:
set:我们自己定义的sigset_t变量
signum:要去修改的信号编号
返回值:
成功返回,失败返回0
sigemptyset():把每个比特位全部置为0
sigfillset():把每个比特位全部置为1
sigaddset():将某一个信号置为1
sigdelset():将某一个信号置为0
sigismember():判断某一个信号是否为1
问题:我们定义一个sigset_t变量之后,通过signal系列函数将该变量值改变之后,改变后的值就被设置进内核里面了吗?并没有,首先我们要知道的是定义的这个sigset_t类型变量,它是在哪里开辟的空间呢?是在用户区的栈上,而不是在内核里面,要想通过它去改变内核里面的阻塞信号集数据我们还要通过函数来完成sigprocmask
作用:sigprocmask函数目的是去读取或更改进程的信号屏蔽字(阻塞信号集)
参数:
how:如何去修改oldset,假设原来的信号集为mask,这个参数就是用于是让mask = mask | set,还是mask = maske&~set,还是mask = set
set:自定义的信号集
oldset:用于保存原来的信号集,我们只需传这个参数,底层会给我们自行将修改前的信号集保存到这个变量里面
返回值:
成功返回0,失败返回-1
其中how有三个选项分别是
SIG_BLOCK:这个参数的意思是让set或上oldset
SIG_UNBLOCK:这个参数意思是取消set中已有的信号
SIG_SETMASK:这个意思是让新的信号集等于set
假设原来的信号集为mask,how对应的不同参数,更新后的mask等于让原信号集mask和传的参数set做对应的运算,如下表:
我们自定义出来的sigset_t变量,目的就是为了去让它与内核信号集做运算从而改变内核里的信号集,我们一般在使用自定义信号集前,要把他清空,然后再去改变自定义信号集的值,之后再让它添加到内核中与内核信号集做运算,从而做到能够去修改内核信号集。
作用:读取当前进程的未决信号集
参数:用户区自定义的信号集变量,用于将内核中的未决信号集数据带出来,也可以理解为内核信号集把它自己的数据,拷贝给了当前变量,因此这两个变量区别是一个是在内核区一个是在用户区,而数据是完全相同的
返回值:
成功返回0,失败返回-1
#include
#include
#include
using namespace std;
void handler(int sig)
{
cout << "I am a process" << endl;
}
int main()
{
//在用户区,给定set
sigset_t set, oldset;
sigemptyset(&set);
sigaddset(&set, 2);
sigprocmask(SIG_BLOCK, &set, &oldset);
cout << "procmask" << endl;
for(int i = 31; i > 0; i--)
{
if(sigismember(&set, i))
{
cout << "1";
}
else{
cout << "0";
}
}
cout << endl;
signal(2, handler);
sleep(5);
cout << "procmask" << endl;
for(int i = 31; i > 0; i--)
{
if(sigismember(&set, i))
{
cout << "1";
}
else{
cout << "0";
}
}
cout << endl;
sigpending(&set);
cout << "pending" << endl;
for(int i = 31; i > 0; i--)
{
if(sigismember(&set, i))
{
cout << "1";
}
else{
cout << "0";
}
}
cout << endl;
return 0;
}
信号是在什么时候被捕捉的?
当程序在进行系统调用或者发生异常时,操作系统会自动的去检测有没有收到信号,那么当这两者都没有时,操作系统就不会检测了吗?不是的,操作系统中有一个死循环,它会每隔一段时间就会进行检测有没有接收到信号,这个循环也叫做时钟中断,它会不断的进行检测。
信号是在内核态还是在用户态进行处理的?内核态,具体过程见下图:
1表示我们正在执行程序,当遇到异常或者进行系统调用或者时钟中断时,就会在2的位置由用户态陷入到内核态,在2号位置进行信号检测,在3号位置进行信号的处理,如果信号的处理是自定义的处理方式,那么就由3号位置再进入到用户态4对信号进行处理,在对信号处理完成之后,再回到内核态5,最后再由内核态5回到用户态1.
信号的处理是在3号位置进行的,只不过处理方式可能是自定义的需要再用户区进行函数调用。
signal函数用于信号捕捉,并可将处理动作设为自定义的。
参数:
signum:信号编号
handler:类型是一个指向参数为int的函数指针
参数有三种:SIG_DFL,SIG_IGN,和自定义的函数指针
库中对这两个SIG_DFL,SIG_IGN,SIG_ERR宏进行了封装,对应如下
signal函数当第二个参数设为SIG_DFL时,意为设置该信号的处理动作设为默认处理,
当设为SIG_IGN时,意为忽略该信号,如果不处理2号信号默认是SIG_DEL
返回值:
成功返回信号对应的函数指针,失败时,返回SIG_ERR
忽略2号信号:
#include
#include
#include
#include
#include
using namespace std;
int main()
{
signal(2, SIG_IGN);
while(true)
{
cout << "this is process pid:" << getpid() << endl;
sleep(1);
}
return 0;
}
该函数也是用于捕捉信号的,但是它的功能更强大,可以通过它的struct sigaction结构体参数同时去将多个数据设置进内核
参数:
signum:信号编号
act:自定义要怎么去处理的结构体指针
oldact:用于保存原来的处理动作的结构体指针
结构体里面存的是什么?
sa:signal
void (*sa_handler)(int):要怎么去处理的函数指针,如果我们想要去自定义处理,那么就让这个指针改为我们自定义的函数指针
sigset_t sa_mask:block表(阻塞信号集)
返回值:
成功返回0,失败返回-1,并设置错误码
sigaction是在用户区给我们提供的一个结构体,我们要想修改信号的处理动作就要先去修改这个结构体成员的变量,然后再将这个结构体指针通过sigaction函数传入系统内核
act结构体的成员变量sa_handler存储的是处理动作的结构体指针
问题1:如果再进行信号处理的过程中我们再给这个进程发送这个信号,它会直接进行处理吗?
代码示例:
#include
#include
#include
#include
#include
#include
using namespace std;
void PrentPending()
{
sigset_t set;
sigpending(&set);
for (int i = 1; i < 32; i++)
{
if (sigismember(&set, i))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
void handler(int signo)
{
while (true)
{
PrentPending();
sleep(1);
}
sleep(5);
}
int main()
{
struct sigaction act, oldact;
// 把act和oldset结构体变量数据置空
memset(&act, 0, sizeof(act));
memset(&oldact, 0, sizeof(oldact));
act.sa_handler = handler; // 将如何处理的成员指针改为自定义的函数指针
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, 1);
sigaddset(&act.sa_mask, 3);
sigaddset(&act.sa_mask, 4);
sigaction(2, &act, &oldact); // 添加进内核
while (true)
{
cout << "I am a process: " << getpid() << endl;
sleep(1);
}
return 0;
}
回答问题1:在处理这个信号的过程中,再次发送这个信号不会再对该信号进行处理,因为操作系统会自动的将这个信号的block位图置为1,即阻塞该信号
阻塞该信号是在处理这个信号前还是处理信号后?
通过上面的结果我们可知,在接受到这个信号后,在没有进行处理前就已经将这个信号的block置为1了,因此再次发送这个信号时,它对应的pending位置为1,即再次发送的这个信号,在该信号还没有处理完成时,它处于未决状态。那么如果发送别的信号给这个进程呢?他会不会去处理呢?若发送的别的信号没有被阻塞,则操作系统会在合适时进行处理,即使当前信号没有被处理完,也可能会处理其它信号。
那么我们不想让其他信号干扰我们怎么办?即在处理这个信号时,不去处理其他信号,也阻塞其它信号,那么怎么去阻塞呢?
通过结构体成员变量sigset_t sa_mask去阻塞其它信号,先解释一下这个变量,这个变量就是阻塞信号集,即block信号表,我们只需把我们想去阻塞的信号,添加到该表中即可,怎么添加呢?
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, 1);
sigaddset(&act.sa_mask, 3);
sigaddset(&act.sa_mask, 4);
是否似曾相识呢?
在阻塞部分我们也是通过这几个函数调用修改用户区阻塞信号集的,而这里的成员变量sa_mask也就是我们用户区的阻塞信号集,我们在用户区把要阻塞的信号添加到该信号集中,当有2号信号发送过来时,就会调用sigaction(2, &act, &oldact); 函数将这个结构体添加进内核。
它的参数act是一个s指向用户区的sigaction结构体(函数名和结构体名允许同名),该结构体存储的是我们在处理该信号时要怎么做的数据,其中成员sa_handler指向处理动作函数,成员sa_mask是一个阻塞信号集,当接收到sigaction第一个参数对应的信号时,sigaction函数就会把用户区的sigaction结构体数据添加到系统内核,接收到的这个信号会自动添加到阻塞信号集,我们要是想阻塞其它信号就直接将对应的信号添加到成员sa_mask信号集即可
什么是可重入函数?什么是不可重入函数?
例如我们正在执行某个函数时,接收到一个信号,而这个信号的处理动作也是去执行这个函数,如果在重复进入该函数的过程中导致错误,那么这个函数就是不可重入的,相反就是可重入的。
如上重入后导致node2节点丢失,那么这就是不可重入函数。
我们在对程序进行优化时,可能编译器认为某个变量不会变,因此会把它优化到CPU的寄存器当中,因此当用这个变量时系统就会直接从该寄存器中访问这个变量,而不是再到物理内存中去访问,那么当我们修改这个变量后,它等于是没被修改,因为并不会去访问物理内存,而是访问寄存器,要想阻止这种情况发生就要给这个变量添加volatile关键字,阻止他被优化
代码示例:
#include
#include
#include
using namespace std;
int flag = 1;
void handler(int signo)
{
flag = 0;
cout << "flag from 1 to 0" << endl;
}
int main()
{
signal(2, handler);
while(flag);
return 0;
}
mytest:signal.cc
g++ -o $@ $^ -O2 -std=c++11
.PHONY:clean
clean:
rm -r mytest
编译时我们添加上优化选项O2,结果如下
我们发现循环一直没停止,虽然flag由1变为0了,这是为什么呢?
因为我们改变的是内存中的flag值,而在程序中访问flag变量不是去内存中访问,而是直接从CPU中的寄存器中进行访问,而CPU寄存器中的flag变量仍然是1,并没有被改变。
通过man g++查看优化选项
我们一般在debug版本下不会被优化,而以release版本进行发布时,就会对它进行优化,那么我们怎么去防止这种情况发生,阻止它优化呢?
通过给这个变量添加volatile关键字,就可以阻止他被优化到寄存器中
volatile int flag = 1;
volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作
#include
#include
#include
using namespace std;
void handler(int signo)
{
cout << "I am father ppid: " << getppid() << "signal:" << signo << endl;
}
int main()
{
signal(SIGCHLD, handler);
int id = fork();
if(id == 0)
{
while(true)
{
cout << "I an child pid: " << getpid() << endl;
sleep(3);
exit(0);
}
}
if(id > 0)
{
while(true)
{
cout << "I am father ppid: " << getppid() << endl;
sleep(1);
}
}
return 0;
}
在父进程收到子进程退出的信号后,就会对子进程做出处理,如果我们不自定义这个信号的处理动作,它对该信号是DFL默认的,对应的action处理动作是IGN忽略的。
对于子进程的退出,我们一般会通过父进程来对子进程进行回收,父进程对于子进程的回收我们之前是采用的阻塞式等待或者是轮询式等待的,但无论哪种方式,都会影响父进程,因为在等待回收的过程中,父进程是什么也做不了的,导致效率低下,那么我们如何去提高效率呢?如何做到让父进程在做其他事情的同时,也能够同时去处理子进程退出的问题呢?父进程通过接收子进程的退出信号来解决。
#include
#include
#include
#include
using namespace std;
void handler(int signo)
{
pid_t rid;
while (rid = waitpid(-1, nullptr, WNOHANG) > 0)
{
cout << "I am father ppid: " << getppid() << " ,rid:" << rid << endl;
}
}
int main()
{
signal(SIGCHLD, handler);
for (int i = 0; i < 10; i++)
{
int id = fork();
if (id == 0)
{
while (true)
{
cout << "I am child pid: " << getpid() << endl;
sleep(3);
exit(0);
}
}
sleep(1);
}
while (true)
{
cout << "I am father ppid: " << getppid() << endl;
sleep(1);
}
return 0;
}
可以看到父进程在fork创建子进程的同时也在做其它子进程回收的工作。
对于自定义处理方法,signal和sigaction等函数,我们是定义了该函数,并且看似调用了该函数,但是如果没有收到对应的信号就不会调用该函数,那么为什么自定义处理函数在进行处理的时候,父进程仍然可以做其他事情呢?一个进程不是一个执行流吗?其实这里的handler函数看似是被main函数调用了,但他俩并不存在调用与被调的关系,它俩是主线程与其它线程的关系,这里涉及到了多线程,多执行流的知识