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

Posted snowyaa

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了结合中断上下文切换和进程上下文切换分析Linux内核一般执行过程相关的知识,希望对你有一定的参考价值。

一、实验要求

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

  • 以fork和execve系统调用为例分析中断上下文的切换

  • 分析execve系统调用中断上下文的特殊之处

  • 分析fork子进程启动执行时进程上下文的特殊之处

  • 以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程

二、execve系统调用

 

 

 

Linux系统一般会提供了execl、execlp、execle、execv、execvp和execve等6个用以加载执行一个可执行文件的库函数,这些库函数统称为exec函数,差异在于对命令行参数和环境变量参数的传递方式不同。exec函数都是通过execve系统调用进入内核,对应的系统调用内核处理函数为sys_execve或__x64_sys_execve,它们都是通过调用do_execve来具体执行加载可执行文件的工作。

exec函数族就实现了在一个进程中启动另外一个程序的方法。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新的进程替换了。

整体的调用关系为sys_execve()或__x64_sys_execve -> do_execve() –>do_execveat_common() -> __do_execve_file -> exec_binprm()-> search_binary_handler() ->oad_elf_binary() -> start_thread()。

 

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;
}   

 

三、fork系统调用过程

 

 

 

fork系统调用在陷⼊内核态之后有两次返回:

  1. 第⼀次返回到原来的⽗进程的位置继续向下执⾏
  2. 在⼦进程中fork也返回了⼀次,会返回到ret_from_fork,再经过系统调⽤返回到⽤户态

 

 

 

在fork系统调用中,最重要的的就是_do_fork函数:do_fork函数主要完成了调⽤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;
    }
    // 拷贝父进程task_struct以及其中的一些资源,返回创建的task_struct的指针
    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);
    // 取出task结构体内的pid
    pid = get_task_pid(p, PIDTYPE_PID);
    nr = pid_vnr(pid);

    if (clone_flags & CLONE_PARENT_SETTID)
        put_user(nr, args->parent_tid);
        // 如果使用的是vfork,那么必须采用某种完成机制,确保父进程后运行
    if (clone_flags & CLONE_VFORK) {
        p->vfork_done = &vfork;
        init_completion(&vfork);
        get_task_struct(p);
    }
    // 将子进程添加到调度器的队列,使得子进程有机会获得CPU
    wake_up_new_task(p);

    /* forking complete and child started to run, tell ptracer */
    if (unlikely(trace))
        ptrace_event_pid(trace, pid);
    // 如果设置了 CLONE_VFORK 则将父进程插入等待队列,并挂起父进程直到子进程释放自己的内存空间,保证子进程优先于父进程运行
    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() 主要做了如下工作:

   a. 调用 dup_task_struct 复制一份task_struct结构体,作为子进程的进程描述符;

   b. 初始化与调度有关的数据结构,调用了sched_fork,这里将子进程的state设置为TASK_RUNNING;

   c. 复制所有的进程信息,包括fs、信号处理函数、信号、内存空间(包括写时复制)等;

        d.调用copy_thread,设置子进程的堆栈信息;  

   e. 为子进程分配一个pid。

 

四、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的内核态。

  1. 中断处理过程中或中断返回前调⽤了schedule函数,其中完成了进程调度算法选择next进程、进程地址空间切换、以及switch_to关键的进程上下⽂切换等。
  2. switch_to调⽤了__switch_to_asm汇编代码做了关键的进程上下⽂切换。将当前进程X的内核堆栈切换到进程调度算法选出来的next进程(本例假定为进程Y)的内核堆栈,并完成了进程上下⽂所需的指令指针寄存器状态切换。之后开始运⾏进程Y(这⾥进程Y曾经通过以上步骤被切换出去,因此可以从switch_to下⼀⾏代码继续执⾏)。
  3. 中断上下⽂恢复,与(3)中断上下⽂切换相对应。注意这⾥是进程Y的中断处理过程中,⽽(3)中断上下⽂切换是在进程X的中断处理过程中,因为内核堆栈从进程X 切换到进程Y了。
  4. 为了对应起⻅,中断上下⽂恢复的最后⼀步单独拿出来(6的最后⼀步即是7)iret - pop cs:rip/ss:rsp/rflags,从Y进程的内核堆栈中弹出(3)中对应的压栈内容。此时完 成了中断上下⽂的切换,即从进程Y的内核态返回到进程Y的⽤户态。注意快速系统调⽤返回sysret与iret的处理略有不同。
  5. 继续运⾏⽤户态进程Y。

 

以上是关于结合中断上下文切换和进程上下文切换分析Linux内核一般执行过程的主要内容,如果未能解决你的问题,请参考以下文章

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

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

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

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

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

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