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

一、实验要求

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

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

二、实验内容

1、fork

  fork函数将运行着的程序分成2个(几乎)完全一样的进程,每个进程都启动一个从代码的同一位置开始执行的线程。这两个进程中的线程继续执行,就像是两个用户同时启动了该应用程序的两个副本。

  fork系统调用用于创建一个新进程,称为子进程,它与进程(称为系统调用fork的进程)同时运行,此进程称为父进程。创建新的子进程后,两个进程将执行fork()系统调用之后的下一条指令。子进程使用相同的pc(程序计数器),相同的CPU寄存器,在父进程中使用的相同打开文件。  
  它不需要参数并返回一个整数值。在父进程中,fork返回新创建子进程的进程ID。在子进程中,fork返回0。如果出现错误,fork返回一个负值。
  而调用fork函数,则会导致中断,调用sys_fork函数,然后sys_fork函数则会调用clone,之后会调用do_fork函数(在Linux4.2之后,新版本的系统中clone的TLS设置标识会通过TLS参数传递, 因此_do_fork替代了老版本的do_fork)
  vim linux内核中_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;
}

p=copy_process(NULL, trace, NUMA_NO_NODE, args);创建一个新的进程p,之后分配其pid,之后再将该进程加入到进程队列当中。

2、execve

在父进程中fork一个子进程,在子进程中调用exec函数启动新的程序。exec函数一共有六个,其中execve为内核级系统调用,其他(execl,execle,execlp,execv,execvp)都是调用execve的库函数。

#include<unistd.h>

函数定义 int execve(const char *filename, char *const argv[ ], char *const envp[ ]);

返回值 函数执行成功时没有返回值,执行失败时的返回值为-1.

函数说明 execve()用来执行参数filename字符串所代表的文件路径,第二个参数是利用数组指针来传递给执行文件,并且需要以空指针(NULL)结束,最后一个参数则为传递给执行文件的新环境变量数组。

通过execve系统调⽤进⼊内核,对应的系统调⽤内核处理函数为 sys_execve或__x64_sys_execve,它们都是通过调⽤do_execve来具体执⾏加载可执⾏⽂件的⼯作.整体的调⽤关系为sys_execve()或__x64_sys_execve -> do_execve() –> do_execveat_common() -> __do_execve_file -> exec_binprm()-> search_binary_handler() -> load_elf_binary() -> start_thread()。

sys_execve是调用do_execve实现的。do_execve则是调用do_execveat_common实现的,而do_execveat_common()函数如下

/*
 * sys_execve() executes a new program.
 */
static int do_execveat_common(int fd, struct filename *filename,
                          struct user_arg_ptr argv,
                          struct user_arg_ptr envp,
                          int flags)
{
    char *pathbuf = NULL;
    struct linux_binprm *bprm;  /* 这个结构当然是非常重要的,下文,列出了这个结构体以便查询各个成员变量的意义   */
    struct file *file;
    struct files_struct *displaced;
    int retval;

    if (IS_ERR(filename))
            return PTR_ERR(filename);

    /*
     * We move the actual failure in case of RLIMIT_NPROC excess from
     * set*uid() to execve() because too many poorly written programs
     * don't check setuid() return code.  Here we additionally recheck
     * whether NPROC limit is still exceeded.
     */
    if ((current->flags & PF_NPROC_EXCEEDED) &&
        atomic_read(&current_user()->processes) > rlimit(RLIMIT_NPROC)) {
            retval = -EAGAIN;
            goto out_ret;
    }

    /* We're below the limit (still or again), so we don't want to make
     * further execve() calls fail. */
    current->flags &= ~PF_NPROC_EXCEEDED;

    //  1.  调用unshare_files()为进程复制一份文件表;
    retval = unshare_files(&displaced);
    if (retval)
            goto out_ret;

    retval = -ENOMEM;

    //  2、调用kzalloc()在堆上分配一份structlinux_binprm结构体;
    bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
    if (!bprm)
            goto out_files;

    retval = prepare_bprm_creds(bprm);
    if (retval)
            goto out_free;

    check_unsafe_exec(bprm);
    current->in_execve = 1;

    //  3、调用open_exec()查找并打开二进制文件;
    file = do_open_execat(fd, filename, flags);
    retval = PTR_ERR(file);
    if (IS_ERR(file))
            goto out_unmark;

    //  4、调用sched_exec()找到最小负载的CPU,用来执行该二进制文件;
    sched_exec();

    //  5、根据获取的信息,填充structlinux_binprm结构体中的file、filename、interp成员;
    bprm->file = file;
    if (fd == AT_FDCWD || filename->name[0] == '/') {
            bprm->filename = filename->name;
    } else {
            if (filename->name[0] == '\0')
                    pathbuf = kasprintf(GFP_TEMPORARY, "/dev/fd/%d", fd);
            else
                    pathbuf = kasprintf(GFP_TEMPORARY, "/dev/fd/%d/%s",
                                        fd, filename->name);
            if (!pathbuf) {
                    retval = -ENOMEM;
                    goto out_unmark;
            }
            /*
             * Record that a name derived from an O_CLOEXEC fd will be
             * inaccessible after exec. Relies on having exclusive access to
             * current->files (due to unshare_files above).
             */
            if (close_on_exec(fd, rcu_dereference_raw(current->files->fdt)))
                    bprm->interp_flags |= BINPRM_FLAGS_PATH_INACCESSIBLE;
            bprm->filename = pathbuf;
    }
    bprm->interp = bprm->filename;

    //  6、调用bprm_mm_init()创建进程的内存地址空间,并调用init_new_context()检查当前进程是否使用自定义的局部描述符表;如果是,那么分配和准备一个新的LDT;
    retval = bprm_mm_init(bprm);
    if (retval)
            goto out_unmark;

    //  7、填充structlinux_binprm结构体中的命令行参数argv,环境变量envp
    bprm->argc = count(argv, MAX_ARG_STRINGS);
    if ((retval = bprm->argc) < 0)
            goto out;

    bprm->envc = count(envp, MAX_ARG_STRINGS);
    if ((retval = bprm->envc) < 0)
            goto out;

    //  8、调用prepare_binprm()检查该二进制文件的可执行权限;最后,kernel_read()读取二进制文件的头128字节(这些字节用于识别二进制文件的格式及其他信息,后续会使用到);
    retval = prepare_binprm(bprm);
    if (retval < 0)
            goto out;

    //  9、调用copy_strings_kernel()从内核空间获取二进制文件的路径名称;
    retval = copy_strings_kernel(1, &bprm->filename, bprm);
    if (retval < 0)
            goto out;

    bprm->exec = bprm->p;

    //  10.1、调用copy_string()从用户空间拷贝环境变量
    retval = copy_strings(bprm->envc, envp, bprm);
    if (retval < 0)
            goto out;

    //  10.2、调用copy_string()从用户空间拷贝命令行参数;
    retval = copy_strings(bprm->argc, argv, bprm);
    if (retval < 0)
            goto out;
    /*
        至此,二进制文件已经被打开,struct linux_binprm结构体中也记录了重要信息;

        下面需要识别该二进制文件的格式并最终运行该文件
    */
    retval = exec_binprm(bprm);
    if (retval < 0)
            goto out;

    /* execve succeeded */
    current->fs->in_exec = 0;
    current->in_execve = 0;
    acct_update_integrals(current);
    task_numa_free(current);
    free_bprm(bprm);
    kfree(pathbuf);
    putname(filename);
    if (displaced)
            put_files_struct(displaced);
    return retval;

out:
    if (bprm->mm) {
            acct_arg_size(bprm, 0);
            mmput(bprm->mm);
    }

out_unmark:
    current->fs->in_exec = 0;
    current->in_execve = 0;

out_free:
    free_bprm(bprm);
    kfree(pathbuf);

out_files:
    if (displaced)
            reset_files_struct(displaced);
out_ret:
    putname(filename);
    return retval;
}

  

3、总结

  fork和其他系统调⽤不同之处是它在陷⼊内核态之后有两次返回,第⼀次返回到原来的⽗进程的位 置继续向下执⾏,这和其他的系统调⽤是⼀样的。在⼦进程中fork也返回了⼀次,会返回到⼀个特 定的点——ret_from_fork,通过内核构造的堆栈环境,它可以正常系统调⽤返回到⽤户态,所以它稍微特殊⼀点。 

  execve特殊的地方在于execve实际上就是fork了一个当前进程,之后再系统调用时,对其内容进行覆盖,使其相当于一个新的进程

  而在进程运行的过程当中,发生中断(系统调用)后,CPU会跳到中断处理程序入口,之后会使用swapgs保存现场,加载当前进程内核堆栈栈顶地址到RSP寄存器,再将当前CPU关键上下⽂压⼊进程X的内核堆栈,快速系统调⽤是由系统调⽤⼊⼝处的汇编代码实现的。

  此时完成了中断上下⽂切换,即从进程X的⽤户态到进程X的内核态。 

  中断处理过程中或中断返回前调⽤了schedule函数,其中完成了进程调度算法选择next进程、进程地址空间切换、以及switch_to关键的进程上下⽂切换等。 

  switch_to调⽤了__switch_to_asm汇编代码做了关键的进程上下⽂切换。将当前进程X的内核堆栈切换到进程调度算法选出来的next进程(本例假定为进程Y)的内核堆 栈,并完成了进程上下⽂所需的指令指针寄存器状态切换。之后开始运⾏进程Y。

  中断上下⽂恢复,即再之前步骤保存的上下文环境。从Y进程的内核堆栈中弹出之前中对应的压栈内容。此时完成了中断上下⽂的切换,即从进程Y的内核态返回到进程Y的⽤户态。

 

参考资料:https://blog.csdn.net/gatieme/article/details/51594439 和https://blog.csdn.net/gatieme/article/details/51569932

posted @ 2020-06-15 22:41  us_yeyuan  阅读(182)  评论(0编辑  收藏  举报