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

1. 实验要求

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

2. 分析步骤

2.1 名词解释

2.1.1 内核态和用户态

内核态:当一个任务(进程)执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态)。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。

用户态:当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)

2.2.2 进程上下文与中断上下文

进程上下文 :进程上下文实际上是进程执行全过程的静态描述。包括

  • 上文:把已执行过的进程指令和数据在相关寄存器与堆栈中的内容称为上文
  • 中文:把正在执行的指令和数据在寄存器和堆栈中的内容称为正文
  • 下文:把待执行的指令和数据在寄存器与堆栈中的指令。

中断上下文 :硬件通过中断触发信号,导致内核调用中断处理程序,进入内核空间。在这个过程中,硬件的一些变量和参数需要传递给内核,内核通过这些参数进行中断处理。

  • 中断上文:硬件传递过来的参数和内核需要保存的一些其他环境(主要是当时被中断的进程的环境)
  • 中断下文:执行在内核空间的中断服务程序

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

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

从源码可以看到由copy_process()这个函数创建新进程

static __latent_entropy struct task_struct *copy_process(
struct pid *pid,
int trace,
int node,
struct kernel_clone_args *args)
{
//复制进程描述符task_struct、创建内核堆栈等
p = dup_task_struct(current, node);
/* copy all the process information */
shm_init_task(p);
…
// 初始化?进程内核栈和thread
retval = copy_thread_tls(clone_flags, args->stack, args->stack_size, p,
args->tls);
…
return p;//返回被创建的?进程描述符指针
}

copy_process复制了父进程的进程描述符task_struct并创建了相应的内核堆栈,同时初始化子进程

分析execve系统调用

execve作用

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

execve工作流程

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

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

总结

通过分析Linux系统的一般执行过程,对整个中断过程以及系统调用有了更加清晰的认识。也大体了解了系统调用的过程!

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

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


下一篇:Linux个人常用命令记录