目录
我们之前说信号产生后OS向进程的PCB中写入信号,那么什么时候对信号进行处理呢?
就是进程从内核态切换回用户态的时候,信号会被检测并处理。
信号的处理
这里就不得不提什么是内核态,什么是用户态,其实我们平常在调用系统调用的时候就会去执行OS的代码,这时我们就说进程陷入了内核,目前处于内核态。那有可能我们的代码不调用系统调用,那它怎么会进入内核态呢?不要忘记,进程是基于时间片进行调度的,当时间到了进程会从CPU中剥离下来,这时就是OS在执行操作,就是处于内核态。
所以基于上面的一些原理,我们就可以模拟出进程从内核态和用户态之间转换的图,就像下面一样:
这个图是信号处理方法被自定义时需要走的路线,如果只是默认处理或忽略信号时,只需要在内核态处理完直接杀死进程或修改pending位图返回用户态就够了,这里是进程需要切换回用户态执行用户写的方法,那为什么不能直接在内核态执行用户写的方法呢?肯定也是为了安全考虑,内核态的权限较高,用户有可能在自定义方法中越界访问一些信息
并且有些信号的默认处理动作是杀死进程(core、term),为什么OS不直接杀死进程,而要把信号写道pending位图中,等到合适的时候进程自己终止?因为进程可能在做一些很重要的工作,比如它申请了资源,直接杀死可能会导致资源泄露或数据不一致,所以OS必须允许进程进行必要的清理工作。
那为什么CPU在运行进程的时候永远都能找到操作系统呢?这个就得从进程地址空间说了,我们知道一个进程的地址空间有4G,其中0-3G都存的是关于进程有关的信息,剩下3-4G就是映射的OS,所以,一个进程要调用系统调用只需要在它自己的地址空间中跳转即可。
这也就解释了为什么会有用户态和内核态,当执行用户级别的代码时,此时就是用户态,权限较低,当执行系统级别的代码时,此时就是内核态,权限较高。并且CPU中有个寄存器叫CS,这里边存的就是权限标识,为0就是内核态,为3就是用户态
操作系统是如何运行的呢?其实操作系统本身就是一个死循环,它需要不断接受来自外部的硬件中断来执行相应的操作。比如有周期性的时钟中断,信号技术本身就是通过软件的方式来模拟的硬件中断。
sigaction
我们之前说信号捕捉可以用signal,今天再介绍一种方法:
man sigaction
它还有一个同名结构体
第一个参数就是要捕捉哪个信号,第二、三个参数是传入一个结构体对象的指针,我们可以创建这个结构体对象,并修改对象的内容,那么我们具体来用一下
对于这个结构体创建的对象,我们只需要管第一个和第三个成员变量即可,其他的都给成0或nullptr。
第三个成员变量是当我们处理比如二号信号时,进程会默认阻塞二号信号,如果你还想阻塞其他的信号,你可以通过这个成员变量去告诉OS。那么处理信号的时候,内核就会自动阻塞这些信号,处理完,这些信号也会从信号屏蔽字中去除。
那为什么进程会默认阻塞当前正在处理的信号呢?因为如果我们目前正在处理二号信号,但是又来了一个二号信号,那我们就可能会嵌套处理二号信号,这是没有意义的,所以为了避免这种情况,操作系统就会默认阻塞。
我们也可以来证明一下进程在处理一个信号时会默认阻塞这个信号,我们的证明方法就是在处理比如2号信号时再发2号信号,然后打印pending位图
void printpending(sigset_t &pending) { for (int i = 31; i > 0; i--) { if (sigismember(&pending, i)) cout << "1"; else cout << "0"; } cout << endl; } void handler(int sig) { cout << "signal: " << sig << endl; sigset_t pending; sigemptyset(&pending); while (1) { sigpending(&pending); printpending(pending); } } int main() { struct sigaction act, oact; act.sa_handler = handler; act.sa_flags = 0; sigemptyset(&act.sa_mask); sigaction(2, &act, &oact); while (1) sleep(1); return 0; }
我们可以看到二号信号确实被阻塞了
volatile
用这个关键字修饰变量就是告诉编译器不要把变量放到寄存器中,要一直从内存中读取。
因为如果我们编译时让编译器进行优化,那么它是有可能把变量放到寄存中的,比如
int g_flag=0; void handler(int sig) { cout<<"g_flag 0 -> 1"<<endl; g_flag=1; } int main() { signal(2,handler); while(!g_flag); cout<<"process quit success"<<endl; return 0; }
我们可以用下面的指令,让编译器进行优化
g++ test.cc -O1
这里必须是大写的O,小写的编译器会认为是形成哪个文件,后面的1可以是2可以是3,表示优化等级。
这个代码我们如果不优化的话,当进程收到2号信号就会改掉g_flag的值,从而程序退出。但是优化的话g_flag这个变量因为编译器检查到,下面没有对g_flag进行修改的地方,所以它就会被放到寄存器中,从而我们修改内存中的值也不会影响到寄存器,所以程序不会停止。
于是,在这种情况下,在初始化变量的时候我们就可以这样写:volatile int g_flag=0;这样就算优化也不会把g_flag放到寄存器中。
SIGCHLD
SIGCHLD是17号信号,子进程退出时会向父进程发送此信号。并且这个信号父进程的默认动作是忽略,所以之前我们没有发觉。
我们之前父进程等待子进程退出是主动等待,是阻塞式等待或非阻塞轮询等待,父进程就无法专心忙于自己的工作,但是在我们学习了这个信号后,父进程可以收到子进程信号后在处理信号时等待。
于是在面对同时有多个子进程且只有部分子进程退出时,我们可以选择循环非阻塞式等待,这样就可以保证父进程等待完所有要退出的子进程后就退出等待了。
void handler(int sig) { if(sig==SIGCHLD) { while(1) {//-1表示等待任意子进程 pid_t rid=waitpid(-1,nullptr,WNOHANG); if(rid>0) { cout<<"wait child success"<<endl; } else if(rid<=0) { break; } cout<<"wait sub process done"<<endl; } } }
有时如果我们不想管子进程,但是不等待就会产生僵尸进程,所以这时Linux下有一种特殊处理,就是将SIGCHLD的处理动作改为SIG_IGN
这样子进程在退出后会自动清理掉,不会产生僵尸,也不会通知父进程。