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

实验要求

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

实验内容

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

对于一般的系统调用来说,会从用户态陷入内核态,并在内核态构建新的内核堆栈,执行相应的异常处理程序。
当异常处理程序执行完毕后,调用iret汇编指令重新返回到用户态进程空间继续执行。

由于fork会根据父进程创建一个子进程,因此会将父进程包括页表在内的所有资源拷贝给子进程。在创建完子
进城后,父进程与一般的系统调用过程一样,会返回到用户态进程空间继续执行,而子进程则会通过ret_from_fork
来返回到用户态进程空间。

下面通过do_fork代码来具体分析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;
    }
    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;
}

do_fork函数中进行了两个重要的操作,分别是拷贝父进程资源、子进程加入调度队列。

do_fork函数首先会通过调用copy_process来将父进程所拥有的资源拷贝给子进程,并创建子进程。
子进程创建成功后,会调用wake_up_new_task来将子进程加入就绪队列,等待CPU分配资源并运行。

static __latent_entropy struct task_struct *copy_process(
struct pid *pid,
int trace,
int node,
struct kernel_clone_args *args)
{
p = dup_task_struct(current, node);
/* copy all the process information */
shm_init_task(p);
…
retval = copy_thread_tls(clone_flags, args->stack, args->stack_size, p,
args->tls);
…
return p;
}

copy_process中主要复制了父进程的进程描述符task_struct并创建了相应的内核堆栈,并对
子进程进行相应的初始化。

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

当在用户态使用execve系统调用陷入到内核态时,execve在内核中会使用do_execve处理函数来加载可执行
文件,并把用该可执行文件替换掉原来的可执行文件。当使用execve加载的可执行文件执行完毕后,会返回
到用户态,此时的可执行文件已经不是原来被替换掉的旧的可执行文件了,而是新的可执行文件。execve返回的
是新的可执行程序的起点,如main函数。

在执行execve的内核处理函数__x64_sys_execve时,会通过exec_binprm()来搜索相应的可执行文件。

static int exec_binprm(struct linux_binprm *bprm)
{
    pid_t old_pid, old_vpid;
    int ret;

    /* Need to fetch pid before load_binary changes it */
    old_pid = current->pid;
    rcu_read_lock();
    old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent));
    rcu_read_unlock();

    ret = search_binary_handler(bprm);
    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;
}

在该函数中,会调用search_binary_handler()对formats链表进行逐个扫描,并尽力应用每个元素的load_binary方法,
把linux_binprm结构传递给这个函数。只要load_binary方法成功应答了文件的可执行格式,对formats的扫描即终止。

找到对应的可执行个时候,会调用load_elf_binary() 函数来加载新的可执行文件,并最后调用start_thread() 开始执行
新的可执行文件。**注意在执行完成后返回用户进程时,会将new_ip和new_sp赋值给ip和sp指针。

Linux系统的一般执行过程

  1. 正在用户空间运行进程x。
  2. 产生中断和异常,此时进入内核态。
  3. 查询IDT表,根据中断向量找到对应的中断或异常处理程序对应的门描述。
  4. 根据门描述中的段基地址和段偏移量查找GDT表,找到对应的中断或异常处理程序的入口。
  5. 保存线程,包括将eip,esp和eflags压栈,如果产生硬件出错码,则将硬件出错码入栈。
  6. 执行中断或异常处理函数。
  7. 将栈中保存的现场出栈。
  8. 调用iret汇编指令,重新装载cs和eip寄存器,返回用户态进程得堆栈空间。
posted @ 2020-06-15 09:56  寒冰陨云  阅读(166)  评论(0)    收藏  举报