理解用户态和内核态

结论:

以32位举例,进程地址空间分为 [0,3GB]用户区,[3,4GB]内核区。

进程地址空间有两张页表,内核页表用户页表

内核页表在物理内存中只有一份,用户页表有多份。

无论进程如何调度,我们总能找到操作系统。

OS为了保护自己,不相信任何人,必须采用系统调用的方式进行访问!

操作系统系统调用方法的执行,是在进程地址空间上执行的。

在OS中,OS或用户如知道现在处于内核态还是用户态?

在x86架构中,CPU通过当前特权级(CPL, Current Privilege Level)标识当前状态,存储在CS段寄存器的低2位。

  • CPL=0:内核态(Ring 0)
  • CPL=3:用户态(Ring 3)

系统调用

fork()

{

        mov eax n;

        syscall  0x80 //更改CPU特权级别,陷入内核

}

sigaction

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

signum:需要自定义捕捉的信号编号

act:如何处理自定义捕捉方法

结构:

struct sigaction {
               void     (*sa_handler)(int); //自定义捕捉方法
               void     (*sa_sigaction)(int, siginfo_t *, void *);  //...
               sigset_t   sa_mask; //需要额外屏蔽的信号集
               int        sa_flags;  //...
               void     (*sa_restorer)(void);  //...

};

当某个信号处理函数被调用了,该信号就会被屏蔽,直到处理函数返回,信号屏蔽就会恢复到原来状态。(目的,为了防止在该信号处理函数过程中,又收到了该信号,造成类似递归调用)。

sa_mask就是在信号处理函数过程中需要额外屏蔽的信号集。
验证信号屏蔽以及sigaction的使用:
#include <iostream>
#include <signal.h>
#include <unistd.h>

void handler(int signum)
{
    std::cout << "信号捕捉到:" << signum << std::endl;
    // 循环打印pending表,观察2号信号屏蔽情况
    while (true)
    {
        sigset_t set;
        sigpending(&set);
        for (int i = 31; i >= 1; i--)
        {
            if (sigismember(&set, i))
                std::cout << "1";
            else
                std::cout << "0";
        }
        std::cout << std::endl;
        sleep(1);
    }
}

int main()
{
    // signal(2, handler);
    struct sigaction act, oact;
    // 此时额外的屏蔽信号集为{3,4,5}
    sigemptyset(&(act.sa_mask));
    sigaddset(&act.sa_mask,3);
    sigaddset(&act.sa_mask,4);
    sigaddset(&act.sa_mask,5);
    // 自定义捕捉方法
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigaction(2, &act, &oact);

    while (true)
    {
        std::cout << "pid: " << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}

可重入函数

举例:一个全局链表,main函数中有头插,信号处理方法也有头插。

        当main函数执行头插函数时,执行完头插的第一句之后,因某种原因进入内核,恰好执行对应信号的中断处理方法,二次执行完头插,回到main执行流之后,头插函数执行完第二句。此时node2节点就是一个不会被销毁的节点,丢失了,造成内存泄漏。

main执行流和handler执行流 -> insert方法,被两个以上的执行流进入了-> 函数被重入了

不可重入函数:重入有可能造成资源的丢失或行为异常,如果函数使用资源是全局共享且没有被保护,那就是不可重入的。

可重入函数:函数只有自己的临时变量。

volatile

volatile作用:保证资源的可见性。

CPU运算流程:从物理内存搬数据到寄存器->从寄存器取数据运算->或将结果写回内存

例子:如图,main函数中没有直接调用handler函数的语句,并没有关联。编译器优化情况比较高的情况下:flag->register->优化到寄存器中,只进行一次第一步,之后只有第二步,即使发信号修改了flag的内容,CPU依旧用寄存器中的原始数据(寄存器覆盖了进程看到变量的真实情况,内存不可见了)。

SIGCHLD信号

子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略。
        子进程回收可以采用对SIGCHLD信号自定义捕捉的方式来完成,自定义捕捉中采用WNOHANG非阻塞调用+死循环来等待子进程,确保完全回收和不影响父进程。
        若不关心子进程状态码,可以父进程使用signal(SIGCHILD, SIG_IGN); 在子进程退出时会自动回收。
#include <iostream>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>

void handler(int signum)
{
    std::cout << "信号编号为:" << signum << std::endl;
    while (true)
    {
        pid_t n = waitpid(-1, nullptr, WNOHANG);
        if (n == 0)
            break;
        else if (n < 0)
        {
            std::cout << "等待错误" << std::endl;
            break;
        }
    }
}

int main()
{
    // signal(SIGCHLD, handler);
    signal(SIGCHLD,SIG_IGN); //等同于信号捕捉方式回收,只不过拿不到状态码
    for (int i = 0; i < 10; i++)
    {
        pid_t pid = fork();
        if (pid == 0)
        {
            std::cout << "child exit" << std::endl;
            if(i<=5) exit(1);
            while(true) {;}
        }
    }
    while (true)
    {
        std::cout << "father running" << std::endl;
        sleep(1);
    }
    return 0;
}
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐