一、实验目标
以fork和execve系统调用为例分析中断上下文的切换
分析execve系统调用中断上下文的特殊之处
分析fork子进程启动执行时进程上下文的特殊之处
以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程
二、实验过程
理解task_struct数据结构
进程是处于执行期的程序以及它所管理的资源(如打开的文件、挂起的信号、进程状态、地址空间等等)的总称。
在linux操作系统下,当触发任何一个事件时,系统都将它定义为一个进程,并且给予这个进程一个ID,即PID。
那么如何产生一个进程呢?简单来说就是“执行一个程序或命令”。
Linux内核通过一个被称为进程描述符的task_struct结构体来管理进程,这个结构体包含了一个进程所需的所有信息。
为了管理进程,操作系统必须对每个进程所做的事情进行清楚的描述,为此,操作系统使用数据结构来代表处理不同的实体,这个数据结构就是通常所说的进程描述符或进程控制块(PCB)。
系统调用
一.fork系统调用
在Linux内核中,一般用fork系统调用创建新进程,被创建的进程称之为子进程。linux下fork系统调用是通过_do_fork()来实现的。进程的创建过程大致是父进程通过fork系统调用进入内核_do_fork函数,复制进程描述符以及相关进程资源,为子进程分配内核堆栈,并对内核堆栈和thread等进程关键上下文进行初始化,最后将子进程放入就绪队列,fork系统调用返回。子进程则在被调度执行时根据设置的内核堆栈和thread等进程关键上下文开始执行。具体的过程如下图所示
我们知道fork,vfork和clone,do_fork,kernel_thread内核函数都可以创建一个新进程,而且都是通过_do_fork函数来创建的,只是参数有不同罢了,所以我们只需要分析_do_fork函数即可。
1 long _do_fork(struct kernel_clone_args *args) 2 { 3 u64 clone_flags = args->flags; 4 struct completion vfork; 5 struct pid *pid; 6 struct task_struct *p; 7 int trace = 0; 8 long nr; 9 10 /* 11 * Determine whether and which event to report to ptracer. When 12 * called from kernel_thread or CLONE_UNTRACED is explicitly 13 * requested, no event is reported; otherwise, report if the event 14 * for the type of forking is enabled. 15 */ 16 if (!(clone_flags & CLONE_UNTRACED)) { 17 if (clone_flags & CLONE_VFORK) 18 trace = PTRACE_EVENT_VFORK; 19 else if (args->exit_signal != SIGCHLD) 20 trace = PTRACE_EVENT_CLONE; 21 else 22 trace = PTRACE_EVENT_FORK; 23 24 if (likely(!ptrace_event_enabled(current, trace))) 25 trace = 0; 26 } 27 p = copy_process(NULL, trace, NUMA_NO_NODE, args); 28 add_latent_entropy(); 29 30 if (IS_ERR(p)) 31 return PTR_ERR(p); 32 33 /* 34 * Do this prior waking up the new thread - the thread pointer 35 * might get invalid after that point, if the thread exits quickly. 36 */ 37 trace_sched_process_fork(current, p); 38 pid = get_task_pid(p, PIDTYPE_PID); 39 nr = pid_vnr(pid); 40 41 if (clone_flags & CLONE_PARENT_SETTID) 42 put_user(nr, args->parent_tid); 43 if (clone_flags & CLONE_VFORK) { 44 p->vfork_done = &vfork; 45 init_completion(&vfork); 46 get_task_struct(p); 47 } 48 wake_up_new_task(p); 49 50 /* forking complete and child started to run, tell ptracer */ 51 if (unlikely(trace)) 52 ptrace_event_pid(trace, pid); 53 if (clone_flags & CLONE_VFORK) { 54 if (!wait_for_vfork_done(p, &vfork)) 55 ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid); 56 } 57 58 put_pid(pid); 59 return nr; 60 }
_do_fork函数主要完成了调用copy_process()复制父进程,获得pid,调用wake_up_new_task将子进程加入就绪队列等待调度执行等。copy_process()具体函数如下:
1 static __latent_entropy struct task_struct *copy_process( 2 struct pid *pid, 3 int trace, 4 int node, 5 struct kernel_clone_args *args) 6 { 7 p = dup_task_struct(current, node); 8 /* copy all the process information */ 9 shm_init_task(p); 10 … 11 retval = copy_thread_tls(clone_flags, args->stack, args->stack_size, p, 12 args->tls); 13 … 14 return p; 15 }
copy_process函数主要完成了调用dup_task_struct复制当前进程描述符task_struct,信息检查,初始化,把进程状态设置为TASK_RUNNING(此时?进程置为就绪态)、采?写时复制技术逐?复制所有其他进程资源、调?copy_thread_tls初始化?进程内核栈、设置子进程pid等。
二.execve系统调用
内核装载可执行程序的过程,实际上是执行一个系统调用的execve,和前面分析的fork系统调用的主要过程是一样的。但是execve这个系统调用还是比较特殊的。当前的可执行程序在执行,执行到execve系统调?时陷?内核态,在内核里面用do_execve加载可执行文件,把当前进程的可执?程序给覆盖掉。当execve系统调?返回时,返回的已经不是原来的那个可执?程序了,而是新的可执?程序。execve返回的是新的可执行程序执行的起点,静态链接的可执行文件也就是main函数的?致位置,动态链接的可执行文件还需要ld链接好动态链接库再从main函数开始执?。
1 static int exec_binprm(struct linux_binprm *bprm) 2 { 3 pid_t old_pid, old_vpid; 4 int ret; 5 6 /* Need to fetch pid before load_binary changes it */ 7 old_pid = current->pid; 8 rcu_read_lock(); 9 old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent)); 10 rcu_read_unlock(); 11 12 ret = search_binary_handler(bprm); 13 if (ret >= 0) { 14 audit_bprm(bprm); 15 trace_sched_process_exec(current, old_pid, bprm); 16 ptrace_event(PTRACE_EVENT_EXEC, old_vpid); 17 proc_exec_connector(current); 18 } 19 20 return ret; 21 }
以上是其中exec_binprm,它实际执行了文件,关键是调用search_binary_handler,它对formats链表进行了逐个扫描,并尽力应用每个元素的load_binary方法。找到对应的可执行文件的时候,会调用load_elf_binary()函数来加载新的可执行文件,并最后调用start_thread()开始执行。在执行完成后返回用户进程时,会将new_ip和new_sp赋值给ip和sp指针。
三.fork,execve和普通的系统调用
正常的?个系统调?都是陷?内核态,再返回到?户态,然后继续执?系统调?后的下?条指令。fork和其他系统调用不同之处是它在陷?内核态之后有两次返回,第?次返回到原来的父进程的位置继续向下执行,这和其他的系统调?是?样的。在子进程中fork也返回了?次,会返回到?个特定的点——ret_from_fork,通过内核构造的堆栈环境,它可以正常系统调?返回到用户态,所以它稍微特殊?点。
同样,execve也?较特殊。当前的可执行程序在执行,执行到execve系统调?时陷?内核态,在内核???do_execve加载可执行文件,把当前进程的可执?程序给覆盖掉。当execve系统调?返回时,返回的已经不是原来的那个可执行程序了,而是新的可执行程序。execve返回的是新的可执?程序执?的起点,静态链接的可执行文件也就是main函数的?致位置,动态链接的可执行?件还需要ld链接好动态链接库再从main函数开始执行。
四.分析Linux系统的一般执行过程
(1)正在用户空间运行进程X
(2)发生中断(包括异常、系统调用等)
(3)保存现场,此时完成了中断上下文切换,即从进程X的?户态到进程X的内核态
(4)将当前进程X的内核堆栈切换到进程调度算法选出来的next进程的内核堆栈(假定为进程Y),并完成了进程上下文所需的EIP等寄存器状态切换
(5)开始运行进程Y
(6)中断上下文恢复
(7)中断上下文的切换,即从进程Y的内核态返回到进程Y的用户态
(8)继续运行户态进程Y