@
实验环境
OS | Linux cj-virtual-machine 5.3.0-51-generic |
---|---|
虚拟机 | QEMU |
内核版本 | 5.3.4 |
调式方法 | GDB |
PS:调试环境安装请看上一篇博客汇编级理解Linux系统调用
fork系统调用过程
和普通系统系统调用对比
正常的?个系统调?都是陷?内核态,再返回到?户态,然后继续执?系统调?后的下?条指令。
fork和其他系统调?不同之处是它在陷?内核态之后有两次返回
,第?次返回到原来的?进程的位置继续向下执?,这和其他的系统调?是?样的。
在?进程中fork也返回了?次,会返回到?个特 定的点——ret_from_fork,通过内核构造的堆栈环境,它可以正常系统调?返回到?户态
_do_fork系统调用流程概述
源码在/linux/kernel/fork.c目录下,由于代码太多,只是大概了解
long _do_fork(struct kernel_clone_args *args) {
.....
//复制进程描述符和执?时所需的其他数据结构
p = copy_process(NULL, trace, NUMA_NO_NODE, args);
......
wake_up_new_task(p);//将?进程添加到就绪队列
.......
return nr;//返回?进程pid(?进程中fork返回值为?进程的pid)
}
- _do_fork
- copy_process
复制进程描述符和执?时所需的其他数据结构
- dup_task_struct
复制进程描述符task_struct、创建内核堆栈等
- copy_thread_tls
初始化?进程内核栈和thread
- dup_task_struct
- wake_up_new_task
将?进程添加到就绪队列
- copy_process
- 系统调用返回
总的来说,进程的创建过程?致是?进程通过fork系统调?进?内核_do_fork函数,如图所示复制进程描述符及相关进程 资源(采?写时复制技术)、分配?进程的内核堆栈并对内核堆栈和thread等进程关键上下?进?初始化,最后将?进程 放?就绪队列,fork系统调?返回;??进程则在被调度执?时根据设置的内核堆栈和thread等进程关键上下?开始执?。
普通系统调用和fork子进程内核堆栈对比
fork系统调用子进程的内核堆栈和普通系统调用堆栈相比多了一个,inactive_task_frame,该结构主要用于进程切换过程。
fork系统调用实验
-
编写程序,使用fork() 函数
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main(int argc, char* argv[]) { int pid; pid = fork(); if(pid<0) { //error fprintf(stderr,"For Failed"); exit(-1); } else if(pid==0) { //child printf("this is child process \n"); } else { //parent printf("this is Parent process \n"); wait(NULL); printf("child complete \n"); } return 0; }
-
编译后执行
gcc -o fork fork.c -static
./fork
-
反汇编
objdump -S fork -o fork.s
,查看fock.s
中使用的系统调用为56号
,查/linux-5.4.34/arch/x86/entry/syscalls/syscall_64.tbl
表得到内核函数__x64_sys_clone
在/linux/kernel/fork.c
中,发现,__x64_sys_clone
是调用了内核中的_do_fork
函数。 -
开启虚拟机,在
__x64_sys_clone
,_do_fork
,cpoy_process
,dup_task_struct
,copy_thread_tls
下断点,shell下运行fork
可执行文件,查看此时函数栈 -
结果
execve系统调用
图示
和普通系统系统调用对比
当前的可执?程序在执?,执?到execve系统调?时陷?内核态,在内核???do_execve加载可执??件,把当前进程的可执?程序给覆盖掉
。当execve系统调?返回 时,返回的已经不是原来的那个可执?程序了,?是新的可执?程序。
execve返回的是新的可执?程序执?的起点,静态链接的可执??件也就是main函数的?致位置,动态链接的可执??件还需 要ld链接好动态链接库再从main函数开始执?。
Linux系统?般会提供了execl、execlp、execle、execv、execvp和execve
等6个?以加载执? ?个可执??件的库函数,这些库函数统称为exec函数,差异在于对命令?参数和环境变量参数 的传递?式不同。
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() // 开始新进程
进程切换
进程切换时机
- ?户进程上下?中主动调?特定的系统调?进?中断上下?,系统调?返回 ?户态之前进?进程调度。
- 内核线程或可中断的中断处理程序,执?过程中发?中断进?中断上下?, 在中断返回前进?进程调度。
- 内核线程主动调?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
指令实现的。
Linux系统的一般执行过程(含中断与进程切换)
一般函数调用框架
(1)正在运?的?户态进程X。
(2)发?中断(包括异常、系统调?等),CPU完成load cs:rip(entry of a speci?c ISR),即跳转到中断处理程序??。
(3)中断上下?切换,具体包括如下?点:
- swapgs指令保存现场,可以理解CPU通过swapgs指令给当前CPU寄存器状态做了?个快照。
- rsp point to kernel stack,加载当前进程内核堆栈栈顶地址到RSP寄存器。快速系统调?是由系统调???处的汇编代码实现?户堆栈和内核堆栈的切换。
- save cs:rip/ss:rsp/r?ags:将当前CPU关键上下?压?进程X的内核堆栈,快速系统调?是由系统调???处的汇编代码实现的。
此时完成了中断上下?切换,即从进程X的?户态到进程X的内核态。
(4)中断处理过程中或中断返回前调?了schedule函数,其中完成了进程调度算法选择next进程、进程地址空间切换、以及switch_to关键的进程上下?切换等。
(5)switch_to调?了__switch_to_asm汇编代码做了关键的进程上下?切换。将当前进程X的内核堆栈切换到进程调度算法选出来的next进程(本例假定为进程Y)的内核堆栈,并完成了进程上下?所需的指令指针寄存器状态切换。之后开始运?进程Y(这?进程Y曾经通过以上步骤被切换出去,因此可以从switch_to下??代码继续执?)。
(6)中断上下?恢复,与(3)中断上下?切换相对应。注意这?是进程Y的中断处理过程中,?(3)中断上下?切换是在进程X的中断处理过程中,因为内核堆栈从进程X 切换到进程Y了。
(7)为了对应起?,中断上下?恢复的最后?步单独拿出来(6的最后?步即是7)iret - pop cs:rip/ss:rsp/r?ags,从Y进程的内核堆栈中弹出(3)中对应的压栈内容。此时完 成了中断上下?的切换,即从进程Y的内核态返回到进程Y的?户态。注意快速系统调?返回sysret与iret的处理略有不同。
(8)继续运??户态进程Y。