结合中断上下文切换和进程上下文切换分析linux内核的一般执行过程
主要内容
结合中断上下文切换和进程上下文切换分析Linux内核一般执行过程
- 以fork和execve系统调用为例分析中断上下文的切换
- 分析execve系统调用中断上下文的特殊之处
- 分析fork子进程启动执行时进程上下文的特殊之处
- 以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程
fork系统调用:
fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。
#include <unistd.h> #include <iostream> using namespace std; int main () { pid_t pid1; int count = 0; pid1 = fork(); if (pid1 < 0) cout<<"fork is error"<<endl; else if (pid1 == 0) { cout<<"现在是子进程,进程号为"<<getpid()<<endl; count++; } else { cout<<"现在是父进程,进程号为"<<getpid()<<endl; count++; } cout<<"累加了"<<count<<"次"<<endl; return 0; }
查看结果:
可以发现,子进程号满足大于父进程号的关系,并且,两个进程执行的代码一模一样。
在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。进程形成了链表,父进程的fpid(p 意味point)指向子进程的进程id,因为子进程没有子进程,所以其fpid为0.
fork的系统调用实现:
父进程调用fork,因为这是一个系统调用,会导致int软中断,进入内核空间;
内核根据系统调用号,调用sys_fork系统调用,而sys_fork系统调用则是通过clone系统调用实现的,会调用clone系统调用。clone函数中会调用do_fork函数。
查看do_fork源码:
位于/linux-5.4.34/kernel/fork.c目录下
do_fork算法流程为:
-
do_fork()
——copy_process()
————dup_task_struct()
————检查是否超过最大进程数目
————初始新进程task_struct部分变量
————sched_fork()
——————__sched_fork()
——————设置新进程状态为TASK_RUNNING
——————设置新进程动态优先级为父进程普通优先级
——————加入新进程到一个调度器类,并更新调度器时钟
————copy_xxx
————为新进程分配pid
————设置进程组与父子进程关系
——wake_up_new_task()
————activate_task()
————check_preempt_curr()
再分析分析execve()
对于execuve系统调用,主要过程在do_execve_common() 函数中
static int do_execve_common(struct filename *filename,struct user_arg_ptr argv,struct user_arg_ptr envp) { struct linux_binprm *bprm; // 用于解析ELF文件的结构 struct file *file; struct files_struct *displaced; int retval; current->flags &= ~PF_NPROC_EXCEEDED; // 标记程序已被执行 retval = unshare_files(&displaced); // 拷贝当前运行进程的fd到displaced中 bprm = kzalloc(sizeof(*bprm), GFP_KERNEL); retval = prepare_bprm_creds(bprm); // 创建一个新的凭证 check_unsafe_exec(bprm); // 安全检查 current->in_execve = 1; file = do_open_exec(filename); // 打开要执行的文件 sched_exec(); bprm->file = file; bprm->filename = bprm->interp = filename->name; retval = bprm_mm_init(bprm); // 为ELF文件分配内存 bprm->argc = count(argv, MAX_ARG_STRINGS); bprm->envc = count(envp, MAX_ARG_STRINGS); retval = prepare_binprm(bprm); // 从打开的可执行文件中读取信息,填充bprm结构 // 下面的4句是将运行参数和环境变量都拷贝到bprm结构的内存空间中 retval = copy_strings_kernel(1, &bprm->filename, bprm); bprm->exec = bprm->p; retval = copy_strings(bprm->envc, envp, bprm); retval = copy_strings(bprm->argc, argv, bprm); // 开始执行加载到内存中的ELF文件 <strong>retval = exec_binprm(bprm);</strong> // 执行完毕 current->fs->in_exec = 0; current->in_execve = 0; acct_update_integrals(current); task_numa_free(current); free_bprm(bprm); putname(filename); if (displaced) put_files_struct(displaced); return retval; }
其中,search_binary_handler() 函数实现了核心功能,即当前正在执行的进程内存空间会被加载进来的可执行程序所覆盖,并根据具体情况指向新可执行程序。若新的可执行程序为静态链接的文件,main函数的入口地址为新进程的 IP 寄存器所指向的值;若为动态链接,IP 值为加载器 ld 的入口地址,ld 负责动态链接库的处理工作。
综上所述,通过execve系统调用执行的新进程,都会将原来的进程完全替换掉。
execve系统调用过程及其上下文的变化情况:
陷入内核--->加载新的进程--->将新的进程,完全覆盖原先进程的数据空间--->将IP值设置为新的进程的入口地址--->返回用户态,新程序继续执行下去。
借用《linux编程实践教程》的说法,execve就是换脑子,execve 系统调用不会返回原进程,而是返回新进程。
总结下,exe和fork的区别:
fork两次返回,第一次返回到父进程下执行,第二次子进程返回到ret_from_fork后正常返回用户态。
exe在执行时陷入了内核态,用exe中加载的程序把当前正在执行的进程覆盖掉,当系统调用返回时也就是返回到新的可执行程序起点。
linux系统的一般执行过程:
- 正在运行的用户态进程X
- 发生中断(包括异常、系统调用等),CPU完成load cs:rip(entry of a specific ISR),即跳转到中断处理程序入口。
- 中断上下文切换,具体包括如下几点:swapgs指令保存现场,可以理解CPU通过swapgs指令给当前CPU寄存器状态做了一个快照rsp point to kernel stack,加载当前进程内核堆栈栈顶地址到RSP寄存器。快速系统调用是由系统调用入口处的汇编代码实现⽤户堆栈和内核堆栈的切换save cs:rip/ss:rsp/rflags:将当前CPU关键上下文压入进程X的内核堆栈,快速系统调用是由系统调用入口处的汇编代码实现的此时完成了中断上下文切换,即从进程X的用户态到进程X的内核态
- 中断处理过程中或中断返回前调用了schedule函数,其中完成了进程调度算法选择next进程、进程地址空间切换、以及switch_to关键的进程上下文切换等
- switch_to调用了__switch_to_asm汇编代码做了关键的进程上下文切换。将当前进程X的内核堆栈切换到进程调度算法选出来的next进程(本例假定为进程Y)的内核堆栈,并完成了进程上下文所需的指令指针寄存器状态切换。之后开始运行进程Y(这里进程Y曾经通过以上步骤被切换出去,因此可以从switch_to下一行代码继续执行)
- 中断上下文恢复,与3中断上下文切换相对应。注意这里是进程Y的中断处理过程中,而3中断上下文切换是在进程X的中断处理过程中,因为内核堆栈从进程X切换到进程Y了
- 为了对应起见中断上下文恢复的最后一步单独拿出来(6的最后一步即是7)iret - pop cs:rip/ss:rsp/rflags,从Y进程的内核堆栈中弹出3中对应的压栈内容。此时完成了中断上下文的切换,即从进程Y的内核态返回到进程Y的用户态。注意快速系统调用返回sysret与iret的处理略有不同
- 继续运行用户态进程Y