学习内容来自庖丁解牛,仅作为个人学习研究用途,如作者认为侵权请联系第一时间删除。
中断分类
中断分为内中断(异常) 和 外中断。
内中断又分为
陷阱(陷入):程序主动产生的异常,例如系统调用、调试的int 3指令。
故障:例如除 0 错误、缺页中断等。
终止:程序无法继续运行。
外中断又分为时钟中断、I/O中断请求等。
进程切换知识点
一般来说,CPU 在任何时刻都处于以下 3 种情况之一
(1)运行于用户空间,执行用户进程上下文。
(2)运行于内核空间,处于进程(一般是内核线程)上下文。
(3)运行于内核空间,处于中断上下文。
linux内核是通过schedule函数实现进程调度的。即调用此函数一次,就是进程调度一次。
schedule 函数被调用的两种情况:
一是进程主动调用 schedule(),
二是内核会在适当的时机检测 need_resched 标记,决定是否调用 schedule()函数。
进程切换基本由两个步骤组成。
切换页全局目录(CR3)以更新到一个新的地址空间,这样不同进程的虚拟地址如会经过不同的页表转换为不同的物理地址。
切换内核态堆栈和硬件上下文。
知识点: Linux 内核中没有操作系统原理中定义的线程概念。从内核的角度看,不管是进程还是内核线程都对应一个 task_struct 数据结构,本质上都是进程。Linux 系统在用户态实现的线程库 pthread 是通过在内核中多个进程共享一个地址空间实现的。
schedule函数跟进
schedule();
->__schedule();
->context_switch(rq, prev, next);
->(1)switch_mm(oldmm, mm, next);(切换 CR3);
(2)switch_to(prev, next, prev);(硬件上下文切换)
context_switch函数
static inline void
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next)
{
struct mm_struct *mm, *oldmm;
prepare_task_switch(rq, prev, next);
mm = next->mm;
oldmm = prev->active_mm;
arch_start_context_switch(prev);
if (!mm) {
next->active_mm = oldmm;
atomic_inc(&oldmm->mm_count);
enter_lazy_tlb(oldmm, next);
} else //调用switch_mm切换地址空间
switch_mm(oldmm, mm, next);
......
//切换寄存器状态和栈
switch_to(prev, next, prev);
......
}
switch_mm函数修改了cr3寄存器的值
static inline void switch_mm(struct mm_struct *prev, struct mm_struct *next, struct task_struct *tsk)
{
...
if (!cpumask_test_and_set_cpu(cpu, mm_cpumask(next))) {
load_cr3(next->pgd); //地址空间切换
load_LDT_nolock(&next->context);
}
...
}
switch_to宏简化后的伪代码
pushfl
pushl %ebp
prev->thread.sp=%esp
%esp=next->thread.sp
prev->thread.ip=$1f
push next->thread.ip
jmp _switch_to
1f:
popl %%ebp
popfl
进程切换总结
进程x切换到进程y的过程
(1)正在运行的用户态进程 x。
(2)发生中断(包括异常、系统调用等),硬件完成以下动作。
把当前 CPU 上下文压入用户态进程 x 的内核堆栈。
加载当前进程内核堆栈相关信息,跳转到中断处理程序,即中断执行路径的起点。
(3)SAVE_ALL,保存现场,此时完成了中断上下文切换,即从进程 x 的用户态到进程 X 的内核态。
(4)中断处理过程中或中断返回前调用了 schedule 函数,其中的 switch_to 做了关键的进程上下文切换。将当前用户进程 x的内核堆栈切换到选出的 y 进程的内核堆栈,并完成了进程上下文所需的 EIP 等寄存器状态切换。
(5)eip被赋值为运行用户态进程 y的地址
(6)restore_all,恢复现场,与(3)中保存现场相对应。
(7)iret - pop cs:eip/ss:esp/eflags,从 y 进程的内核堆栈中弹出(2)中硬件完成的压栈内容。此时完成了中断上下文的切换,即从进程 Y 的内核态返回到进程 y 的用户态。
(8)继续运行用户态进程 y。
Linux 系统执行过程中的几种特殊情况
[*] 通过中断处理过程中的调度时机,内核线程之间互相切换。与最一般的情况非常类似,只是内核线程在运行过程中发生中断,没有进程用户态和内核态的转换。比如两个内核线程之间切换,CS 段寄存器没有发生变化,没有用户态与内核态的切换。
[*] 用户进程向内核线程的切换。比最一般的情况更简略,内核现场不需要从内核态返回到用户态,也就是说省略了恢复现场和 iret 恢复 CPU 上下文。
[*] 内核线程向用户进程的切换。内核线程主动调用 schedule 函数,只有进程上下文的切换,不需要发生中断和保存现场。它比最一般的情况更简略,但用户进程从内核态返回到用户态时依然需要恢复现场和 iret 恢复 CPU 上下文。
[*] 创建子进程的系统调用在子进程中的执行起点及返回用户态的过程较为特殊。如fork 一个子进程时,子进程不是从 switch_to 中的标号 1 开始执行的,而是从ret_from_fork 开始执行的,在源代码中可以找到语句“next_ip =ret_from_fork”。
[*] 加载一个新的可执行程序后返回到用户态的情况也较为特殊。比如 execve 系统调用加载新的可执行程序,在 execve 系统调用处理过程中修改了中断上下文,即在execve 系统调用内核处理函数内部修改了中断保存现场的内容,也就是返回到用户态的起点为新程序的 elf_entry 或者 ld 动态连接器的起点地址。