1.信号的产生
信号递达:实际执行信号的处理动作称为信号的递达
信号未决:信号从产生到递达之间的状态
进程可以阻塞某个信号
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作
注意,忽略和阻塞是不同的,只要信号阻塞就不会被递达,而忽略是递达之后可选的一种处理动作
pending表:用来存放接收到的信号,操作系统向进程发送信号时,都会修改pending表中对应编号处的比特位
block表:用来存放被阻塞的信号,当指定信号需要被阻塞时,操作系统会修改block表中对应编号处的比特位。
handler表:这是是一个数组,用来存放不同信号的处理方法,保存的是函数指针。
比特位的位置:表示信号的编号
比特位的内容:表示是否对特定的信号进行屏蔽(阻塞)
block pending handler
0 0 SIG_DFL
block为0,没有阻塞1号信号
pending为0,没有收到1号信号
handler为SIG_DFL,对1号信号默认的处理方式为SIG_DFL
信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志
2.sigset_t
未决和阻塞可以用相同的数据类型sigset_t来存储,sigset_t 称为信号集,这个类型可以表示每个信号的"有效"或"无效"状态,在阻塞信号集中"有效"和"无效“的含义是该信号是否被阻塞,在未决信号集中"有效"和"无效"的含义是该信号是否处于未决状态。
3.信号集操作函数
int sigemptyset(sigset_tset)
函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号
int sigfillset(sigset_tset)
函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号、
在使用sigset_t 类型的变量之前,一定要调用sigemptyset或者sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t 变量之后就可以再调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号
int sigaddset(sigset_tset)
int sigdelset(sigset_tset,int signo)
int sigdelset(sigset_tset,int signo)
int sigismember(const sigset_tset,int signo) 包含返回1,不包含返回0,出错返回-1
sigprocmask
int sigprocmask(int how,const sigset_t *set,sigset_t *oset);
返回值:若成功为则为0,若出错则为-1
如果oset是非空指针,则读取当前进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何修改。
SIG_BLOCK set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set
SIG_UNBLOCK set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=nask&~set
SIG_SETMASK 设置当前信号屏蔽字为set所指向的值,相当于mask=set
sigpending
读取当前进程的未决信号集,通过set参数传出。调用成功返回0,出错返回-1
4.用户级页表和内核级页表
用户态:正在执行用户层的代码,此时CPU的状态是用户态。
内核态:正在通过系统调用访问内核或者硬件资源时,此时CPU的状态是内核态。
操作系统是怎么知道当前进程的身份状态的呢?
CR3寄存器:专门用来表征当前进程的运行级别的。
0:表示内核态,此时访问的是内核资源或者硬件。
3:表示用户态,此时执行的是用户层的代码。
我们之前一直所说的页表都是用户级页表,每个进程都有一个。
进程地址空间的大小一共有4GB,我们之前谈论的只有0~3GB,这3GB的空间属于用户空间,用来存放用户的代码,数据等。为了保证进程的独立性,每个进程都有一个进程地址空间,都有一个用户级页表。
还有一共内核级页表,所有进程共用一份。
进程地址空间中的3~4GB空间,是不允许用户访问的,因为这1GB空间中的数据等,通过内核级页表和内存中的操作系统相映射,属于内核级别的。因为内存中只存在一份内核,所以所有进程的虚拟地址空间的这1GB空间都通过同一份内核级页表和内存中的内核相映射。
每一个进程地址空间中的3~4GB的内容都是一样的,因为它们都通过同一个内核级页表和内存映射。
动态链接是通过代码段的位置无关码跳转到共享区从内存中映射过来的动态库来执行相应的方法
系统调用是:
①.当执行到代码段中的系统调用时,会在跳转到当前进程虚拟地址空间中的内核空间中。
②.系统调用的具体实现都放在这1GB的内核空间中。
③.然后根据内核级页表和内存中内核的映射关系实现内核的访问。
为什么我们的代码中不能访问这3~4GB的空间,而系统调用就跳转到这1GB的内核空间中进行访问了呢?
因为从代码段跳转到内核空间中后,CPU中的CR3寄存器从3变成了0。
意味着进程运行级别从用户态变成了内核态,也就是执行者从用户变成了操作系统,所以可以对这1GB的内核空间进行访问。
系统调用接口的起始位置,会将CR3寄存器中的数据从3变成0,完成从用户态向内核态的转变。
5.信号的处理
(如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号)
信号在合适的时候被处理-什么时候?进程从内核态返回到用户态的时候,进行信号的检测和信号的处理
默认处理方式:所有信号的默认处理方式都是结束进程,只是不同信号表示不同的异常。
忽略处理方式:忽略和阻塞不一样,忽略也是一种处理方式,它仅仅是将task_struct中的pending位图中对应信号的比特位清空,然后就直接返回到用户态了。
user mode ①.在执行主控制流程的某条指令时因为中断、异常或系统调用进入内核、
kernel mode ②.内核处理完异常准备回用户模式之前先处理当前进程中可以递送的信号
kernel mode ③.如果信号的处理动作自定义的信号处理函数则回到用户模式执行信号处理函数(而不是回到主控制流程)
(如果不是自定义处理方式,是SIG_DFL和SIG_IGN,以内核态身份进行处理,然后就可以直接返回到用户代码中系统调用的位置,少了两次系统调用)
user mode ④.信号处理函数返回时执行特殊的系统调用sigreturn再次进内核
kernel mode ⑤.返回用户模式从主控制流程中上次被中断的地方继续向下执行
在进程开始运行后,我们在10s内发送了很多次2号信号,但是最终只捕获了两次。
当递达第一个2号信号的时候,同类型的信号无法被递达。
因当前信号在被捕捉的时候,系统会自动将当前信号加入到进程的信号屏蔽字,也就是将block对应的比特位置位,然后将pending表对应比特位清空,再去进行递达。
但是第二个2号信号在第一个信号被捕捉的时候会将对应pending位图的比特位置位。
所以当第一个2号信号处理完毕以后,解除对2号信号的屏蔽后,第二个2号信号就会被递达。
除了这两个2号信号,其余的2号信号都被舍弃了。
注意: 进程处理信号的原则是串行的处理同类型的信号,不允许递归,所以同类型的多个信号同时产生,最多可以处理两个。