fenxinzi 2020-06-15
1.以fork和execve系统调用为例分析中断上下文的切换
2.分析execve系统调用中断上下文的特殊之处
3.分析fork子进程启动执行时进程上下文的特殊之处
4.以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程
1.进程上下文
1.1进程上文:用户态的应用程序,通过系统调用进入内核空间时,用户态的进程需要传递变量、参数的值给内核,在内核态运行时也要保存用户进程的一些寄存器值、变量等等。相对于进程而言,就是进程执行时的环境。具体来说就是各个变量和数据,包括所有的寄存器变量、进程打开的文件、内存信息等,以便再次执行该进程时,能够恢复切换时的状态,继续执行。
1.2进程下文:切换后在内核态执行的程序。
2.中断上下文
2.1中断上文:对于中断而言,硬件通过触发信号,内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理,中断上下文,其实也可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被打断执行的进程环境)。
2.2中断下文:执行在内核空间的中断服务程序。
3.用户态和内核态
3.1内核态和用户态有各自的地址空间,所能使用的处理器资源也是不一样的。当工作在用户态的进程想访问某些内核才能访问的资源时,必须通过系统调用或者中断切换到内核态,由内核代替其执行。进程上下文和中断上下文就是完成这两种状态切换所进行的操作总称。
3.2进程上下文主要是异常处理程序和内核线程。内核之所以进入进程上下文是因为进程自身的一些工作需要在内核中做。例如,系统调用是为当前进程服务的,异常通常是处理进程导致的错误状态等。
3.3中断上下文是由于硬件发生中断时会触发中断信号请求,请求系统处理中断,执行中断服务子程序。
execve系统调用的作用是运行另外一个指定的程序。它会把新程序加载到当前进程的内存空间内,当前的进程会被丢弃,它的堆、栈和所有的段数据都会被新进程相应的部分代替,然后会从新程序的初始化代码和 main 函数开始运行。同时,进程的 ID 将保持不变。
execve系统调用的过程:内核调用sys_execve() -> 进入内核态后调用do_execve()加载可执行文件–>do_execveat_common() -> __do_execve_file() -> exec_binprm()-> search_binary_handler() ->oad_elf_binary() -> start_thread()。
search_binary_handler() 函数实现了核心功能,覆盖当前进程的可执行程序。search_binary_handler() 在目录中检索需要执行的文 件,并根据文件类型来采用对应的加载函数对其进行加载。在加载的过程中,将原来进程的代码段、以及堆栈等利用所加载的文件中的对应值进行替换,最后重新设 定EIP和ESP来使可执行文件运行起来。
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; }
execve系统调用过程及其上下文的变化情况:
陷入内核--->加载新的进程--->将新的进程,完全覆盖原先进程的数据空间--->将IP值设置为新的进程的入口地址--->返回用户态,新程序继续执行下去。老进程的上下文被完全替换,但进程的 pid 不变,所以 execve 系统调用不会返回原进程,而是返回到新的可执行程序起点。
fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,根据初始参数或者传入的变量决定两个进程做的事是否相同。一个进程调用fork()函数后,系统先给新的进程分配资源,然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。在fork()函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。进程形成了链表,父进程的fpid(p 意味point)指向子进程的进程id,因为子进程没有子进程,所以其fpid为0.
fork的系统调用实现:
(1)父进程调用fork,进入内核空间;
(2)内核根据系统调用号,调用sys_fork系统调用,sys_fork再调用clone系统调用;
(3)clone函数中会调用do_fork()函数;
(4)do_fork()主要调用了两个关键函数:copy_process和wake_up_new_task。
copy_process 复制?进程产生子进程,为子进程分配一个pid;
wake_up_new_task将?进程加?就绪队列等待调度执?。
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; }
fork在陷?内核态之后有两次返回,第?次返回到原来的?进程的位置继续向下执?,这和其他的系统调?是?样的。在?进程中fork也返回了?次,会返回到?个特定的点——ret_from_fork,通过内核构造的堆栈环境,它可以正常系统调?返回到?户态。
一次系统调用的过程,其实是发生了两次 CPU 上下文切换(用户态-内核态-用户态)。
1.用户态进程X正在运行。
2.运行的过程当中,发生了中断(包括异常、系统调用等)。
3.进程X的中断上下文切换,swapgs指令保存现场,加载当前进程内核堆栈栈顶地址到RSP寄存器。将当前CPU关键上下文压入进程X的内核堆栈,从进程X的用户态到进程X的内核态。
4.中断处理过程中或中断返回前调用schedule函数,完成进程调度算法。
5.switch_to调用了__switch_to_asm汇编代码做了关键的进程上下文切换。将当前进程X的内核堆栈切换到进程调度算法选出来的next进程Y的内核堆栈,之后开始运行进程Y。
6.进程Y的中断上下文恢复,从进程Y的内核态返回到进程Y的用户态。
7.继续运??户态进程Y。