阻塞信号:
sigset_t 是一个在栈上定义的一个用户级变量,而这些数据添加并不会影响进程,因为 sigset_t 并没有设置进 PCB 内,所以我们必须经过系统调用设置进 OS,才能够影响进程、pending 等。
这里我们需要了解的函数是:
sigprocmask(int how, const sigset_t *set, sigset_t* oset);
// 该函数的作用是读取或更改信号屏蔽字
// 第一个参数是选择哪种方式
// 第二个参数是设置哪个信号
// 第三个参数是一种输出型参数,不需要可以置为NULL
oset 是可以看作 old set,我们调用函数会对信号屏蔽字进行修改,返回老的信号屏蔽字,万一哪一天想设置回来可以记得。
how 有三种方式:
1、SIG_BLOCK:添加信号屏蔽字,mask = mask | set;
2、SIG_UNBLOCK:接触信号阻塞,mask = mask & ~set;
3、SIG_SETMASK:覆盖信号,mask = set;
int sigpending(sigset_t *set)
// 获取当前 pending 有哪些信号,是输出型参数;
sigprocmask(SIG_SETMASK, &oset, NULL)
// 恢复曾经的信号屏蔽字
他的底层原理:
int sigaction(int signum, const struct sigaction *act, const struct sigaction *oldact)
// 和sigprocmask一样含义,但是这里的act是个结构体
// 我们重要关注act里的三个成员
act{
sa_flags; // 0
sa_mask; // sigemptyset(&act.sa_mask) 置空
sa_handler // = handler
}
这里的用法与 sigprocmask 大致相同,所以用 后者即可。
处理信号:
之前我们说过,信号在接收时不会立即处理,会到“合适”的时候处理,那是什么时候呢?当内核态切换到用户态时,会处理对应的信号。
什么是用户态:就算执行用户自己的代码,所在的权限是普通权限,叫做用户态;什么是内核态:当发生系统调用时,调用的是内核虚拟地址空间中的代码,必须是内核级别的权限,也就是 OS 通常执行代码的状态的内核态。
每个进程都有一个用户自定义的用户级页表,在内存中占3个G,还有一个全局的内核页表,在内存中占1个G,也就是我们常看的内存发布图区分的内存。所有的进程都看到的是同一份页表,也就是 OS 的代码和数据,所以进程无论如何切换,都能看到操作系统,但不一定都能访问。
用户态和内核态相比,内核态权限高,用户态用来执行普通用户的代码,是一种受监管的普通状态。
什么时候发生切换?当发生系统调用、时间片到了导致进程切换、异常中断、陷阱时就会发生状态切换,反之,当内核态处理完毕时就会返回用户态。
我们可以看一个概图,分别是自定义和默认情况下,状态的切换顺序:
问:自定义捕捉时,当前是内核态,可以直接访问用户态上的代码吗?
答:理论上是可以,但是我们绝不能这样做,因为万一有非法代码放在自定义中,会发生越界问题,比如删了一些本来没有权限删除的数据等等。在 OS 层面上看,OS 不信任任何用户,因为不保证是否是非法代码,但理论上可以这样操作,现实不允许,也就是只能想想。所以得先回到用户态执行,再回到内核态,再回用户态。
可重入函数:
两种或以上执行流都进入函数当中,叫做重入函数。一个执行流执行函数期间,被中断时处理信号,导致另一个执行流也进入了该函数,叫做该函数可被重入。一个函数被多个执行流进入,如果该函数发生错误,这种现象叫不可重入函数,反之,没事则是可重入函数。
我们之前调用的函数,STL接口等,是否是可重入的呢?基本全部都是“不可重入函数”,避免内部混乱,malloc / free,I/O 函数都是不可重入的。
volatile 关键字:
volatile 关键字的含义是保持内存可见性。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int g_flag = 0;
void handler(int signo)
{
printf("change flag\n");
g_flag = 1;
}
int main()
{
signal(SIGINT, handler);
while (!g_flag){
}
printf("end\n");
return 0;
}
当 main 函数中的 while(!flag),flag 是一个全局变量,在多执行流中可能存在对 flag 的修改,但是在编译器优化级别较高时,main 在循环当中,检测到并没有对 flag 进行修改,所以回直接把 flag 放到 CPU 的寄存器中,每次循环都检测寄存器上的 flag,而其他执行流下修改 flag 所在内存上修改的,寄存器上没有修改,所以一直会死循环。
那需要怎么做到优化呢?需要用到 -O3
mytest:test.c
gcc -O3 -o $@ $^
.PHONY:clean
clean:
rm -f mytest
那该怎么解决优化的问题?可以给变量前面加上 volatile 关键字修饰。编译器会理解成编译时不要把变量优化到寄存器中,不能够直接进寄存器的检测,要从内存读到寄存器中再检测,保持了内存的可见性。