1.以fork系统调用为例分析中断上下文的切换:
fork系统调用用于创建一个新进程,称为子进程,它与进程(称为系统调用fork的进程)同时运行,此进程称为父进程。创建新的子进程后,两个进程将执行fork()系统调用之后的下一条指令。子进程使用相同的pc(程序计数器),相同的CPU寄存器,在父进程中使用的相同打开文件。调用fork之后,数据、堆、栈有两份,代码仍然为一份但是这个代码段成为两个进程的共享代码段都从fork函数中返回。fork给父进程返回子进程pid,给其拷贝出来的子进程返回0,这也是他的特点之一,一次调用,两次返回,所以与一般的系统调用处理流程也必定不同。所有的子进程是在do_fork实现创建和调用的。
查看do_fork的代码:
long _do_fork(struct kernel_clone_args *args) { u64 clone_flags = args->flags; struct completion vfork; struct pid *pid; struct task_struct *p; int trace = 0; long nr; /* * Determine whether and which event to report to ptracer. When * called from kernel_thread or CLONE_UNTRACED is explicitly * requested, no event is reported; otherwise, report if the event * for the type of forking is enabled. */ if (!(clone_flags & CLONE_UNTRACED)) { if (clone_flags & CLONE_VFORK) trace = PTRACE_EVENT_VFORK; else if (args->exit_signal != SIGCHLD) trace = PTRACE_EVENT_CLONE; else trace = PTRACE_EVENT_FORK; if (likely(!ptrace_event_enabled(current, trace))) trace = 0; } p = copy_process(NULL, trace, NUMA_NO_NODE, args); add_latent_entropy(); if (IS_ERR(p)) return PTR_ERR(p); /* * Do this prior waking up the new thread - the thread pointer * might get invalid after that point, if the thread exits quickly. */ trace_sched_process_fork(current, p); pid = get_task_pid(p, PIDTYPE_PID); nr = pid_vnr(pid); if (clone_flags & CLONE_PARENT_SETTID) put_user(nr, args->parent_tid); if (clone_flags & CLONE_VFORK) { p->vfork_done = &vfork; init_completion(&vfork); get_task_struct(p); } wake_up_new_task(p); /* forking complete and child started to run, tell ptracer */ if (unlikely(trace)) ptrace_event_pid(trace, pid); if (clone_flags & CLONE_VFORK) { if (!wait_for_vfork_done(p, &vfork)) ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid); } put_pid(pid); return nr; }
其中调用copy_process函数进行进程创建
static __latent_entropy struct task_struct *copy_process( struct pid *pid, int trace, int node, struct kernel_clone_args *args) { ... p = dup_task_struct(current, node); ... /* copy all the process information */ shm_init_task(p); retval = security_task_alloc(p, clone_flags); if (retval) goto bad_fork_cleanup_audit; retval = copy_semundo(clone_flags, p); if (retval) goto bad_fork_cleanup_security; retval = copy_files(clone_flags, p); if (retval) goto bad_fork_cleanup_semundo; retval = copy_fs(clone_flags, p); if (retval) goto bad_fork_cleanup_files; retval = copy_sighand(clone_flags, p); if (retval) goto bad_fork_cleanup_fs; retval = copy_signal(clone_flags, p); if (retval) goto bad_fork_cleanup_sighand; retval = copy_mm(clone_flags, p); if (retval) goto bad_fork_cleanup_signal; retval = copy_namespaces(clone_flags, p); if (retval) goto bad_fork_cleanup_mm; retval = copy_io(clone_flags, p); if (retval) goto bad_fork_cleanup_namespaces; retval = copy_thread_tls(clone_flags, args->stack, args->stack_size, p, args->tls); if (retval) goto bad_fork_cleanup_io; ... return p; ...
进程的创建过程大致是父进程通过fork系统调用进入内核_ do_fork函数,如下图所示复制进程描述符及相关进程资源(采用写时复制技术)、分配子进程的內核堆栈并对內核堆栈和 thread等进程关键上下文进行初始化,最后将子进程放入就绪队列,fork系统调用返回;而子进程则在被调度执行时根据设置的內核堆栈和thread等进程关键上下文开始执行。
2.以execve系统调用为例分析中断上下文的切换:
execve() 系统调用的作用是运行另外一个指定的程序。它会把新程序加载到当前进程的内存空间内,当前的进程会被丢弃,它的堆、栈和所有的段数据都会被新进程相应的部分代替,然后会从新程序的初始化代码和 main 函数开始运行。同时,进程的 ID 将保持不变。
execve() 系统调用通常与 fork() 系统调用配合使用。从一个进程中启动另一个程序时,通常是先 fork() 一个子进程,然后在子进程中使用 execve() 变身为运行指定程序的进程。 例如,当用户在 Shell 下输入一条命令启动指定程序时,Shell 就是先 fork() 了自身进程,然后在子进程中使用 execve() 来运行指定的程序。
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; }
exec_binprm()函数具体如下:
static int exec_binprm(struct linux_binprm *bprm) { pid_t old_pid, old_vpid; int ret; old_pid = current->pid; rcu_read_lock(); old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent)); rcu_read_unlock(); <strong>ret = search_binary_handler(bprm);</strong> if (ret >= 0) { audit_bprm(bprm); trace_sched_process_exec(current, old_pid, bprm); ptrace_event(PTRACE_EVENT_EXEC, old_vpid); proc_exec_connector(current); } return ret; }
其中,search_binary_handler() 函数实现了核心功能,即当前正在执行的进程内存空间会被加载进来的可执行程序所覆盖,并根据具体情况指向新可执行程序。若新的可执行程序为静态链接的文件,main函数的入口地址为新进程的 IP 寄存器所指向的值;若为动态链接,IP 值为加载器 ld 的入口地址,ld 负责动态链接库的处理工作。
综上所述,通过execve系统调用执行的新进程,都会将原来的进程完全替换掉。
execve系统调用过程及其上下文的变化情况:
陷入内核--->加载新的进程--->将新的进程,完全覆盖原先进程的数据空间--->将IP值设置为新的进程的入口地址--->返回用户态,新程序继续执行下去。
老进程的上下文被完全替换,但进程的 pid 不变,所以 execve 系统调用不会返回原进程,而是返回新进程。
3.以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程
(1)正在运行的⽤户态进程X。
(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关键上下⽂压⼊进程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/rflags,从Y进程的内核堆栈中弹出(3)中对应的压栈内容。此时完 成了中断上下⽂的切换, 即从进程Y的内核态返回到进程Y的⽤户态。注意快速系统调⽤返回sysret与iret的处理略有不同。
(8)继续运行用户态进程Y。