结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程

实验目的

  • 以fork和execve系统调用为例分析中断上下文的切换
  • 分析execve系统调用中断上下文的特殊之处
  • 分析fork子进程启动执行时进程上下文的特殊之处
  • 以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程

进程上下文

  • 用户地址空间:包括程序代码、数据、用户堆栈等。
  • 控制信息:进程描述符、内核堆栈等。
  • 进程的CPU上下文,相关寄存器的值。

每个进程描述符包含一个类型为thread_struct的thread成员变量,只要进程被切换出去,内核就把其CPU上下文保存在这个结构体变量thread和内核堆栈中。 thread_struct数据结构包含部分CPU寄存器的状态,另外一些寄存器的状态存储在内核堆栈中。 

以下是进程切换的核心代码,主要包括两个方面,第一调用switch_mm切换CR3(页全局目录)来安转一个新的地址空间;第二调用switch_to切换内核堆栈和进程的CPU上下文。

context_switch(struct rq *rq, struct task_struct *prev,
           struct task_struct *next)
{
    struct mm_struct *mm, *oldmm;

    prepare_task_switch(rq, prev, next);

    mm = next->mm;
    oldmm = prev->active_mm;
    /*
     * For paravirt, this is coupled with an exit in switch_to to
     * combine the page table reload and the switch backend into
     * one hypercall.
     */
    arch_start_context_switch(prev);

    if (!mm) {
        next->active_mm = oldmm;
        atomic_inc(&oldmm->mm_count);
        enter_lazy_tlb(oldmm, next);
    } else
        switch_mm(oldmm, mm, next);

    if (!prev->mm) {
        prev->active_mm = NULL;
        rq->prev_mm = oldmm;
    }
    /*
     * Since the runqueue lock will be released by the next
     * task (which is an invalid locking op but in the case
     * of the scheduler it's an obvious special-case), so we
     * do an early lockdep release here:
     */
    spin_release(&rq->lock.dep_map, 1, _THIS_IP_);
    context_tracking_task_switch(prev, next);
    /* Here we just switch the register state and the stack. */
    switch_to(prev, next, prev);

    barrier();
    /*
     * this_rq must be evaluated again because prev may have moved
     * CPUs since it called schedule(), thus the 'rq' on its stack
     * frame will be invalid.
     */
    finish_task_switch(this_rq(), prev);
} 

 

中断上下文

以下是内核堆栈,其中蓝色部分是pt_regs的数据结构来保存中断上下文。

 

 

分析fork调用中的中断上下文

都知道fork系统调用用来创建一个子进程,此时处在内核态,内核堆栈如下,pt_regs保存中断上下文,用来返回用户态,inactive_task_frame保存进程上下文。

_do_fork调用copy_process复制了进程描述符,创建新的内核堆栈并初始化,在调用wake_up_new_task把子进程变成就绪态。在内核态执行完后因为有pt_regs(保存有中断上下文)的存在,就可以返回用户态,完成一次中断上下文的切换。

 

分析execve系统调用中断上下文

execve() 系统调用的作用是运行另外一个指定的程序。它会把新程序加载到当前进程的内存空间内,当前的进程会被丢弃,它的堆、栈和所有的段数据都会被新进程相应的部分代替,然后会从新程序的初始化代码和 main 函数开始运行。同时,进程的 ID 将保持不变。execve() 系统调用通常与 fork() 系统调用配合使用。从一个进程中启动另一个程序时,通常是先 fork() 一个子进程,然后在子进程中使用 execve() 变身为运行指定程序的进程。 例如,当用户在 Shell 下输入一条命令启动指定程序时,Shell 就是先 fork() 了自身进程,然后在子进程中使用 execve() 来运行指定的程序。

系统调用栈

  1. execve系统调用陷入内核,并传入命令行参数和shell上下文环境

  2. execve陷入内核的第一个函数:do_execve,该函数封装命令行参数和shell上下文

  3. do_execve调用do_execveat_common,后者进一步调用__do_execve_file,打开ELF文件并把所有的信息一股脑的装入linux_binprm结构体

  4. __do_execve_file中调用search_binary_handler,寻找解析ELF文件的函数

  5. search_binary_handler找到ELF文件解析函数load_elf_binary

  6. load_elf_binary解析ELF文件,把ELF文件装入内存,修改进程的用户态堆栈(主要是把命令行参数和shell上下文加入到用户态堆栈),修改进程的数据段代码段

  7. load_elf_binary调用start_thread修改进程内核堆栈(特别是内核堆栈的ip指针)

  8. 进程从execve返回到用户态后ip指向ELF文件的main函数地址,用户态堆栈中包含了命令行参数和shell上下文环境 

当前进程执行execve系统调用时陷入内核态,在内核里面用do_execve加载可执行文件,把当前进程的可执行程序给覆盖掉。当execve系统调用返回时,返回的已经不是原来的那个可执行程序了,而是新的可执行程序。execve返回的是新的可执行程序执行的起点,静态链接的可执行文件也就是main函数的大致位置,动态链接的可执行文件还需 要ld链接好动态链接库再从main函数开始执行。

Linux系统的一般执行过程

以32位x86系统结构linux-3.18.6为例,以系统调用作为特殊的中断简要总结如下。

  1. 正在运行的用户态程序X
  2. 发生中断,CPU使用该进程的内核堆栈,并将用户态堆栈寄存器保存在内核堆栈中
  3. SAVE_ALL保存现场,此时完成中断上下文的切换,即从X进程的用户态到内核态
  4. 在中断处理过程中或中断返回前发生了进程调度,将进程X的内核堆栈切换成进程Y的内核堆栈
  5. 运行进程Y
  6. RESTORE_ALL恢复进程Y的现场
  7. iret从进程Y的内核堆栈弹出用户态堆栈寄存器,完成进程Y的中断上下文切换,即从进程Y的内核态切换到用户态
  8. 继续运行进程Y

 

posted @ 2020-06-15 10:34  cyh2czj  阅读(146)  评论(0编辑  收藏  举报