Linux知识点 – 进程信号(二)
文章目录
- Linux知识点 -- 进程信号(二)
- 一、信号保存
- 1.相关概念
- 2.信号保存的相关接口
- 3.对所有的信号都进行自定义捕捉
- 4.将2号信号block,并打印pending信号集
- 5.将所有信号都block
- 二、处理信号
- 1.信号处理的时机
- 2.信号处理的流程
- 3.sigaction
- 三、可重入函数
- 四、volatile关键字
- 五、SIGCHILD信号
一、信号保存
1.相关概念
-
信号递达(Delivery):实际执行信号的处理动作;
-
信号未决(Pedning):信号从产生到递达之间的状态;信号未决就是进程收到了一个信号,但是未处理,就是临时保存到了进程PCB中的对应的位图中;
-
进程可以选择阻塞(block)某个信号;
-
被阻塞的信号产生时将保持在未决状态,直到进程解决对此信号的阻塞,才执行递达的动作;
-
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在抵达之后可选的一种处理动作;
-
在进程PCB内部有3张表:
其中,pending就是信号未决的位图,进程在收到一个信号后,会将pending表中相应的位置位;
handler是函数指针数组 – 数组下标对应信号的编号,就是对应信号的处理方式;signal自定义捕捉就是将信号对应的方法填入handler表;
也可以设置信号的忽略和默认;IGN是忽略;DFL是默认;
block表是阻塞表,结构和pending一摸一样,代表的含义是对应的信号是否被阻塞; -
信号的处理过程:
进程在接受一个信号后,会将pending表中相应的位置位,然后先去block表中查看该进程是否被阻塞,如果被阻塞,就不做任何动作,如果没有阻塞,再去handler表中查询处理方法;
2.信号保存的相关接口
(1)语言会为我们提供.h.hpp和语言的自定义类型;
同时,操作系统也会给我们提供.h和自定义类型;
(2)OS向我们提供了接口,一定要提供相对应的类型;
语言提供了访问系统调用的接口,也一定会提供相对应的类型;
-
sigset_t类型:
未决和阻塞标志可以使用相同的数据类型(位图),sigset_t称为信号集,这个类型可以表示每个信号的有效或无效状态;
在阻塞信号集中有效和无效的含义是该信号是否被阻塞,阻塞信号集也叫做信号屏蔽字;
而在未决信号集中有效和无效的含义是该信号是否处于未决状态;
注:
sigset_t不允许用户自己进行位操作,OS为我们提供了对应的操作方法;
sigset_t使用者可以直接使用该类型,和用内置类型、自定义类型没有任何差别;
sigset_t一定需要对应的系统接口,来完成对应的功能,其中系统接口需要的参数,可能就包含了sigset_t定义的变量或者对象; -
OS提供的对sigset_t操作的接口:
分别是:
全部位清0;
全部位置1;
某个信号置位;
某个信号复位;
判断信号是否存在;
sigpending函数:获取当前调用进程的pending信号集;
set是输出型参数;
成功返回0,失败返回-1;
sigprocmask函数:检查并更改block信号集;
how参数:
set:根据how的不同的宏,有不同的功能;
oldset:输出型参数,返回老的信号屏蔽字,不需要可以传空指针;
3.对所有的信号都进行自定义捕捉
#include<iostream>
#include<unistd.h>
#include<signal.h>using namespace std;void catchSig(int signum)
{cout << "获得了一个信号:" << signum << endl;
}int main()
{for(int i = 1; i <= 31; i++){signal(i, catchSig);}while(true){sleep(1);}return 0;
}
运行结果:
可以发现,其他信号都被自定义捕捉了,只有9号信号杀死了该进程,因为9号信号是不能被捕捉的;
4.将2号信号block,并打印pending信号集
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<cassert>using namespace std;static void showPending(sigset_t &pending)
{for(int sig = 1; sig <= 31; sig++){if(sigismember(&pending, sig)){cout << "1";}else{cout << "0";}}cout << endl;
}int main()
{//1.定义信号集对象sigset_t bset, obset;sigset_t pending;//2.初始化sigemptyset(&bset);sigemptyset(&obset);sigemptyset(&pending);//3.添加要进行屏蔽的信号sigaddset(&bset, 2);//4.设置set到内核中对应的进程内部int n = sigprocmask(SIG_BLOCK, &bset, &obset);assert(n == 0);(void)n;cout << "block 2号信号成功 " << endl;//5.重复打印当前进程的pending信号集while(true){//获取当前进程的pending信号集sigpending(&pending);//显示当前进程的pending信号集showPending(pending);sleep(1);}return 0;
}
运行结果:
当发送了2号信号后,pending表中对应的位置1了,2号信号是被阻塞了,应该一直在pending表中,无法被递达;
在一定时间后恢复2号信号的block
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<cassert>using namespace std;static void showPending(sigset_t &pending)
{for(int sig = 1; sig <= 31; sig++){if(sigismember(&pending, sig)){cout << "1";}else{cout << "0";}}cout << endl;
}int main()
{//1.定义信号集对象sigset_t bset, obset;sigset_t pending;//2.初始化sigemptyset(&bset);sigemptyset(&obset);sigemptyset(&pending);//3.添加要进行屏蔽的信号sigaddset(&bset, 2);//4.设置set到内核中对应的进程内部int n = sigprocmask(SIG_BLOCK, &bset, &obset);assert(n == 0);(void)n;cout << "block 2号信号成功 " << endl;//5.重复打印当前进程的pending信号集int count = 0;while(true){//获取当前进程的pending信号集sigpending(&pending);//显示当前进程的pending信号集showPending(pending);sleep(1);count++;if(count == 20){int n = sigprocmask(SIG_SETMASK, &obset, nullptr);//将原来的信号集附上去assert(n == 0);(void)n;cout << "接触对2号信号的block " << endl;}}return 0;
}
运行结果:
结果是没有看到pending表从1变为0;
默认情况下,回复对于2号信号block的时候,确实会进行递达;
但是2号信号的默认处理动作是终止进程,将进程直接终止;
我们需要对2号信号进行捕捉:
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<cassert>using namespace std;void catchSig(int signum)
{cout << "获得了一个信号:" << signum << endl;
}static void showPending(sigset_t &pending)
{for(int sig = 1; sig <= 31; sig++){if(sigismember(&pending, sig)){cout << "1";}else{cout << "0";}}cout << endl;
}int main()
{signal(2, catchSig);//1.定义信号集对象sigset_t bset, obset;sigset_t pending;//2.初始化sigemptyset(&bset);sigemptyset(&obset);sigemptyset(&pending);//3.添加要进行屏蔽的信号sigaddset(&bset, 2);//4.设置set到内核中对应的进程内部int n = sigprocmask(SIG_BLOCK, &bset, &obset);assert(n == 0);(void)n;cout << "block 2号信号成功 " << endl;//5.重复打印当前进程的pending信号集int count = 0;while(true){//获取当前进程的pending信号集sigpending(&pending);//显示当前进程的pending信号集showPending(pending);sleep(1);count++;if(count == 20){int n = sigprocmask(SIG_SETMASK, &obset, nullptr);//将原来的信号集附上去assert(n == 0);(void)n;cout << "接触对2号信号的block " << endl;}}return 0;
}
注:
没有一个接口时用来设置pending位图的,这是因为所有信号的发送方式,都是修改pending位图的过程;
5.将所有信号都block
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cassert>using namespace std;static void showPending(sigset_t &pending)
{for (int sig = 1; sig <= 31; sig++){if (sigismember(&pending, sig)){cout << "1";}else{cout << "0";}}cout << endl;
}static void blockSig(int sig)
{sigset_t bset;sigemptyset(&bset);sigaddset(&bset, sig);int n = sigprocmask(SIG_BLOCK, &bset, nullptr);assert(n == 0);(void)n;
}int main()
{for(int sig = 1; sig <= 31; sig++){blockSig(sig);}sigset_t pending;while(true){sigpending(&pending);showPending(pending);sleep(1);}return 0;
}
当发到9号信号的时候,进程停止,9号信号是不能被屏蔽的;
跳过9号信号:
19号也是无法屏蔽
二、处理信号
1.信号处理的时机
- 信号产生之后,可能无法被立即处理,要在合适的时候处理;
- 因为信号的相关数据字段都是在进程PCB内部,这属于内核范畴,进程在运行时会从内核范畴 -> 内核状态 -> 用户态 -> 内核状态 -> 内核范畴;
在内核态中,从内核态返回用户态的时候,进行信号的检测和处理; - 当我们进行系统调用的时候,比如缺陷异常等,会进入内核态;int 80是一个系统中断语句,可以陷入内核;
- 用户态是一个受管控的状态,内核态是一个操作系统执行自己代码的一个状态,具备非常高的优先级;
- CPU的寄存器是由两套的,一套用户可见,另一套不可见,CPU自用;
- CR3表示当前CPU的执行权限,1表示内核,3表示用户;
- 在进程地址空间中,不光有用户地址空间,还有内核地址空间,内核地址空间使用的是内核级的页表,该页表是整个OS只有一份的,能够被所有的进程看到,因此所有进程看到的都是一个操作系统;
当我们进程需要调用系统接口时,就跳转到进程的内核地址空间,根据内核级页表,在内存中找到系统调用的相关方法; - 当我们有权限进入内核态时,进程使用的页表就是内核级页表了,就能够访问 OS的方法了,这也就意味着进程进入了内核态,可以处理信号了;
2.信号处理的流程
- 注意:
(1)在第二步时,进程在内核态处理完成系统任务后,会在重回用户态的时候进行信号的检测和处理;
(2)在第三步检测到信号,并处理时,如果信号的处理方式时系统默认方式,就直接在内核态处理了,然后返回用户态的执行流继续执行;如果信号的处理方式是用户自定义的,就需要返回用户态去执行相应的方法;这时进程的状态时用户态,能够执行自定义信号处理,但是系统不会去在内核态执行用户代码,因为涉及到系统安全问题;
(3)在第四步返回用户态执行信号处理后,进程会再次进入内核态,从内核态在返回用户态进程中断处继续执行;
(4)一共四次状态切换;
3.sigaction
- 参数:
signum:信号编号;
act:信号处理动作;struct sigaction是一个结构体,里面包含用户自定义的信号处理方式的函数指针等数据;
oldact:信号过去的处理方式;
#include<iostream>
#include<signal.h>
#include<unistd.h>using namespace std;void handler(int signum)
{cout << "获取了一个信号:" << signum << endl;
}int main()
{//内核数据类型,用户栈定义的struct sigaction act, oact;act.sa_flags = 0;sigemptyset(&act.sa_mask);act.sa_handler = handler;//设置进当前调用进程的PCB中sigaction(2, &act, &oact);while(true) sleep(1);return 0;
}
运行结果:
捕获2号信号并执行自定义处理方式;
- 当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的的信号屏蔽字,这样就保证了在处理某个信号时,如果该信号再次产生,那么它就会被阻塞到当前信号处理结束为止;如果在调用信号处理函数时,还希望屏蔽除当前信号的其他信号,就可以使用sigaction函数的sa_mask参数,来指定希望额外屏蔽的信号;
#include<iostream>
#include<signal.h>
#include<unistd.h>using namespace std;static void showPending(sigset_t &pending)
{for (int sig = 1; sig <= 31; sig++){if (sigismember(&pending, sig)){cout << "1";}else{cout << "0";}}cout << endl;
}void handler(int signum)
{cout << "获取了一个信号:" << signum << endl;sigset_t pending;int c = 10;while(true){sigpending(&pending);showPending(pending);c--;if(!c){break;}sleep(1);}
}int main()
{//内核数据类型,用户栈定义的struct sigaction act, oact;act.sa_flags = 0;sigemptyset(&act.sa_mask);act.sa_handler = handler;//设置进当前调用进程的PCB中sigaction(2, &act, &oact);while(true) sleep(1);return 0;
}
运行结果:
第二次获取二号信号的时候,就进行了屏蔽;
如果需要同时添加对其他信号的屏蔽:
#include<iostream>
#include<signal.h>
#include<unistd.h>using namespace std;static void showPending(sigset_t &pending)
{for (int sig = 1; sig <= 31; sig++){if (sigismember(&pending, sig)){cout << "1";}else{cout << "0";}}cout << endl;
}void handler(int signum)
{cout << "获取了一个信号:" << signum << endl;sigset_t pending;int c = 10;while(true){sigpending(&pending);showPending(pending);c--;if(!c){break;}sleep(1);}
}int main()
{//内核数据类型,用户栈定义的struct sigaction act, oact;act.sa_flags = 0;sigemptyset(&act.sa_mask);act.sa_handler = handler;//同时添加对其他信号的屏蔽sigaddset(&act.sa_mask, 3);sigaddset(&act.sa_mask, 4);sigaddset(&act.sa_mask, 5);sigaddset(&act.sa_mask, 6);sigaddset(&act.sa_mask, 7);//设置进当前调用进程的PCB中sigaction(2, &act, &oact);while(true) sleep(1);return 0;
}
运行结果:
三、可重入函数
在main函数调用insert方法时,信号来了,调用handler,handler也去调用insert,那么像这样被多个执行流调用insert就叫做函数重入;
函数重入出问题的叫做不可重入函数;
不出问题的叫做可重入函数;
函数的可重入性是函数的一种特征,我们目前使用的大多数函数,都是不可重入的;
四、volatile关键字
当接收到2号信号时,将flag置1,进程退出;
运行结果:
如果我们更改编译选项,让g++对代码作出一定的优化:
运行结果:
现在进程就无法退出了,但是flag还是变成了1;
这是因为在优化了代码之后,后面的语句没有更改flag,在后面检测flag的时候,就不访问内存中的flag了,而是检测寄存器edx中的flag;而寄存器中的flag是第一次读取的0,因此进程就不会退出了;
在变量定义的时候加上volatile关键字:
这个关键字的作用是**保持变量在内存中的可见性;**
运行结果:
注:优化是在编译时就完成的;
五、SIGCHILD信号
如果我们需要等待子进程退出,10个子进程5个退出,后面的信号还需要进行wait检测是否退出;
因为5个进程都发送了sigchild信号,但是OS只能收到一个;
这时主进程只能阻塞等待该子进程退出;
我们也可使用vector保存进程pid,来进行非阻塞遍历所有进程,这样不会被阻塞;
也可以在waitpid时候传入-1, 就可以等待任意一个退出的进程,进程也不会被阻塞;
- 如果我们不想等待子进程,还想在子进程退出之后,自动释放僵尸子进程:可以设置对SIGCHILD信号的忽略
运行结果:
子进程退出后自动回收僵尸子进程;
sigchild的默认动作就是忽略,但是为什么要再加一个忽略呢?
因为这两个忽略时不同等级的,OS的忽略就是默认动作,不会回收子进程,会形成僵尸进程;
而自己设置的忽略,告诉OS不光要忽略子进程,还要回收资源;