1.前言
在之前的学习中,我们已经知道了信号是如何产生的,也知道了进程是如何记录一个信号的。如果进程收到了一个信号且没有屏蔽这个信号,则会将信号编号作为handler数组的下标执行该信号的处理函数。我们将处理特定信号的事件的过程叫做信号的捕捉。下面将带大家了解从操作系统的角度看是如何捕捉信号的。
2.了解内核态和用户态的区别
用户态和内核态是操作系统中两种不同的执行模式,对应着两种执行权限。处于内核态的执行模式下的执行权限比用户态的高。 遇到需要较高权限的代码,就进入内核态去执行。这两种模式对系统的安全和稳定性起到了至关重要的作用。下面来具体说说这两种状态。
- 用户态:像上面说的,用户态其实具有的权限是有限的,用户态下的程序不能直接访问硬件资源或者进行某些敏感的操作 ,如系统调用等。
- 内核态:内核态就是操作系统内核运行的模式,具有最高权限。内核态下的代码可以访问任何CPU指令并访问任何内存地址。内核态主要用于操作系统内核和设备驱动程序的运行。
2.1虚拟内存视角
从虚拟内存的角度上来看,内核态和用户态的区别体现在内存空间的分配和访问权限上。
我们知道,所有的进程都有一个统一的虚拟地址空间,以32为操作系统为例,3-4G的地址空间是内核空间(高地址),0-3G的地址空间是用户空间(低地址)。最重要的是,内核空间和用户空间都有各自的页表(内核级页表和用户级页表)。这也就意味着每一个进程的空间内,包含了操作系统本身这个程序的空间。
类似于共享空间,即使每一个进程空间都有内核空间,但所有进程的内核空间其实是共享的,这就说明了,内核级页表在任何进程中都是一样的,映射的都是同一个内核空间。这也解释了,为什么无论进程如何切换,我们都能找到操作系统。因为不同进程的3-4G的地址空间是同一个操作系统。
用户态和内核态访问进程空间的区别:
- 处于用户态的程序只能访问用户空间的内存地址,而无法访问内核空间的地址。
- 内核态可以访问整个虚拟空间地址,包括用户空间和内核空间。
2.2内核态和用户态的区别总结
- 权限不同:用户态权限有限,户态权限最高。
- 安全性:由于权限较低,用户态不能访问系统核心,较为安全。而内核态具有完全控制权,一旦出错可能导致整个系统崩溃。
- 系统调用:用户态不能执行系统调用,需要切换到内核态。
- 切换条件:用户态切换到内核态通常是发生中断或者是异常,是需要条件的。而内核态可以随时切换为用户态。
共同点:内核态和用户态之间的切换都涉及到上下文的切换,都会增加额外的开销
到此,我们就大概了解了什么是用户态与内核态,也知道了这两者之间的区别。但是这和信号的捕捉有什么关系呢?
处理信号会涉及到内核态和用户态之间的转换吗?答案是会的。无论是发生中断还是异常,都会伴随信号的产生以及捕捉 。当信号到达一个进程的时候,操作系统会进行一些必要的操作来处理这个信号。这个过程就涉及到内核态和用户态之间的切换。
3.内核如何实现信号的捕捉
如果信号的的处理动作是用户自定义的,在信号递达时就调用这个函数,这就称为捕捉信号。但是由于信号的处理函数是在用户空间的,处理的过程就势必会伴随用户态内核态之间的切换。假设用户程序自定义了SIGQUIT信号的处理函数sighandler。程序捕捉该信号是的状态变化过程如下:
- 开始运行程序,程序处于用户态
- 运行时进程收到信号
SIGQUIT
,发生中断。CPU保存上下文,并把当前进程切换为内核态,以便处理信号的相关操作。 - 由于信号的处理动作是自定义的,切换回用户态执行sighandler函数。
- 信号处理函数执行完,通常会执行特殊的系统调用sigreturn(该函数回到上次被中断的地方),此时又要切换回内核态。假如自定义信号处理函数调用了exit,则终止进程。
- 执行sigreturn,切换回用户态,回到上次中断发生的地方并向下继续执行代码。
至此,中断信号的产生到处理,一共经历了四次状态的切换。(假设中间没有终止进程)
为了更好理解,可以观察下图:
4.sigaction函数
sigaction
和signal都是用来处理信号的函数,但是sigaciton是一种更为强大和灵活的喜好处理机制,提供了更多的选项和更高的可靠性。
函数原型如下:
#include <signal.h> int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
signo
表示要捕捉的信号act
是指向sigaction
结构体的指针,用于指定信号新的处理行为。为NULL
使用系统默认的信号处理函数。lodact
也是指向sigaction
结构体的指针,用于保存旧的信号处理行为。为NULL
表示不保存。- 调用成功返回0,否则-1.
4.1sigactoin结构体
sigaction结构体定义如下:
struct sigaction { void (*sa_handler)(int); // 信号处理函数 void (*sa_sigaction)(int, siginfo_t *, void *); // 用于处理附加信息的信号处理函数 sigset_t sa_mask; // 在处理信号时阻塞的信号集 int sa_flags; // 行为标志 void (*sa_restorer)(void); // 不常用,可以忽略 };
sigaction结构体中不仅包含了信号的处理函数,还包含了很多其它的选项,例如可以在处理信号的时候阻塞其它信号(sa_mask),以防止竞争条件。
4.2扩展
当某个信号的处理函数正在执行时,内核将自动将当前信号加入进程信号的阻塞集合中。当信号处理完之后又会将该信号从阻塞信号集中移除。 这种机制保证了在处理信号的时候不会重复处理同种信号。也可以通过设置sa_mask的值来屏蔽其它信号。