一、以fork和execve系统调用为例分析中断上下文的切换
1.fork具体流程如下:
fork系统调用定义是依赖于体系结构的, 因为在用户空间和内核空间之间传递参数的方法因体系结构而异,但他们都调用体系结构无关的_do_fork(或者早期的do_fork)函数, 负责进程的复制。
_do_fork以调用copy_process开始, 后者执行生成新的进程的实际工作, 并根据指定的标志复制父进程的数据。在子进程生成后, 内核必须执行一些收尾操作,如复制进程信息,子进程加入调度器等。
copy_process流程:调用 dup_task_struct复制当前的task_struct->检查进程数限制并初始化CPU 定时器等信息->调用 sched_fork 初始化进程数据结构,并把进程状态设置为 TASK_RUNNING->复制所以进程信息并调用copy_thread_tls初始化子进程内核栈->为新进程分配设置新的pid。
long do_fork(unsigned long clone_flags, unsigned long stack_start, unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidptr) { struct task_struct *p; int trace = 0; long nr; // ... // 复制进程描述符,返回创建的task_struct的指针 p = copy_process(clone_flags, stack_start, stack_size, child_tidptr, NULL, trace); if (!IS_ERR(p)) { struct completion vfork; struct pid *pid; trace_sched_process_fork(current, p); // 取出task结构体内的pid pid = get_task_pid(p, PIDTYPE_PID); nr = pid_vnr(pid); if (clone_flags & CLONE_PARENT_SETTID) put_user(nr, parent_tidptr); // 如果使用的是vfork,那么必须采用某种完成机制,确保父进程后运行 if (clone_flags & CLONE_VFORK) { p->vfork_done = &vfork; init_completion(&vfork); get_task_struct(p); } // 将子进程添加到调度器的队列,使得子进程有机会获得CPU wake_up_new_task(p); // ... // 如果设置了 CLONE_VFORK 则将父进程插入等待队列,并挂起父进程直到子进程释放自己的内存空间 // 保证子进程优先于父进程运行 if (clone_flags & CLONE_VFORK) { if (!wait_for_vfork_done(p, &vfork)) ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid); } put_pid(pid); } else { nr = PTR_ERR(p); } return nr; }
2.execve
execve() 系统调用的作用是运行另外一个指定的程序。它会把新程序加载到当前进程的内存空间内,当前的进程会被丢弃,它的堆、栈和所有的段数据都会被新进程相应的部分代替,然后会从新程序的初始化代码和 main 函数开始运行。同时,进程的 ID 将保持不变。execve() 系统调用通常与 fork() 系统调用配合使用。从一个进程中启动另一个程序时,通常是先 fork() 一个子进程,然后在子进程中使用 execve() 变身为运行指定程序的进程。 例如,当用户在 Shell 下输入一条命令启动指定程序时,Shell 就是先 fork() 了自身进程,然后在子进程中使用 execve() 来运行指定的程序。
系统调用execve的内核入口为sys_execve
asmlinkage int sys_execve(struct pt_regs regs) { int error; char * filename; filename = getname((char *) regs.ebx); error = PTR_ERR(filename); if (IS_ERR(filename)) goto out; error = do_execve(filename, (char **) regs.ecx, (char **) regs.edx, ®s); if (error == 0) current->ptrace &= ~PT_DTRACE; putname(filename); out: return error; }
sys_execve的核心是调用do_execve函数,传给do_execve的第一个参数是已经拷贝到内核空间的路径名filename,第二个和第三个参数仍然是系统调用execve的第二个参数argv和第三个参数envp,它们代表的传给可执行文件的参数和环境变量仍然保留在用户空间中。
二、分析execve系统调用中断上下文的特殊之处
当前的可执⾏程序在执⾏,执⾏到execve系统调⽤时陷⼊内核态,在内核⾥⾯⽤do_execve加载可执⾏⽂件,把当前进程的可执⾏程序给覆盖掉。当execve系统调⽤返回时,返回的已经不是原来的那个可执⾏程序了,而是新的可执⾏程序。execve返回的是新的可执⾏程序执⾏的起点,静态链接的可执⾏⽂件也就是main函数的⼤致位置,动态链接的可执⾏⽂件还需 要ld链接好动态链接库再从main函数开始执⾏。
三、分析fork子进程启动执行时进程上下文的特殊之处
它陷入内核态之后有两次返回,第一次返回到原来的父进程的位置继续向下执行,这和一般的系统调用是一样的;在子进程中fork也返回了一次,会返回到一个特定的点——ret_from_fork,通过内核构造的堆栈环境,它可以正常系统调用返回到用户态。
四、以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过
1.正在运⾏的⽤户态进程A。
2.发⽣中断(包括异常、系统调⽤等), CPU完成load cs:rip(entry of a specific ISR),即跳转到中断处理程序⼊⼝。
3.中断上下⽂切换,具体包括如下⼏点:
swapgs指令保存现场,可以理解CPU通过swapgs指令给当前CPU寄存器状态做了⼀个快照。
rsp point to kernel stack,加载当前进程内核堆栈栈顶地址到RSP寄存器。快速系统调⽤是由系统调⽤⼊⼝处的汇编代码实现⽤户堆栈和内核堆栈的切换。
save cs:rip/ss:rsp/rflags:将当前CPU关键上下⽂压⼊进程A的内核堆栈,快速系统调⽤是由系统调⽤⼊⼝处的汇编代码实现的。
此时完成了中断上下⽂切换,即从进程A的⽤户态到进程A的内核态。
4.中断处理过程中或中断返回前调⽤了schedule函数,其中完成了进程调度算法选择next进程、进程地址空间切换、以及switch_to关键的进程上下⽂切换等。
5. switch_to调⽤了__switch_to_asm汇编代码做了关键的进程上下⽂切换。将当前进程A的内核堆栈切换到进程调度算法选出来的next进程(本例假定为进程B)的内核堆
栈,并完成了进程上下⽂所需的指令指针寄存器状态切换。之后开始运⾏进程B(这⾥进程B曾经通过以上步骤被切换出去,因此可以从switch_to下⼀⾏代码继续执⾏)。
6.中断上下⽂恢复,与(3)中断上下⽂切换相对应。注意这⾥是进程B的中断处理过程中,⽽(3)中断上下⽂切换是在进程A的中断处理过程中,因为内核堆栈从进程A
切换到进程B了。
7.为了对应起⻅中断上下⽂恢复的最后⼀步单独拿出来(6的最后⼀步即是7) iret - pop cs:rip/ss:rsp/rflags,从B进程的内核堆栈中弹出(3)中对应的压栈内容。此时完
成了中断上下⽂的切换,即从进程B的内核态返回到进程B的⽤户态。注意快速系统调⽤返回sysret与iret的处理略有不同。
8.继续运⾏⽤户态进程B