【Linux】详解用户态和内核态&&内核中信号被处理的时机&&sigaction信号自定义处理方法

北飞的山羊 2024-06-11 08:37:04 阅读 58

一、用户态和内核态的理解

        在操作系统中,用户态和内核态是两种主要的执行模式,它们代表了不同的访问级别和权限,用于确保系统的安全和稳定性。

1.1、用户态

        用户态是操作系统中用户进程的运行状态。在这种状态下,进程只能访问受限的系统资源,并且不能执行某些特权操作。用户态下的进程没有权限直接访问硬件或执行某些敏感的系统调用。它们必须通过系统调用接口来请求内核态的服务。

1.2、内核态

        内核态是操作系统的核心部分(即内核)的运行状态。在这种状态下,代码可以访问系统内的所有内存空间,并且可以执行特权指令。内核态下的代码具有最高级别的权限,可以访问硬件、执行敏感操作,并管理系统资源。

1.3、用户态和内核态的切换时机

        当用户态下的进程需要执行特权操作时,它会通过系统调用接口向内核发出请求。这时,操作系统会保存用户态的上下文,然后切换到内核态来执行相应的服务。除了系统调用外,如硬件中断、软件异常也会导致系统从用户态切换到内核态。在这三种情况下,操作系统都会保存用户态的上下文,并在内核态下处理这些事件。

二、信号被处理的时机

        如下图所示,进程从内核态切换回用户态的时候,信号会被检测并处理。如果该信号的处理方法为默认处理方法,就不需要从内核态切换回用户态来处理我们写的方法,但如果该信号的处理方法为我们自己的自定义处理方法,就要从内核态切换回用户态来执行,执行完毕程序再进入内核。下面是进程处理信号并执行我们写的自定义处理方法的过程:

        执行信号自定义的处理方法时,可以由操作系统直接帮我们做了,但是为了避免我们写的处理方法存在非法操作,就必须切换回用户态, 由用户态的权限来约束我们是否能执行这个处理方法

三、用户态和内核态切换的内核级理解

在我们的进程地址空间中存在1G的内核空间,这部分会用来映射加载到内存中的操作系统。

         在内核中,所有的系统调用函数其实是被用一个函数指针数组所管理起来的,该函数指针数组会通过页表的映射与1G的内核空间的某些虚拟地址建立起映射关系,这样当正文代码部分要调用某个系统调用函数时,只需要拿着虚拟地址在内核空间中寻找再通过页表的映射就可以在内存中找到该系统调用函数。

        在系统中会存在多个进程,每个进程的地址空间的[0,3G]的地址空间映射的内容都不相同,但是,每个进程都要进行系统调用 ,也就意味着地址空间中的1G内核级空间所映射的内容可以完全相同。进一步说,我们的进程无论如何总能找到操作系统,我们的进程访问操作系统,都是通过进程地址空间中[3GB,4GB]这一部分空间来访问的。

        既然操作系统已经被映射到我们进程的地址空间上,那是否意味着我们可以随便访问操作系统中的内容了呢?答案肯定不是的。在CPU中有寄存器可以标识当前进程的状态(比如说00就表示处于用户态,11就表示处于内核态),如果进程处于用户态就阻止进程访问操作系统

四、设置自定义信号处理的函数

 设置自定义信号处理的函数除了signal函数外,还有一个sigaction函数。

        第一个参数为几号信号,第二个参数为一个描述新方法的结构体, 第三个参数为一个描述旧方法的结构体。

        该结构体中第一个成员变量为新设置的信号处理方法,第二个参数为类似于sa_handler,但它提供了更强大的功能,因为该函数还可以接收有关信号发送者的信息,可以设置为nullptr,第三个参数为一个信号集,可以用来设置在处理signum信号的同时所屏蔽的信号,第四个参数用于修改信号处理的某些默认行为,第五个参数通常不用于现代操作系统,不设置。 下面是一个sigaction函数使用的例子。

自定义2号信号的处理方法,让进程不断打印pending位图:

#include <iostream>#include <signal.h>#include <unistd.h>using namespace std;void print(sigset_t& pending){ for(int i = 31; i>= 1; i--) { if(sigismember(&pending, i)) cout << "1"; else cout << "0"; } cout << endl;}//自定义2号信号的处理方法,让进程不断打印pending位图void handler(int sig){ sigset_t pending; sigemptyset(&pending); while(true) { sigpending(&pending); print(pending); sleep(1); }}

再让进程收到2号信号的同时阻塞3,4,5号信号:

int main(){ struct sigaction act,oact; act.sa_handler = handler; act.sa_flags = 0; cout << getpid() << endl; //清空要添加的信号集 sigemptyset(&act.sa_mask); //让进程收到2号信号的同时阻塞3,4,5号信号 sigaddset(&act.sa_mask, 3); sigaddset(&act.sa_mask, 4); sigaddset(&act.sa_mask, 5); sigaction(2, &act, &oact); while(true) {} return 0;}

最终运行结果:

        第一次给进程发送2号信号,信号被执行了所以pending位图中没有2号信号,再次发送2号信号发现pending位图中有2号信号,证明2号信号被屏蔽了,因为进程在处理某个信号时如果再次给它发送同一个信号该信号会被自动屏蔽,依次再向进程发送3,4,5号信号,发现信号都被屏蔽了。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。