四个交点(四次身份切换)
在用户态中因为一些原因陷入内核,执行系统调用后,在内核态中再进行信号的检测过程,再由内核态切换到用户态执行方法,完毕后再切换身份回到内核态,通过信号检测结束后,再身份切换,回到进程执行流中上次中断的地方。
三、sigset_t 信号集
我们知道信号是在进程的pcb中,即内核中。所以用户级操作难免会困难一些。所以sigset_t 信号集就是为了更好的在用户级操作信号所产生的类型。sigset_t 信号集包括 pending信号集、信号屏蔽字(block信号集)。sigset_t 底层就是一个大数组实现的位图结构。
信号集操作函数:
#include <signal.h>
int sigemptyset(sigset_t *set); //初始化set信号集
int sigfillset(sigset_t *set); //将信号集set全部设置为1
int sigaddset (sigset_t *set, int signo); //往set信号集添加信号
int sigdelset(sigset_t *set, int signo); //删除set信号集中的信号
这四个函数都是成功返回0,出错返回-1 。
int sigismember(const sigset_t *set, int signo); //判断信号是否在set中
sigismember 是一个布尔函数 , 用于判断一个信号集的有效信号中是否包含某种信号, 若包含则返回 1, 不包含则返回 0, 出错返回 -1 。
sigprocmask
调用函数 sigprocmask 可以读取或更改进程的信号屏蔽字 ( 阻塞信号集)
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
how就是下面的几种方式:
返回值 : 若成功则为 0, 若出错则为 -1
sigpending
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值 : 若成功则为 0, 若出错则为 -1
读取当前进程的未决信号集 , 通过 set 参数传出。调用成功则返回 0, 出错则返回 -1。
下面我们利用上面所学,来实现一个观察pending信号集,通过信号屏蔽子来观察pending信号集的变化:
#include <iostream> #include <vector> #include <signal.h> #include <unistd.h> // #define BLOCK_SIGNAL 2 #define MAX_SIGNUM 31 using namespace std; static vector<int> sigarr = {2}; //输出pending信号集 static void show_pending(const sigset_t &pending) { for(int signo = MAX_SIGNUM; signo >= 1; signo--) { if(sigismember(&pending, signo)) { cout << "1"; } else cout << "0"; } cout << "\n"; } //递达自定义动作 static void myhandler(int signo) { cout << signo << " 号信号已经被递达!!" << endl; } int main() { for(const auto &sig : sigarr) signal(sig, myhandler); // 1. 先尝试屏蔽指定的信号 sigset_t block, oblock, pending; // 1.1 初始化 sigemptyset(&block); sigemptyset(&oblock); sigemptyset(&pending); // 1.2 添加要屏蔽的信号 for(const auto &sig : sigarr) sigaddset(&block, sig); //批量化屏蔽 // 1.3 开始屏蔽,设置进内核(进程) sigprocmask(SIG_SETMASK, &block, &oblock); // 2. 遍历打印pengding信号集 int cnt = 10; while(true) { // 2.1 初始化 sigemptyset(&pending); // 2.2 获取它 sigpending(&pending); // 2.3 打印它 show_pending(pending); // 3. 慢一点 sleep(1); if(cnt-- == 0) { sigprocmask(SIG_SETMASK, &oblock, &block); // 一旦对特定信号进行解除屏蔽,一般OS要至少立马递达一个信号! cout << "恢复对信号的屏蔽,不屏蔽任何信号\n"; } } }
四、信号的处理细节
4.1 对于同类型信号的处理
当我们正在递达一个信号期间,同类型的信号无法被递达!(信号的处理细节)
当信号正在被递达中,又来了同类型的信号,此时当前信号会被加入到进程的信号屏蔽字,且会将pending中该信号对应的那一位由0变为1。(因为该信号被递达前,会将pending中对应的那一位由1改为0),若结束递达后,同类型仍发送,则会继续重复上面的动作。但若结束递达后,同类型的信号没有发送了,进程就只会再捕捉一次,将pending中的1改为0。递达后则继续检其他信号进行递达。
进程处理信号的原则是穿行的处理同类型的信号,不允许递归处理!
4.2 可重入函数和不可重入函数
举例说明:
在main执行流中,没有头结点的单链表进行头插,如上图所示:在执行到第一步时,此时被信号中断,结果导致main中还没有执行完又进入insert()中,最后回到main执行流中,再执行完剩下的代码结果导致内存泄漏等问题。
1.一般而言,main执行流和信号捕捉执行流是两个执行流!
2.如果在main中,和在handler中,该函数被反复进入:1出现问题的就是不可重入函数;2.没有出现问题的就是可重入函数。当然可重入和不可重入只是他们的特性,没有好坏之分。
4.3 volatile关键字
我们在读取变量的值时,一般会从内存中读取,但是由于编译器的优化,就会将内存中的值加载到cpu的寄存器中,从而之后访问该变量的值只会从寄存器中读取,如果这个变量的值被修改了,自然而然内存上的值也被修改了,但是寄存器中的值仍然没有变化,还是修改之前的值,所以为了避免这种优化产生的后果,我们就会在变量前加上volatile,意为一直从内存中读取值!
总结:
我们了解了信号的保存原来是通过进程pcb中的pending、block位图,handler函数指针数组来进行保存,从而信号递达。