结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程
实验目的:
- 以fork和execve系统调用为例分析中断上下文的切换
- 分析execve系统调用中断上下文的特殊之处
- 分析fork子进程启动执行时进程上下文的特殊之处
- 以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程
fork系统调用:
fork系统调用用于创建一个新进程,称为子进程,它与进程(称为系统调用fork的进程)同时运行,此进程称为父进程。创建新的子进程后,两个进程将执行fork()系统调用之后的下一条指令。子进程使用相同的pc(程序计数器),相同的CPU寄存器,在父进程中使用的相同打开文件。

fork、vfork和 clone这3个系统调⽤,以及do_fork和 kernel_thread内核函数都可以创建⼀个 新进程,⽽且都是通过_do_fork函数来创 建进程的,只不过传递的参数不同。
首先用户程序调用fork()发出一个软中断,由entry_SYSCALL_64 ()进入系统调用入口,它会保存用户态现场,调出内核态现场,包括在内核堆栈上保存⼀些寄存器的值,包括用户堆栈栈顶地址(SS:ESP)、当时的状态字(EFlags)、当时的 CS:EIP的值。同时会将当前进程内核堆栈的栈顶地址、内核的状态字等放⼊CPU 对应的寄存器,并且 CS:EIP 寄存器的值会指向中断处理程序的⼊⼝,
再由do_syscall_64根据系统调用号来执行相应的系统调用函数,进而调用_do_fork()函数完成相应功能。_do_fork函数主要完成了调⽤copy_process()创建子进程,再调⽤wake_up_new_task将⼦进程加⼊就绪队列等待调度执⾏等。
之后恢复用户态现场,返回用户态,继续向下执行。
⼦进程复制了⽗进程中所有的进程上下文信息,包括内核堆栈、进程描述符等,⼦进程作为⼀个独⽴的进程也会被调度。复制⽗进程的资源时采⽤了Copy OnWrite(写时复制)技术,不需要修改的进程资源⽗⼦进程是共享内存存储空间的。
_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;
}<br> //copy_process主要通过dup_task_struct()函数进行进程描述符的复制,将父进程的结构体task_struct变量的值复制给子进程<br> //通过copy_thread()函数进行子进程内核栈的初始化
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);//将已经准备好进程上下文的子进程加入就绪队列<br> //该函数对task_struct结构体中的thread_struct结构体进行了初始化。<br> //将子进程的ax值设置为0,即fork()系统调用最终给子进程的返回值<br> //设置子进程的sp值,即子进程的内核堆栈<br> //将子进程的ip值设置为ret_from_fork,即子进程返回后开始执行的地方
/* 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;
}
execve系统调用:
execve系统调用的作用是运行另外一个指定的程序。它会把新程序加载到当前进程的内存空间内,当前的进程会被丢弃,它的堆、栈和所有的段数据都会被新进程相应的部分代替,然后会从新程序的初始化代码和 main 函数开始运行。同时,进程的 ID 将保持不变。
execve系统调用通常与 fork系统调用配合使用。从一个进程中启动另一个程序时,通常是先 fork一个子进程,然后在子进程中使用 execve变身为运行指定程序的进程。 例如,当用户在 Shell 下输入一条命令启动指定程序时,Shell 就是先 fork了自身进程,然后在子进程中使用 execve来运行指定的程序。
execve系统调用的过程大概如下:
1.陷入内核;
2.加载新的可执行文件并进行可执行性检查;
3.将新的可执行文件映射到当前运行进程的进程空间中,并覆盖原来的进程数据;
4.将EIP的值设置为新的可执行程序的入口地址。
5.返回用户态,程序从新的EIP出开始继续往下执行。至此,老进程的上下文已经被新的进程完全替代了,但是进程的PID还是原来的。
execve系统调用中断上下文的特殊之处在于,新的运行进程中已经找不到原来的对execve调用的代码了,所以execve函数不会成功返回,而是实现了一次完全的变身。
系统调用的一般过程:
涉及到2个堆栈:用户态堆栈和内核态堆栈。
用户态进入内核态的中断上下文切换包括3部分:cpu硬件保存的寄存器状态+系统调用号+SAVE_ALL保存的寄存器,组成pt_regs数据结构。
内核态退出到用户态的中断上下文切换包括2部分:restore_all(还原SAVE_ALL保存的寄存器)+iret(还原cpu硬件保存的寄存器)。

一个进程的上下文可以分为三个部分:用户级上下文、寄存器上下文以及系统级上下文。
用户级上下文: 正文、数据、用户堆栈以及共享存储区;
寄存器上下文: 通用寄存器、程序寄存器(IP)、处理器状态寄存器(EFLAGS)、栈指针(ESP);
系统级上下文: 进程控制块task_struct、内存管理信息(mm_struct、vm_area_struct、pgd、pte)、内核栈。
当发生进程调度时,进行进程切换就是上下文切换(context switch)。操作系统必须对上面提到的全部信息进行切换,新调度的进程才能运行。而系统调用进行的是模式切换(mode switch)。模式切换与进程切换比较起来,容易很多,而且节省时间,因为模式切换最主要的任务只是切换进程寄存器上下文的切换。
进程上下文主要是异常处理程序和内核线程。内核之所以进入进程上下文是因为进程自身的一些工作需要在内核中做。例如,系统调用是为当前进程服务的,异常通常是处理进程导致的错误状态等。所以在进程上下文中引用current是有意义的。

浙公网安备 33010602011771号