结合中断上下文切换和进程上下文切换分析linux内核的一般执行过程
一、实验环境
os: linux
虚拟机:QEMU
内核版本 5.3.4
调试方法:GDB
fork系统的调用过程
fork函数的不同在于,os依照父进程的堆栈空间,复制了一份一模一样的堆栈空间给子进程,不过改变了子进程的进程号,所以子进程中也有一个fork函数,子进程从父进程fork后开始执行,子进程的fork函数会返回0 父进程的fork函数会返回子进程的进程号代表子进程创建成功
do fork系统调用的过程
_do_fork
- copy_process
复制进程描述符和执?时所需的其他数据结构
- dup_task_struct
复制进程描述符task_struct、创建内核堆栈等
- copy_thread_tls
初始化?进程内核栈和thread
- dup_task_struct
- wake_up_new_task
将?进程添加到就绪队列
系统调用返回
fork系统调用实验
编写程序,使用fork函数
#include "func.h" int g=10; int main() { pid_t pid; pid = fork(); int fd = open("file",O_RDWR); if(0==pid) { printf("I AM CHILD,mypid=%d,mydad_pid=%d\n",getpid(),getppid()); }else { printf("I AM FATHER,mypid=%d,mychild_pid = %d\n",getpid(),pid); sleep(2); return 0; } }
编译后执行结果
开启虚拟机,在__x64_sys_clone
,_do_fork
,cpoy_process
,dup_task_struct
,copy_thread_tls
下断点,shell下运行fork
可执行文件,查看此时函数栈
execv系统调用
当前的可执?程序在执?,执?到execve系统调?时陷?内核态,在内核???do_execve加载可执??件,把当前进程的可执?程序给覆盖掉
。当execve系统调?返回 时,返回的已经不是原来的那个可执?程序了,?是新的可执?程序。
exec
函数都是通过execve
系统调?进?内核,对应的系统调?内核处理函数为sys_execve
或__x64_sys_execve
,它们都是通过调?do_execve
来具体执?加载可执??件的 ?作。
整体的调?的递进关系为:
- sys_execve()或__x64_sys_execve -> // 内核处理函数
- do_execve() –> // 系统调用函数
- do_execveat_common() -> // 系统调用函数
- __do_execve_?le ->
- exec_binprm()-> // 根据读入文件头部,寻找该文件的处理函数
- search_binary_handler() ->
- load_elf_binary() -> // 加载elf文件到内存中
- start_thread() // 开始新进程
三、进程切换
进程切换时机:
1.进程时间片用完
2.进程执行过程中遇到了 中断
3.内核线程主动调?schedule函数进?进程调度
进程上下文
- ?户地址空间:包括程序代码、数据、?户堆栈等。 (
CR3
寄存器代表进程??录表,即地址空间、数据) - 控制信息:进程描述符(
thread
)、内核堆栈(sp
寄存器)等。 - 进程的CPU上下?,相关寄存器的值(指令指针寄存器
ip
代表进程的CPU上下?)
进程切换的过程
- 切换?全局?录(
CR3
)以安装?个新的地址空间,这样不同进程的虚拟地 址如0x8048400
(32位x86)就会经过不同的?表转换为不同的物理地址。 - 切换内核态堆栈和进程的CPU上下?,因为进程的CPU上下?提供了内核执 ?新进程所需要的所有信息,包含所有CPU寄存器状态。
核心代码
((last) = __switch_to_asm((prev), (next))); ENTRY(__switch_to_asm) pushq %rbp pushq %rbx pushq %r12 pushq %r13 pushq %r14 pushq %r15 /* switch stack */ movq %rsp, TASK_threadsp(%rdi) movq TASK_threadsp(%rsi), %rsp popq %r15 popq %r14 popq %r13 popq %r12 popq %rbx popq %rbp jmp __switch_to END(__switch_to)
__switch_to_asm是在C代码中调?的,也就是使?call指令,?这段汇编的结尾是jmp __switch_to, __switch_to函数是C代码最后有个return,也就是ret指令。将__switch_to_asm和__switch_to结合起来,正好是call指令和ret指令的配对出现。
call指令压栈RIP寄存器到进程切换前的prev进程内核堆栈;?ret指令出栈存?RIP 寄存器的是进程切换之后的next进程的内核堆栈栈顶数据。
由此完成了进程的切换。
中断上下文和进程上下文对比
中断上下文的切换
中断是由CPU实现的,所以中断上下?切换过程中最关键的栈顶寄存器sp
和指令指针寄存器 ip
是由CPU协助完成的。
进程上下文的切换
进程切换是由内核实现的(且一般情况下,进程上下文切换嵌套在中断中),所以进程上下?切换过程最关键的栈顶寄存器sp切换是通过进程描述符的thread.sp
实现的,指令指针 寄存器ip的切换是在内核堆栈切换的基础上巧妙利?call/ret
指令实现的。