【Linux】进程 信号保存 信号处理 OS用户态/内核态

news2025/6/7 0:05:44

🌻个人主页:路飞雪吖~

       🌠专栏:Linux


目录

一、信号保存

✨进程如何完成对信号的保存?

 ✨在内核中的表示

✨sigset_t

✨信号操作函数

🪄sigprocmask --- 获取或设置当前进程的 block表

🪄sigpending --- 获取当前的pending信号集

二、信号捕捉

✨信号捕捉的流程

✨sigaction​编辑

✨操作系统是怎么运行的

🍔硬件中断

🍔时钟中断

🍔死循环

🪄时间片

🍔软中断

🍔缺页中断?内存碎片处理?除零野指针错误?

三、🌟如何理解内核态和用户态


一、信号保存

🌠信号相关常见概念

• 实际 执行信号 的处理动作称为 信号到达;

• 信号从 产生到递达 之间的状态,称为信号未决;

• 进程可以选择 阻塞某个信号【阻塞特定信号[叫屏蔽信号,与IO阻塞没有任何联系],信号产生了,一定把信号进行pending(保存),永远不递达,除非我们解除 阻塞】;

• 被阻塞的信号产生时,将保持在 未决状态,直到进程解除对此信号的阻塞,才执行递达的动作;

• 阻塞 和 忽略 是不同的,只要信号被阻塞 就不会递达,而忽略是在递达之后可选的一种处理动作。

✨进程如何完成对信号的保存?

pending [信号]位图 :当前进程收到的信号列表;

handler_t XXX[N] :函数指针数组,指向信号的处理方法

   信号编号 -1 :就是函数指针数组的下标!

block [屏蔽]位图:是否屏蔽信号;

• pending 例子: 当【kill -2】发送2号信号,从右往左的第二个 比特位 由 0 --> 1,此时就向该进程发了一个 2号 信号,从右往左的 第几个 比特位 信号就是几.

• block 例子:SIGINT(2) 信号2,当前的block位 为 1, 表示的是把2号信号屏蔽,即便pending 收到了 2号 信号,这个 2号 信号 也无法执行对应的 handler,即禁止2号信号进行递达,除非把 2号 信号 的 block 位 由 1 --> 0,pending 的2号信号才会执行对应的方法 。

 屏蔽一个信号[block] 和 当前是否收到这个信号[pending] 两者是没有关系的,因为它们是两个位图,修改时没有联系。

进程能识别信号本质是:每一个进程的每一个信号都能横着看这三张表来识别信号,是程序员内置的特性【这些代码的数据结构内核都是程序员写的】。

 ✨在内核中的表示

// 内核结构 2.6.18 
struct task_struct {
 ...
 /* signal handlers */
 struct sighand_struct *sighand;
 sigset_t blocked
 struct sigpending pending;
 ...
}
 
struct sighand_struct {
 atomic_t count;
 struct k_sigaction action[_NSIG]; // #define _NSIG 64
 spinlock_t siglock;
};

struct __new_sigaction {
 __sighandler_t sa_handler;
 unsigned long sa_flags;
 void (*sa_restorer)(void); /* Not used by Linux/SPARC */
 __new_sigset_t sa_mask;
};

struct k_sigaction {
 struct __new_sigaction sa;
 void __user *ka_restorer;
};

/* Type of a signal handler. */
typedef void (*__sighandler_t)(int);
struct sigpending {
 struct list_head list;
 sigset_t signal;
};

✨sigset_t

从上图来看,每个信号只有⼀个bit的未决标志,非0即1, 不记录该信号产生了多少次,阻塞标志也是这样 表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t 称为信号集 ,这个类型 可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号 是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的 信号屏蔽字(Signal Mask), 这⾥的“屏蔽” 应该理解为阻塞而不是忽略。

✨信号操作函数

不建议直接使用 位操作 来对位图直接进行设置、查找、检测。Linux直接提供了一组接口,可以用来对该信号集直接进行比特位的操作:

对位图增删查改:

#include <signal.h>

int sigemptyset(sigset_t *set); // 把对应的信号集 做 清空

int sigfillset(sigset_t *set); // 信号集 全部 置 1

int sigaddset(sigset_t *set, int signo); // 向指定的信号集当中,添加信号

int sigdelset(sigset_t *set, int signo); // 从指定的集合当中,移出某个信号

int sigismember(const sigset_t *set, int signo); // 判断一个信号是否在集合里

• 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。

• 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号 包括系统支持的所有信号。

• 注意,在使用sigset_t类型的变量之前,⼀定要调用 sigemptyset 或 sigfillset 做初始化,使信号集处于 确定的状态。初始化sigset_t变量之后就可以在调用 sigaddset 和 sigdelset 在该信号集中添加或删 除某种有效信号。 

🪄sigprocmask --- 获取或设置当前进程的 block表

读取或更改进程的信号屏蔽字【阻塞信号集】。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
返回值:若成功则为0,若出错则为-1 

🪄sigpending --- 获取当前的pending信号集

#include <signal.h>
int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。
调⽤成功则返回0,出错则返回-1

🌠 为什么 pending表 不提供 修改的方法函数,只提供检查的方法呢?

OS不需要提供操作pending的方法,因为 信号产生的5种方式【键盘、指令、系统调用、软件条件、异常】全部都在修改pending表,所以不需要提供修改。

🌠handler 表 由谁来修改?

signal() 函数,一直都在修改这个表。

sigset_t 是OS提供的数据类型,这个数据类型定义的变量 是在哪里开辟的空间 --> 用户栈上开辟的空间。

#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <string>
#include <signal.h>
#include <functional>
#include <vector>
#include <sys/wait.h>

void PrintPending(const sigset_t &pending)
{
    std::cout << "curr pending list [" << getpid() << "] :";
    for (int signo = 31; signo > 0; signo--)
    {
        if (sigismember(&pending, signo))
        {
            std::cout << 1;
        }
        else
        {
            std::cout << 0;
        }
    }
    std::cout << std::endl;
}

void non_handler(int signo)
{
    std::cout << "处理" << signo << std::endl;
}

int main()
{
    // 不让 2号 信号 执行退出
    ::signal(2, SIG_IGN);
    //::signal(2, non_handler);


    // 1. 对2号信号进行屏蔽
    // sigset_t OS提供的数据类型
    // 栈上开辟的空间
    sigset_t block, oblock;
    // 对空间进行清0
    sigemptyset(&block);
    sigemptyset(&oblock);

    // 1.1 添加2号信号
    // 我们有没有把对2号信号的屏蔽,设置进入内核中?
    // 只是在用户栈上设置了block的位图结构,并没有设置进入内核中
    sigaddset(&block, 2);
    // 1.2 设置进内核中
    sigprocmask(SIG_SETMASK, &block, &oblock); // 把当前的信号集统一进行替换
    
    int cnt = 0;

    // 2. 获取并打印
    while (true)
    {
        // 2.1 如何获取pending 表?
        sigset_t pending;
        sigpending(&pending);

        // 2.2 打印
        PrintPending(pending);
        sleep(1);

        cnt++;
        if(cnt == 10)
        {
            std::cout << "解除对 2号 信号的屏蔽" << std::endl;
            sigprocmask(SIG_SETMASK, &oblock, nullptr);
        }
    }

    return 0;
}

二、信号捕捉

✨信号捕捉的流程

 🪄操作系统运行状态:

1. 用户态 --- CPU开始调度执行我自己写的代码

2. 内核态 --- 执行操作系统的代码

• 处理信号?立即处理吗? 我在做我的事情,优先级很高,信号处理,可能并不是立即处理,是在合适的时候【信号到来,没有立即处理,进程记录下来对应的信号】,即 进程从 内核态 切换回 用户态 的时候,检测当前进程的 pending && block,决定是否处理 再结合 handler表 来处理信号。

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。

由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:

• 用户程序注册了 SIGQUIT 信号的处理函数 sighandler 。

• 当前正在执行 main 函数,这时发生中断或异常切换到内核态。

• 在中断处理完毕后要返回用户态的 main 函数之前检查到有信号 SIGQUIT 递达。

• 内核决定返回用户态后不是恢复 main 函数的上下文继续执行,而是执行 sighandler 函数, sighandler 和 main 函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个 独立的控制流程。

• sighandler 函数返回后自动执行特殊的系统调用 sigreturn 再次进入内核态。

• 如果没有新的信号要递达,这次再返回用户态就是恢复 main 函数的上下文继续执行了。

 从用户态 开始调用我们的代码当需要系统调用,进入内核 执行内核处理动作 进行检测 发现信号要自定义捕捉,进入到用户态 处理完用户态的方法,再返回到内核 执行剩下的动作,紧接着返回到 用户态 的历史代码处 继续向后运行。

 🪄 执行 do_signal() 方法 为什么还要 从 内核态 切转到 用户态【void sighandler(int)】?直接内核执行完不行吗?

信号捕捉的方法是用户自定义的,即怎么处理这个方法是由用户自己写的,若让内核的权限直接执行这个方法,这个方法 若有删除用户 、删除root的配置文件、给用户赋权... 让内核执行用户的方法时 用户的代码 若有非法操作,用户就越权了,所以要切换 ---- 有安全风险

🪄 为什么要从 用户态[void sighandler(int)] 切换到 内核态[sys_sigreturn()] ? 调用完直接切换到 int main() 的下一条指令不行吗?

从一个函数 调用 另一个函数 知道函数名就可以,但是想 从 一个函数 执行完毕 返回到 另一个函数 这两个函数 必须 曾经要有调用关系,即 信号处理完,只能从内核返回。 

🪄 OS 怎么知道把信号处理完了,应该返回到用户空间的下一行代码呢?

CPU 有个寄存器【pc,正在执行指令的下一条地址】,信号处理完成之后,把PC指针恢复。

✨sigaction

#include <iostream>
#include <signal.h>
#include <unistd.h>

void handler(int signo)
{
    std::cout << "get a sig: " << signo <<std::endl;
    exit(1);
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;

    ::sigaction(2, &act, &oact);

    while(true)
    {
        pause();
    }
    
}

 

发信号是直接在 pending表 中直接修改比特位,保存信号用的是位图,

当在信号没有被递达之前,同时来了多个同样的信号,此时当前进程只能记录其中的一个信号【最新的】,

假设 我们执行handler方法非常久,我们处在 处理2号信号之间,若这时又来了一个 2号信号,此时会发生什么?重复执行 handler,会导致 handler方法不断递归,栈就会一直被叠加,造成栈溢出,

#include <iostream>
#include <signal.h>
#include <unistd.h>

void handler(int signo)
{
    static int cnt = 0;
    cnt++;
    while (true)
    {
        std::cout << "get a sig: " << signo << "cnt: " << cnt << std::endl;
        sleep(1);
    }
    exit(1);
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;

    ::sigaction(2, &act, &oact);

    while (true)
    {
        pause();
    }
}

为了规避这种现象 OS 不允许信号处理的方法进行嵌套 --- 当我们某一个信号正在被处理时,假设信号准备被递达了,内核态 返回 用户态,检查测到 2号 信号要被处理/捕捉,OS 会自动的把对应信号的block位设置为1【屏蔽2号 信号】,当信号处理完 返回内核时 会自动的把2号信号的 block 解除。

在 OS 处理进程的信号捕捉方法时,对同一种信号 OS 只允许对每一个信号的方法进行串行处理,而不支持进行嵌套处理 一次。

#include <iostream>
#include <signal.h>
#include <unistd.h>

//printBlockList
void PrintBlock()
{
    sigset_t set, oset;
    sigemptyset(&set);
    sigemptyset(&oset);

    // 读取或更改进程的信号屏蔽字
    sigprocmask(SIG_BLOCK, &set, &oset);

    std::cout << "block: ";
    for(int signo = 31; signo > 0; signo--)
    {
        if(sigismember(&oset, signo))// 判断一个信号是否在集合里
        {
            std::cout << 1;
        }
        else
        {
            std::cout << 0;
        }
    }
    std::cout << std::endl;
}

void handler(int signo)
{
    static int cnt = 0;
    cnt++;
    while (true)
    {
        std::cout << "get a sig: " << signo << ", cnt: " << cnt << std::endl;
        PrintBlock();
        sleep(1);
    }
    exit(1);
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;

    ::sigaction(2, &act, &oact);

    while (true)
    {
        PrintBlock();
        pause();
    }
}

当我们正在处理某一个信号时,会把当前正在处理的信号给屏蔽掉。

• sigset_t sa_mask: 

如果我想自定义屏蔽信号的list,就可以把屏蔽的信号加入到  sigset_t sa_mask 里。

#include <iostream>
#include <signal.h>
#include <unistd.h>

//printBlockList
void PrintBlock()
{
    sigset_t set, oset;
    sigemptyset(&set);
    sigemptyset(&oset);

    // 读取或更改进程的信号屏蔽字
    sigprocmask(SIG_BLOCK, &set, &oset);

    std::cout << "block: ";
    for(int signo = 31; signo > 0; signo--)
    {
        if(sigismember(&oset, signo))// 判断一个信号是否在集合里
        {
            std::cout << 1;
        }
        else
        {
            std::cout << 0;
        }
    }
    std::cout << std::endl;
}

void handler(int signo)
{
    static int cnt = 0;
    cnt++;
    while (true)
    {
        std::cout << "get a sig: " << signo << ", cnt: " << cnt << std::endl;
        PrintBlock();
        sleep(1);
    }
    exit(1);
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;

    sigemptyset(&act.sa_mask);

    sigaddset(&act.sa_mask, 3);
    sigaddset(&act.sa_mask, 4);
    sigaddset(&act.sa_mask, 5);
    sigaddset(&act.sa_mask, 6);
    sigaddset(&act.sa_mask, 7);

    ::sigaction(2, &act, &oact);

    while (true)
    {
        PrintBlock();
        pause();
    }
}

对相应的信号做屏蔽,处理完信号之后,设置的屏蔽号就会被自动恢复:

#include <iostream>
#include <signal.h>
#include <unistd.h>

//printBlockList
void PrintBlock()
{
    sigset_t set, oset;
    sigemptyset(&set);
    sigemptyset(&oset);

    // 读取或更改进程的信号屏蔽字
    sigprocmask(SIG_BLOCK, &set, &oset);

    std::cout << "block: ";
    for(int signo = 31; signo > 0; signo--)
    {
        if(sigismember(&oset, signo))// 判断一个信号是否在集合里
        {
            std::cout << 1;
        }
        else
        {
            std::cout << 0;
        }
    }
    std::cout << std::endl;
}

void handler(int signo)
{
    static int cnt = 0;
    cnt++;
    while (true)
    {
        std::cout << "get a sig: " << signo << ", cnt: " << cnt << std::endl;
        PrintBlock();
        sleep(1);
        break;
    }
    // exit(1);
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;

    sigemptyset(&act.sa_mask);

    sigaddset(&act.sa_mask, 3);
    sigaddset(&act.sa_mask, 4);
    sigaddset(&act.sa_mask, 5);
    sigaddset(&act.sa_mask, 6);
    sigaddset(&act.sa_mask, 7);

    // for(int signo = 1; signo <= 31; signo++)
    //     sigaddset(&act.sa_mask, signo);


    ::sigaction(2, &act, &oact);

    while (true)
    {
        PrintBlock();
        pause();
    }
}

即 对于信号的处理过程,当我们正在处理 2号 信号 时,当前 2号 信号不可被递达,因为默认被屏蔽了。

🌠当有一个信号来时,我们正在处理这个信号,此时block表 所对应的该信号的比特位为1,而 pending表 在没有处理信号之前就已经 把对应的 正在处理的信号值的比特位 给置 0 了,原因:

当处理信号完,回来时,如何区分 pending表 比特位中的 1 是 历史的 1 还是 在处理信号期间又收到的 1 呢?区分不了,所以 这个 1 是在我们调用这个信号处理函数 之前 直接被清零了。

#include <iostream>
#include <signal.h>
#include <unistd.h>

void PrintPending()
{
    sigset_t pending;
    ::sigpending(&pending);

    std::cout << "pending: ";
    for(int signo = 31; signo > 0; signo--)
    {
        if(sigismember(&pending, signo))// 判断一个信号是否在集合里
        {
            std::cout << 1;
        }
        else
        {
            std::cout << 0;
        }
    }
    std::cout << std::endl;
}

void handler(int signo)
{
    static int cnt = 0;
    cnt++;
    while (true)
    {
        std::cout << "get a sig: " << signo << ", cnt: " << cnt << std::endl;
        // PrintBlock();
        // 在信号处理期间,pending表的 2号 比特位 为 0,
        // 就说明 我们在执行 void handler(int signo) 【处理信号】之前
        // 就已经 把pending表 清零了
        PrintPending();
        sleep(1);
    }
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;

    sigemptyset(&act.sa_mask);

    ::sigaction(2, &act, &oact);

    while (true)
    {
        PrintPending();
        pause();
    }
}

✨操作系统是怎么运行的

🍔硬件中断

• OS是计算机开机之后,启动的第一个软件;

• OS启动之后,不退出,除非自己关机;

• 中断向量表就是操作系统的⼀部分,启动就加载到内存中了;
• 通过外部硬件中断,操作系统就不需要对外设进行任何周期性的检测或者轮询;
• 由外部设备触发的,中断系统运行流程,叫做硬件中断;

• 所有的 外部设备,要被对应的OS访问到【键盘、显示器、磁盘......】,并不能直接让OS定期去轮询这些设备的状态【设备太多】,

• 所以 外部设备 一般 会在硬件层面上 发起中断,发起中断后,因外设过多,所以 设备并没有在物理上 直连CPU ,而是连在了 中断控制器这里,

• 当对应的某个设备 发生 中断时,中断控制器就会知道【1. 是哪一个设备发起的中断,就可以得到对应设备的中断号;2. 中断控制器 替发起中断的设备 直接向CPU 发起 中断请求[通知CPU]--->[向CPU特定的针脚触发 ,本质就是高电压]】,

通知CPU,CPU就知道有一个硬件中断了,CPU同时也就获得 设备的中断号,【每个设备一旦中断了,都会有自己唯一的中断号,】

• OS为了能够处理每一个设备,OS在编码设计的时候,就给我们提供了一个表结构【中断向量表,这个表的下标就为 中断号】,其中 硬件上 触发中断 让CPU拿到 硬件所匹配的中断号,中断号和中断对应处理的方法是固定的,都是硬件上写好的,所以 中断这一套机制 是软硬件结合的产物。

• CPU来处理中断的时候,可能CPU当前正在调度某个进程,所以CPU里面的各种寄存器,一定保存其他的临时数据,所以CPU的中断的固定处理历程,就要把CPU的寄存器数据保存在中断的上下文里 --- CPU保护现场

• 在软件上 对应的操作系统 和 CPU 根据拿到的中断号 n,去查 中断向量表,包括 CPU保护现场 和 查表【找到对应的方法】这一系列的操作 --- 执行中断处理历程【1. 保存现场,2. 根据中断号 n,3. 调用对应的中断方法, 4. 恢复现场】。

• 执行完毕,恢复现场,处理完毕中断,继续之前的工作。

初始化中断向量表源码:

//Linux内核0.11源码 
void trap_init(void)
{
 int i;
 set_trap_gate(0,&divide_error);// 设置除操作出错的中断向量值。以下雷同。 
 set_trap_gate(1,&debug);
 set_trap_gate(2,&nmi);
 set_system_gate(3,&int3); /* int3-5 can be called from all */
 set_system_gate(4,&overflow);
 set_system_gate(5,&bounds);
 set_trap_gate(6,&invalid_op);
 set_trap_gate(7,&device_not_available);
 set_trap_gate(8,&double_fault);
 set_trap_gate(9,&coprocessor_segment_overrun);
 set_trap_gate(10,&invalid_TSS);
 set_trap_gate(11,&segment_not_present);
 set_trap_gate(12,&stack_segment);
 set_trap_gate(13,&general_protection);
 set_trap_gate(14,&page_fault);
 set_trap_gate(15,&reserved);
 set_trap_gate(16,&coprocessor_error);
// 下⾯将int17-48 的陷阱⻔先均设置为reserved,以后每个硬件初始化时会重新设置⾃⼰的陷阱
⻔。 
 for (i=17;i<48;i++)
 set_trap_gate(i,&reserved);
 set_trap_gate(45,&irq13);// 设置协处理器的陷阱⻔。 
 outb_p(inb_p(0x21)&0xfb,0x21);// 允许主8259A 芯⽚的IRQ2 中断请求。 
 outb(inb_p(0xA1)&0xdf,0xA1);// 允许从8259A 芯⽚的IRQ13 中断请求。 
 set_trap_gate(39,&parallel_interrupt);// 设置并⾏⼝的陷阱⻔。 
}

void
rs_init (void)
{
 set_intr_gate (0x24, rs1_interrupt); // 设置串⾏⼝1 的中断⻔向量(硬件IRQ4 信
号)。 
 set_intr_gate (0x23, rs2_interrupt); // 设置串⾏⼝2 的中断⻔向量(硬件IRQ3 信
号)。 
 init (tty_table[1].read_q.data); // 初始化串⾏⼝1(.data 是端⼝号)。 
 init (tty_table[2].read_q.data); // 初始化串⾏⼝2。 
 outb (inb_p (0x21) & 0xE7, 0x21); // 允许主8259A 芯⽚的IRQ3,IRQ4 中断信号请
求。 
}

🍔时钟中断

• 若外部设备没有一个就绪, 中断就不会被触发?

• 进程可以在OS的指挥下,被调度,被触发,那么OS自己被谁指挥,被谁推动执行呢?

• 外部设备可以触发硬件中断,但是这个是需要用户自己触发,有没有自己可以定期触发的设备?

• OS在计算机硬件上,利用中断的特性,在硬件上存在一个 时钟源【帮我们定期去触发时钟中断的硬件】,固定周期,持续给CPU发送中断,

• 在软件上,OS给时钟源一个固定的中断号,若时钟源一直通过中断控制器 给CPU发送硬件中断,CPU就要一直进行保护现场和查找中断向量表,执行中断方法,若给中断向量表特定的中断号为 n 的下标里,单独设置一个方法【进程调度】,外部设备 一直通过 中断 向CPU发送中断,就逼着 OS 不断的通过 中断向量表 执行中断方法,一直在进行任务调度 ---- 这就是 OS 能一直跑起来的原因【时钟中断,一直在推进OS进行调度】。

什么是操作系统?操作系统就是基于中断向量表,进行工作的!!!

当代的 x86芯片 CPU,因为觉得时钟源每次都占用中断控制器,影响运行速度,所以 已经把 时钟源 集成在CPU内部上了,所以CPU里面就会有一个 主频【每隔1s 向CPU自己发送n次硬件中断】 !这就是为什么 CPU的主频 越快 效率越高,调度的次数越频繁,CPU响应就越快。

操作系统在硬件的推动下,自动调度!!!

// Linux 内核0.11 
// main.c
sched_init(); // 调度程序初始化(加载了任务0 的tr, ldtr) (kernel/sched.c) 
// 调度程序的初始化⼦程序。 
void sched_init(void)
{
 ...
 set_intr_gate(0x20, &timer_interrupt);
 // 修改中断控制器屏蔽码,允许时钟中断。 
 outb(inb_p(0x21) & ~0x01, 0x21);
 // 设置系统调⽤中断⻔。 
 set_system_gate(0x80, &system_call);
 ...
}
// system_call.s
_timer_interrupt:
 ...
;// do_timer(CPL)执⾏任务切换、计时等⼯作,在kernel/shched.c,305 ⾏实现。 
 call _do_timer ;// 'do_timer(long CPL)' does everything from
// 调度⼊⼝ 
void do_timer(long cpl)
{
 ...
 schedule();
}
void schedule(void)
{
 ...
 switch_to(next); // 切换到任务号为next 的任务,并运⾏之。 
}

🍔死循环

OS要调度 由时钟源查中断向量表,OS要处理IO 直接外部设备准备好 发送中断 OS直接根据中断向量表的方法把数据拿出来。

如果是这样,操作系统就可以躺平了,操作系统自己不做任何事情,需要什么功能,就向中断向量表里面添加方法即可。操作系统的本质:就是一个死循环!

void main(void) /* 这⾥确实是void,并没错。 */ 
{ /* 在startup 程序(head.s)中就是这样假设的。 */ 
 ...
 /*
 * 注意!! 对于任何其它的任务,'pause()'将意味着我们必须等待收到⼀个信号才会返 
 * 回就绪运⾏态,但任务0(task0)是唯⼀的意外情况(参⻅'schedule()'),因为任 
 * 务0 在任何空闲时间⾥都会被激活(当没有其它任务在运⾏时), 
 * 因此对于任务0'pause()'仅意味着我们返回来查看是否有其它任务可以运⾏,如果没 
 * 有的话我们就回到这⾥,⼀直循环执⾏'pause()'。 
 */
 for (;;)
 pause();
} // end main

这样,操作系统,就可以在硬件时钟的推动下,自动调度了。

🪄时间片

每个进程可以分到一点时间片,当这个进程的时间片 被触发过来 

时钟中断在触发时,是固定的时间间隔,给进程设置时间片【task_struct {int count = 1000;}】,每一个时间中断到来了,当前进程在调度【CPU是当前进程的相关数据】并且被 触发了时间中断,要调度中断,进程调度不是切换,即让当前进程对应的时间片进行--, 在当前进程运行期间 只做计数器--,若 当前进程的计数器 != 0,时钟中断就什么都不做;若  当前进程的计数器 == 0 ,就会进行进程切换。时间片的本质:就是PCB内部的计数器!每一次时钟中断触发时,只做调度【判断当前进程的计数器,是否减到 0,减到 0 ,进程时间片到了 就做切换】,不一定做切换。

时钟中断,固定时间间隔,1纳秒

task_struct{  int count = 1000; }

进程调度,task_struct -> count--;

不一定是切换!
if(task_struct -> count)
     // do nothing
else  
     切换!!

🍔软中断

• 上述外部硬件中断,需要硬件设备触发;

• 有没有可能,因为软件原因,也触发上面的逻辑?有!

• 为了让操作系统支持进行系统调用,CPU也设计了对应的汇编指令(x86 32位下为 int[0x80] 或者 x64位 下 syscall),可以让CPU内部触发中断逻辑。

• 汇编指令,就可以写在软件中了,所以 就可以采用类似的软件的原因 来触发CPU执行中断方法。

 为了能让OS支持进行系统调用,任何CPU芯片,都设计了对应的内部指令集,有对应的汇编指令。

 1. 在软件上使用syscall 或 int[0x80],让CPU拿到中断号[0x80],在中断向量表里面设计一个系统调用 的入口函数【void sys_function(int index) {},直接根据系统调用表,根据系统调用的index下标,就可以进行系统调用 】,把这个函数接口的地址放入 系统调用 里面,OS要提供很多的系统调用【fork、exit、close、open....】,OS在源代码设计上,有一大堆的系统调用,这些系统调用把所有的系统调用的方法全部放在一个数组当中 形成一个系统调用表!未来任何系统调用 在操作系统层面,要调用哪个系统调用 使用该数系统调用的数组下标:系统调用号!

2. 【void sys_function(int index) {}】这个方法干什么呢?给这个方法传入一个参数,根据系统调用表,根据index下标进行系统调用【sys_call_table()】

3. 操作系统有了这个表【void sys_function(int index) {}】,当我们想调用系统调用时 需要把调用的系统调用号 交给OS 并且 在执行syscall xxx 或 int[0x80] 可以让CPU进入到陷入内核的阶段,索引到系统调用,找到系统调用 中断向量表方法的调用逻辑【void sys_function(int index) {}】, 即syscall xxx 或 int[0x80] 让我们开始进入系统调用的 固定历程,【void sys_function(int index) {}】这个方法内部 会直接根据我们传给内核的中断号来执行中断向量表当中的方法,完成系统调用。

问题:

• 用户层怎么把系统调用号给操作系统?提前把系统调用号 写入到CPU的寄存器里面。【寄存器(比如EAX)】

• 操作系统怎么把返回值给用户?-寄存器或者用户传入的缓冲区地址

系统调用的传参 包括 系统调用号 全部都会通过寄存器来传递给OS,然后通过寄存器或者用户传入的缓冲区地址 传递给用户。

• 系统调用的过程,先把我们要调用的 系统调用号 写入到寄存器, 再执行int 0x80、syscall陷入内核,本质就是触发软中断,CPU就会自动执行系统调用的处理方法,而这个方法会根据系统调用号,自动查表,执行对应的方法。

系统调用号的本质:数组下标!

系统调用,也是通过中断完成的!!!

当我们想要调用系统调用,我们可以不用系统调用的名字,直接写一段汇编代码,把系统调用号 movl 到 寄存器【EAX】里面,接着直接调用 int 0x80 就可以让OS进入系统调用,OS中断方法会自动查表,查表后 自动会调用 指定的方法,然后把结果给我。可是我们用的系统调用 怎么没见过 int 0x80 或者 syscall 呢?都是直接调用 上层的函数的呢?

Linux内核提供的系统调用接口,根本就不是C函数,而是 系统调用号 + 约定的传递参数【返回值的寄存器】 + int 0x80 / syscall 触发软中断的机制 。所以OS给我们提供了 【GNU glibc】给我们把系统调用进行了封装【C语言封装版的系统调用】!所以我们用的所有系统调用,在底层采用的是 C语言 和 汇编语言 混编构成的 系统调用。

🍔缺页中断?内存碎片处理?除零野指针错误?

缺页中断?内存碎片处理?除零野指针错误?这些问题,全部都会被转换成为CPU内部的软中断, 然后走中断处理例程,完成所有处理。有的是进行申请内存,填充页表,进行映射的。有的是用来 处理内存碎片的,有的是用来给目标进行发送信号,杀掉进程等等。

void trap_init(void)
{
 int i;
 set_trap_gate(0,&divide_error);// 设置除操作出错的中断向量值。以下雷同。 
 set_trap_gate(1,&debug);
 set_trap_gate(2,&nmi);
 set_system_gate(3,&int3); /* int3-5 can be called from all */
 set_system_gate(4,&overflow);
 set_system_gate(5,&bounds);
 set_trap_gate(6,&invalid_op);
 set_trap_gate(7,&device_not_available);
 set_trap_gate(8,&double_fault);
 set_trap_gate(9,&coprocessor_segment_overrun);
 set_trap_gate(10,&invalid_TSS);
 set_trap_gate(11,&segment_not_present);
 set_trap_gate(12,&stack_segment);
 set_trap_gate(13,&general_protection);
 set_trap_gate(14,&page_fault);
 set_trap_gate(15,&reserved);
 set_trap_gate(16,&coprocessor_error);
// 下⾯将int17-48 的陷阱⻔先均设置为reserved,以后每个硬件初始化时会重新设置⾃⼰的陷阱
⻔。 
 for (i=17;i<48;i++)
 set_trap_gate(i,&reserved);
 set_trap_gate(45,&irq13);// 设置协处理器的陷阱⻔。 
 outb_p(inb_p(0x21)&0xfb,0x21);// 允许主8259A 芯⽚的IRQ2 中断请求。 
 outb(inb_p(0xA1)&0xdf,0xA1);// 允许从8259A 芯⽚的IRQ13 中断请求。 
 set_trap_gate(39,&parallel_interrupt);// 设置并⾏⼝的陷阱⻔。 
}

• 操作系统就是躺在中断处理例程上的代码块!

• CPU内部的软中断,比如int 0x80或者syscall,我们叫做 陷阱

• CPU内部的软中断,比如除零/野指针等,我们叫做 异常。

🪄OS 是基于中断的,是死循环,躺在中断上的,外部有设备就绪 OS 就运行,外部没有设备就绪 时钟中断 就会一直推动 OS ,时钟中断 每隔固定时间 触发一次,固定时间内 OS 的 CPU 就可以工作在调度进程的这件事,当固定时间到来 OS 就会直接中断 让 OS 执行 中断向量表中的 基于中断服务:进程调度, 调度的时候 就会检测 当前进程的时间片【计数器】,计数器--,减到0,就 再从进程列表里 选择进程执行【大 O(1) 调度算法】。

🪄 系统调用 --> 查 中断向量表 --> 执行软中断【执行系统调用】 --> 根据外部传输的 中断号 直接索引 OS 内的 系统调用函数指针表 执行对应的系统调用 --> 执行完毕,返回结果。

🪄有一个函数A 调度到 函数B,为什么 B返回后 能返回到 A 的下一行代码 并且 还能把返回值拿出来?

当A 在调用 B 时,在给 B 形成栈帧结构时,A函数 会把它的下一条指令的地址 先入栈,把形参各种实例化再入栈,即 B 在调用时,头部就已经有 A 的返回值 和 下一条要执行的指令,B 执行完之后,会弹栈出来。

🪄 普通人也可以使用 int 0x80 或 syscall 进入内核里,进行系统调用,这样子的话操作系统,会不会不安全?

OS只允许使用系统调用的方式来访问OS,当传入错误的系统调用号 或 陷入内核做其他的事情 访问其他数据结构,OS是不允许的,即 系统调用本质就是可以让OS安全访问!

🪄 最早期的OS,是进程加载切换的模块,后来基于 中断处理 设计出来一个大的程序块。

三、🌟如何理解内核态和用户态

1. OS有巨大的 中断向量表,在开机的时候 从外设 直接 拷贝到 内存 当中了,包括 系统调用表【函数方法】和 各种异常处理方法 和 OS 内的各种数据结构,

2. OS --- 其中 OS 内 不管是 系统调用、各种异常处理方法、打开文件、调度进程... 本质都是 通过系统调用方法 去访问 OS 里面的各种数据结构,把数据结构的操作方法 以函数的方式 提供出来,最后把 所有函数包装成 系统调用,让外部就能以硬件或软件中断的方式去调用。

3. Linux 操作系统 让 每一个进程 都有自己的 虚拟地址,[0,4GB] 的空间,其中 [0,3GB] 为 用户区【用户想要访问自己的代码和数据不用任何系统调用,能直接进行访问】,用户区 提供了 用户页表 把代码和数据 进行 虚拟地址 到 物理地址 的 映射,有了虚拟地址 编写可执行程序时 ,可执行程序在编译器编译时 形成对应的代码 全部以 虚拟地址统一 进行编址,加载这个可执行程序时,就可以用这个程序内部的虚拟地址 来初始化地址空间 和 构建页表 ;[3,4GB] 为 内核区;

4. 内核页表,映射关系单一【OS 是 开机后 加载的第一个软件块,所以 OS 在内存当中 所占据的内存位置 往往可以固定下来】,OS 本身 整体通过 内核页表 整体映射到 内核区 [3,4GB],在内核区里 用户最关心的就是 系统调用! 【只能通过系统调用访问OS[由内核用户态决定的]】

5. 用户不关心 系统调用的物理或虚拟地址,只需关心 系统调用号,OS 内部自己会进行 索引 查找系统调用,所以用户在虚拟地址的代码区里 编译我们调用的 系统调用函数 时,只要跟 glibc 合并 链接,C语言 告诉 用户 系统调用号 是多少,用户就可以直接调用系统调用了。接着在自己的代码区 跳转至 内核区 【跳转用 软中断 int 0x80 或 syscall】,陷入内核 通过 寄存器EAX 把 中断号 给 OS  ,OS 内中断处理逻辑的代码 就会 寄存器 里读 中断号 索引 系统调用函数指针表 ,调用要用的方法,调用之前把返回值入栈, 调用完成后 把返回值 弹栈 返回到代码区。所以 我们调用任何函数(库、系统调用),都是我们自己进程的地址空间中进行调用。   

6.  不同进程的虚拟地址空间中的 [0,3GB] 用的 用户页表 全都不一样 使用的是不同的物理内存 使用的是 自己的代码和数据;不同进程的虚拟地址空间中的 [3,4GB] 全部都是使用一样的 物理内存。即 OS 无论怎么切换进程,都能找到同一个 OS !换句话说,OS 系统调用方法的执行,是在进程的地址空间中执行的!  

7. 不管是通过哪一个进程的地址空间【内核区】,进入内核,都是通过 软中断 进入 OS 的!

8. 用户态 VS 内核态

• 硬件上:【修改值】处于 用户态 或 内核态 不仅仅是由软件决定的【当前的内核页表、软件中断 都不重要】,主要是由 CPU 来决定的,CPU 里有一个 cs段寄存器【其中有两个比特位 为 CPL 当前权限级别,0:表示处于 内核态,3:表示处于 用户态】,从 内核态 切换到 用户态 ,让 CPU 修改自己的执行级别 由 用户态 3 --> 内核态 0。

• 软件上:调用 int 0x80 或 syscall。   

9. 用户态 如何进入 内核态 ?

• 时钟/外设中断

• CPU内部出现异常

• 陷进【系统调用 int 0x80 / syscall】

进程会一直 从 用户态 转到 内核态,因为CPU一直都有 时钟中断! 

• 关于特权级别,涉及到段,段描述符,段选择子,DPL,CPL,RPL等概念,而现在芯片为了保证 兼容性,已经非常复杂了,进而导致OS也必须得照顾它的复杂性。

• 用户态就是执行用户[0,3]GB时所处的状态;

• 内核态就是执行内核[3,4]GB时所处的状态;

• 区分就是按照CPU内的CPL决定,CPL的全称是Current Privilege Level,即当前特权级别。

• 一般执行 int 0x80 或者 syscall 软中断,CPL会在校验之后自动变更。

 切换流程:

1. 从用户态切换到内核态时,首先用户态可以直接读写寄存器,用户态操作CPU,将寄存器的状态 保存到对应的内存中,然后调用对应的系统函数,传入对应的用户栈地址和寄存器信息,方便后 续内核方法调用完毕后,恢复用户方法执行的现场。

2. 从用户态切换到内核态需要提权,CPU 切换指令集操作权限级别为 ring 0。

3. 提权后,切换内核栈。然后开始执行内核方法,相应的方法栈帧时保存在内核栈中。

4. 当内核方法执行完毕后,CPU切换指令集操作权限级别为 ring 3,然后利用之前写入的信息来恢 复用户栈的执行。

如若对你有帮助,记得关注、收藏、点赞哦~ 您的支持是我最大的动力🌹🌹🌹🌹!!!

若有误,望各位,在评论区留言或者私信我 指点迷津!!!谢谢 ヾ(≧▽≦*)o  \( •̀ ω •́ )/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2400604.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[ Qt ] | 与系统相关的操作(一):鼠标相关事件

目录 信号和事件的关系 (leaveEvent和enterEvent) 实现通过事件获取鼠标进入和鼠标离开 (mousePressEvent) 实现通过事件获得鼠标点击的位置 (mouseReleaseEvent) 前一个的基础上添加鼠标释放事件 (mouseDoubleClickEvent) 鼠标双击事件 鼠标移动事件 鼠标滚轮事件 …

stm32使用hal库模拟spi模式3

因为网上模拟spi模拟的都是模式0&#xff0c;很少有模式3的。 模式3的时序图&#xff0c;在clk的下降沿切换电平状态&#xff0c;在上升沿采样&#xff0c; SCK空闲为高电平 初始化cs&#xff0c;clk&#xff0c;miso&#xff0c;mosi四个io。miso配置为输入&#xff0c;cs、c…

OurBMC技术委员会2025年二季度例会顺利召开

5月28日&#xff0c;OurBMC社区技术委员会二季度例会顺利召开。本次会议采用线上线下结合的方式&#xff0c;各委员在会上听取了OurBMC社区二季度工作总结汇报&#xff0c;规划了2025年三季度的重点工作。 会上&#xff0c;技术委员会主席李煜汇报了社区2025年二季度主要工作及…

postman自动化测试

目录 一、相关知识 1.网络协议 2.接口测试 3.编写测试用例 4.系统架构 二、如何请求 1.get请求 ​编辑2.post请求 3.用环境变量请求 4.Postman测试沙箱 一、相关知识 1.网络协议 规定数据信息发送与解析的方式。 网络传输协议 https相比http&#xff0c;信息在网…

力扣热题100之二叉树的直径

题目 给你一棵二叉树的根节点&#xff0c;返回该树的 直径 。 二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。 两节点之间路径的 长度 由它们之间边数表示。 代码 方法&#xff1a;递归 计算二叉树的直径可以理解…

数字人技术的核心:AI与动作捕捉的双引擎驱动(210)

**摘要&#xff1a;**数字人技术从静态建模迈向动态交互&#xff0c;AI与动作捕捉技术的深度融合推动其智能化发展。尽管面临表情僵硬、动作脱节、交互机械等技术瓶颈&#xff0c;但通过多模态融合技术、轻量化动捕方案等创新&#xff0c;数字人正逐步实现自然交互与情感表达。…

针对KG的神经符号集成综述 两篇

帖子最后有五篇综述的总结。 综述1 24年TKDD 系统性地概述了神经符号知识图谱推理领域的进展、技术和挑战。首先介绍了知识图谱&#xff08;KGs&#xff09;和符号逻辑的基本概念&#xff0c;知识图谱被视为表示、存储和有效管理知识的关键工具&#xff0c;它将现实世界的知识…

RabbitMQ和MQTT区别与应用

RabbitMQ与MQTT深度解析&#xff1a;协议、代理、差异与应用场景 I. 引言 消息队列与物联网通信的重要性 在现代分布式系统和物联网&#xff08;IoT&#xff09;生态中&#xff0c;高效、可靠的通信机制是构建稳健、可扩展应用的核心。消息队列&#xff08;Message Queues&am…

Axure设计案例:滑动拼图解锁

设计以直观易懂的操作方式为核心&#xff0c;只需通过简单的滑动动作&#xff0c;将拼图块精准移动至指定位置&#xff0c;即可完成解锁。这种操作模式既符合用户的日常操作习惯&#xff0c;在视觉呈现上&#xff0c;我们精心设计拼图图案&#xff0c;融入生动有趣的元素&#…

MySQL权限详解

在MySQL中&#xff0c;权限管理是保障数据安全和合理使用的重要手段。MySQL提供了丰富的权限控制机制&#xff0c;允许管理员对不同用户授予不同级别的操作权限。本文将会对MySQL中的权限管理&#xff0c;以及内核如何实现权限控制进行介绍。 一、权限级别 MySQL 的权限是分层…

解决fastadmin、uniapp打包上线H5项目路由冲突问题

FastAdmin 基于 ThinkPHP&#xff0c;默认采用 URL 路由模式&#xff08;如 /index.php/module/controller/action&#xff09;&#xff0c;且前端资源通常部署在公共目录&#xff08;如 public/&#xff09;下。Uniapp 的历史模式需要将所有前端路由请求重定向到 index.html&a…

web3-区块链的交互性以及编程的角度看待智能合约

web3-区块链的交互性以及编程的角度看待智能合约 跨链交互性 交互性 用户在某一区块链生态上拥有的资产和储备 ​ 目标&#xff1a;使用户能够把资产和储备移动到另一个区块链生态上 可组合性 使在某一区块链的DAPP能调用另一个区块链上的DAPP 如果全世界都在用以太坊就…

数据结构(7)—— 二叉树(1)

目录 前言 一、 树概念及结构 1.1树的概念 1.2树的相关概念 1.3数的表示 1.二叉树表示 2.孩子兄弟表示法 3.动态数组存储 1.4树的实际应用 二、二叉树概念及结构 2.1概念 2.2特殊的二叉树 1.满二叉树 2. 完全二叉树 2.3二叉树的性质 2.4二叉树的存储结构 1.顺序存储 2.链式存储…

如何使用 Docker 部署grafana和loki收集vllm日志?

环境: Ubuntu20.04 grafana loki 3.4.1 问题描述: 如何使用 Docker 部署grafana和loki收集vllm日志? 解决方案: 1.创建一个名为 loki 的目录。将 loki 设为当前工作目录: mkdir loki cd loki2.将以下命令复制并粘贴到您的命令行中,以将 loki-local-config.yaml …

Kafka入门- 基础命令操作指南

基础命令 主题 参数含义–bootstrap-server连接的Broker主机名称以及端口号–topic操作的topic–create创建主题–delete删除主题–alter修改主题–list查看所有主题–describe查看主题的详细描述–partitions设置分区数–replication-factor设置分区副本–config更新系统默认…

目标检测我来惹1 R-CNN

目标检测算法&#xff1a; 识别图像中有哪些物体和位置 目标检测算法原理&#xff1a; 记住算法的识别流程、解决问题用到的关键技术 目标检测算法分类&#xff1a; 两阶段&#xff1a;先区域推荐ROI&#xff0c;再目标分类 region proposalCNN提取分类的目标检测框架 RC…

lua的笔记记录

类似python的eval和exec 可以伪装成其他格式的文件&#xff0c;比如.dll 希望在异常发生时&#xff0c;能够让其沉默&#xff0c;即异常捕获。而在 Lua 中实现异常捕获的话&#xff0c;需要使用函数 pcall&#xff0c;假设要执行一段 Lua 代码并捕获里面出现的所有错误&#xf…

智能进化论:AI必须跨越的四大认知鸿沟

1. 智能缺口&#xff1a;AI进化中的四大认知鸿沟 1.1 理解物理世界&#xff1a;从像素到因果的跨越 想象一个AI看着一杯倒下的水&#xff0c;它能描述“水滴形状”却无法预测“桌面会湿”。这正是当前AI的典型困境——缺乏对物理世界的因果理解。主流模型依赖海量图像或视频数…

传统足浴行业数字化转型:线上预约平台的技术架构与商业逻辑

上门按摩服务系统开发正成为行业新风口&#xff0c;这绝不是盲目跟风而是实实在在的市场趋势。随着现代人生活节奏加快&#xff0c;时间成本越来越高&#xff0c;传统到店消费模式已经无法满足消费者对便捷服务的需求。我们的团队深耕上门按摩系统开发领域五年&#xff0c;深刻…

从OCR到Document Parsing,AI时代的非结构化数据处理发生了什么改变?

智能文档处理&#xff1a;非结构化数据提出的挑战 在这个时代的每一天&#xff0c;无论是个人处理账单&#xff0c;还是企业处理合同、保险单、发票、报告或成堆的简历&#xff0c;我们都深陷在海量的非结构化数据之中。这类数据不像整齐排列的数据库表格那样规整&#xff0c;…