本文在内核态和用户态参考了这篇博客,特别鸣谢!
1. 信号什么时候被处理
当进程从内核态返回到用户态的时候,进行信号的检测和处理
什么内核态,什么又是用户态呢?
当进程在CPU上运行时,内核态:允许进程访问操作系统的代码和数据,用户态:进程只能访问用户自己的代码和数据
为什么要有,用户态和内核态呢?
因为进程需要访问系统内的资源,以及调用系统函数接口,例如IO就是系统调用,一个进程只有在内核态通过系统调用才能访问操作系统的资源,这样可以保证系统的安全性
从用户态切换到内核态的方式:
- 系统调用(int 80h)
- 异常
- 外围设备中断
系统调用:用户态进程主动切换到内核态的方式,用户态进程通过系统调用向操作系统申请资源完成工作,例如 fork()就是一个创建新进程的系统调用,系统调用的机制核心使用了操作系统为用户特别开放的一个中断来实现,如Linux 的 int 80h 中断,也可以称为软中断
异常:当CPU在执行用户态进程时,发生了一些没有预知的异常,这时当前运行进程会切换到处理此异常的内核相关进程中,也就是切换到内核态,如缺页中断
中断:当CPU在执行用户态进程时,外围设备完成用户请求的操作后,会向 CPU发出相应的中断信号,这时CPU 会暂停执行下一条即将要执行的指令,转到与中断信号对应的处理程序去执行,也就是切换到了内核态。如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后边的操作等。
CPU内部有一个ecs寄存器,它的后两位标识进程属于内核态还是用户态,00标识内核态,11标识用户态。int 80 汇编语句可以让ecs从11变成00,从用户态切换到内核态
这里的00和11其实分别对应CPU指令集的操作权限 ring0 和 ring3,指令集是操作系统实现软件指挥硬件执行的媒介。具体来说,就是每一条汇编语句都对应了一条CPU指令,很多个CPU指令在一起就组成了一个,甚至多个指令集。
指令集又根据权限把指令集划分成ring0~3(硬件层面上),ring0权限最高,可以使用所有CPU指令集,而ring3只能使用常规的指令集,不能使用关于硬件操作的指令集,例如IO读写
linux只采用ring0和ring3这两个权限
内核态和用户态分离是如何实现的?
操作系统的代码和数据在物理内存中只有一份,在计算机刚刚启动的时候,就是在加载OS,内核级页表在操作系统中也是只有一份,它映射整个操作系统的代码和数据,而且每个进程的虚拟内存的内核空间的内容都是一样的,都是通过内核级页表映射来的。每个进程共享内核空间,而用户空间是进程独有的,所以用户级页表有很多个,每个进程独有一份。
当进程需要访问系统资源,调用系统接口,就会从用户态切换到内核态,代码从用户区的代码跳转到内核区执行,当内核区的代码执行完,也就会从内核态切回用户态,也就是在这个时候,OS会检测进程的pending信号集。
2. 信号如何被处理
处理流程:
如果信号的处理动作不是自定义的,那么就会在第三步处理信号,处理完毕后如果进程还活着,就返回用户模式,并继续执行
sighandler和main是两个独立的控制流,使用的是不同的栈空间
在上一篇博客留下了一个问题:
如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在函数返回前,至少将其中一个信号递达(在下一篇博客中解答为什么)
因为当调用sigprocmask,也就是系统调用,就会从用户态切换到内核态,在函数返回前,也就是从内核态切回到用户态前,会进行信号检测,也就会信号递达,至少一个是为啥呢?
这是因为可能在自定义信号处理的过程中发生状态切换,可能是系统调用,也可能是中断,都有可能。自定义信号处理是上面图的第四步,处于用户态,如果切换成内核态再切回的过程中,就会再一次处理信号。甚至可以一直这样。
但是这种情况,有时候并不是我们想要的,例如一个进程一直再给另一个进程发送某个信号,这就可能会出现,当前自定义的信号还没处理完,接着又去处理下一个信号。特别是同一个信号,最容易出现这种情况,会死循环。
但是其实操作系统提供了对应的接口设置,操作系统的设计者已经想到了
函数功能:
自定义信号的处理方法,设置信号屏蔽字,当处理该信号时,内核会提前把sa_mask加入到block位图中,默认会把当前信号加入,确保在自定义函数处理过程中,不会再处理不想处理的信号,信号处理函数返回时自动恢复原来的信号屏蔽字
参数:
signum:信号编码
act:设置信号处理方法,输入型参数
oldact:旧的信号处理方法会通过oldact传出,输出型参数
返回值:
0标识成功,-1标识失败
在Linux下,当子进程退出或者暂停时,会向父进程发送SIGCHLD信号,父进程对于SIGCHLD信号的默认处理动作是忽略,但是我们知道,如果父进程不等待回收子进程,子进程会一直保持僵尸状态,进程PCB会保持下来。
但是如果我们想要子进程自己处理完,就退出并且释放自己的资源,父进程不关心子进程的退出结果,要怎么办呢?
可以通过sigaction或者signal修改信号的默认处理动作为SIG_IGN,这样就可以,可见Linux下系统的默认处理动作为忽略和自定义处理动作忽略还是有区别的。
当然这里我们也可以当父进程修改信号自定义处理动作为非阻塞等待,来获取子进程退出的结果,代码如下:
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <wait.h>
using namespace std;
void handler(int sig)
{
cout << "捕捉到:" << sig << endl;
int status = 0;
pid_t pid = 0;
while((pid = waitpid(-1, &status, WNOHANG)) > 0)
{
cout << "child pid" << pid << endl;
cout << "!!!!!!!!!!!!" << endl;
if(WIFEXITED(status))
cout << "退出码为:" << WEXITSTATUS(status) << endl;
else if(WIFSIGNALED(status))
cout << "终止信号:" << WTERMSIG(status) << endl;
}
}
// 回收子进程通过信号
int main()
{
struct sigaction act;
act.sa_handler = handler;
sigaction(SIGCHLD, &act, nullptr);
// 4. 自定义SIG_IGN
signal(SIGCHLD, SIG_IGN);
if(fork() == 0)
{
cout << "child pid" << getpid() << endl;
sleep(5);
exit(0);
}
while(true) sleep(1);
return 0;
}
完。