【Linux】进程 信号保存 信号处理 OS用户态/内核态
🌻个人主页:路飞雪吖~
🌠专栏: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 blockedstruct sigpending pending;...
}struct sighand_struct {atomic_t count;struct k_sigaction action[_NSIG]; // #define _NSIG 64spinlock_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;// 对空间进行清0sigemptyset(&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,÷_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,¶llel_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,÷_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,¶llel_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 \( •̀ ω •́ )/
相关文章:

【Linux】进程 信号保存 信号处理 OS用户态/内核态
🌻个人主页:路飞雪吖~ 🌠专栏:Linux 目录 一、信号保存 ✨进程如何完成对信号的保存? ✨在内核中的表示 ✨sigset_t ✨信号操作函数 🪄sigprocmask --- 获取或设置当前进程的 block表 🪄s…...

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

stm32使用hal库模拟spi模式3
因为网上模拟spi模拟的都是模式0,很少有模式3的。 模式3的时序图,在clk的下降沿切换电平状态,在上升沿采样, SCK空闲为高电平 初始化cs,clk,miso,mosi四个io。miso配置为输入,cs、c…...
安装 Nginx
个人博客地址:安装 Nginx | 一张假钞的真实世界 对于 Linux 平台,Nginx 安装包 可以从 nginx.org 下载。 Ubuntu: 版本Codename支持平台12.04precisex86_64, i38614.04trustyx86_64, i386, aarch64/arm6415.10wilyx86_64, i386 在 Debian/Ubuntu 系统…...
Vue-1-前端框架Vue基础入门之一
文章目录 1 Vue简介1.1 Vue的特性1.2 Vue的版本2 Vue的基础应用2.1 Vue3的下载2.2 Vue3的新语法2.3 vue-devtools调试工具3 Vue的指令3.1 内容渲染指令{{}}3.2 属性绑定指令v-bind3.3 事件绑定指令v-on3.4 双向绑定指令v-model3.5 条件渲染指令v-if3.6 列表渲染指令v-for4 参考…...

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

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

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

数字人技术的核心:AI与动作捕捉的双引擎驱动(210)
**摘要:**数字人技术从静态建模迈向动态交互,AI与动作捕捉技术的深度融合推动其智能化发展。尽管面临表情僵硬、动作脱节、交互机械等技术瓶颈,但通过多模态融合技术、轻量化动捕方案等创新,数字人正逐步实现自然交互与情感表达。…...
c++ 命名规则
目录 总结1. 类名(Class Names)2. 变量名(Variable Names)3. 函数名(Function Names)4. 宏定义(Macros)5. 命名空间(Namespaces)6. 枚举(Enums&am…...
GRU 参数梯度推导与梯度消失分析
GRU 参数梯度推导与梯度消失分析 1. GRU 前向计算回顾 GRU 单元的核心计算步骤(忽略偏置项): 更新门: z_t σ(W_z [h_{t-1}, x_t]) 重置门: r_t σ(W_r [h_{t-1}, x_t]) 候选状态: ̃h_t tanh(W_h [r_t ⊙ h_{t-1}, x_t]) 新…...

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

RabbitMQ和MQTT区别与应用
RabbitMQ与MQTT深度解析:协议、代理、差异与应用场景 I. 引言 消息队列与物联网通信的重要性 在现代分布式系统和物联网(IoT)生态中,高效、可靠的通信机制是构建稳健、可扩展应用的核心。消息队列(Message Queues&am…...
Vue跨层级通信
下面,我们来系统的梳理关于 Vue跨层级通信 的基本知识点: 一、跨层级通信核心概念 1.1 什么是跨层级通信 跨层级通信是指在组件树中,祖先组件与后代组件(非直接父子关系)之间的数据传递和交互方式。这种通信模式避免了通过中间组件层层传递 props 的繁琐过程。 1.2 适用…...
docker常见命令行用法
🧨 一、关闭和清理 Docker 服务相关命令 🔻 docker-compose down 作用:关闭并删除所有使用当前 docker-compose.yml 启动的容器、网络、挂载卷(匿名的)、和依赖关系。 通俗解释:就像你关掉了一个 App&am…...

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

MySQL权限详解
在MySQL中,权限管理是保障数据安全和合理使用的重要手段。MySQL提供了丰富的权限控制机制,允许管理员对不同用户授予不同级别的操作权限。本文将会对MySQL中的权限管理,以及内核如何实现权限控制进行介绍。 一、权限级别 MySQL 的权限是分层…...
基于BP神经网络的语音特征信号分类
基于BP神经网络的语音特征信号分类的MATLAB实现步骤: 1. 数据预处理 信号采样:读取语音信号并进行采样,确保信号具有统一的采样率。例如: [y, Fs] audioread(audio_file.wav); % 读取音频文件预加重:增强高频信号&am…...

解决fastadmin、uniapp打包上线H5项目路由冲突问题
FastAdmin 基于 ThinkPHP,默认采用 URL 路由模式(如 /index.php/module/controller/action),且前端资源通常部署在公共目录(如 public/)下。Uniapp 的历史模式需要将所有前端路由请求重定向到 index.html&a…...

web3-区块链的交互性以及编程的角度看待智能合约
web3-区块链的交互性以及编程的角度看待智能合约 跨链交互性 交互性 用户在某一区块链生态上拥有的资产和储备 目标:使用户能够把资产和储备移动到另一个区块链生态上 可组合性 使在某一区块链的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.链式存储…...
ROS1和ROS2的区别autoware.ai和autoware.universe的区别
文章目录 前言一、ROS1和ROS2的区别一、ROS2通讯实时性比ROS1强二、ROS1官方不再维护了三、ROS2的可靠性比ros1强四、ROS2的安全性比ros1强五、ROS2资源占用低六、等等等等 二、autoware.ai和autoware.universe的区别一、autoware.ai不维护了二、autoware.universe功能多&#…...

如何使用 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
目标检测算法: 识别图像中有哪些物体和位置 目标检测算法原理: 记住算法的识别流程、解决问题用到的关键技术 目标检测算法分类: 两阶段:先区域推荐ROI,再目标分类 region proposalCNN提取分类的目标检测框架 RC…...

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

智能进化论:AI必须跨越的四大认知鸿沟
1. 智能缺口:AI进化中的四大认知鸿沟 1.1 理解物理世界:从像素到因果的跨越 想象一个AI看着一杯倒下的水,它能描述“水滴形状”却无法预测“桌面会湿”。这正是当前AI的典型困境——缺乏对物理世界的因果理解。主流模型依赖海量图像或视频数…...
L2-056 被n整除的n位数 - java
L2-056 被n整除的n位数 语言时间限制内存限制代码长度限制栈限制Java (javac)400 ms512 MB16KB8192 KBPython (python3)400 ms256 MB16KB8192 KB其他编译器400 ms64 MB16KB8192 KB 题目描述: “被 n n n 整除的 n n n 位数”是这样定义的:记这个 n n…...

传统足浴行业数字化转型:线上预约平台的技术架构与商业逻辑
上门按摩服务系统开发正成为行业新风口,这绝不是盲目跟风而是实实在在的市场趋势。随着现代人生活节奏加快,时间成本越来越高,传统到店消费模式已经无法满足消费者对便捷服务的需求。我们的团队深耕上门按摩系统开发领域五年,深刻…...
Java-IO流之字节输入流详解
Java-IO流之字节输入流详解 一、Java IO体系与字节输入流概述1.1 Java IO体系结构1.2 字节输入流的核心类层次1.3 字节输入流的基本工作模式 二、InputStream类的核心方法2.1 int read()2.2 int read(byte[] b)2.3 int read(byte[] b, int off, int len)2.4 long skip(long n)2…...