系统相关
首页 > 系统相关> > 结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程

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

作者:互联网

中断上下文和进程上下文切换简介

我们熟知的CPU上下文切换可以分为以下三种

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

fork作用

  fork系统调用可以建立一个新进程,把当前的进程分为父进程和子进程。新创建的子进程与父进程十分类似,子进程得到与父进程用户级虚拟地址空间相同(但是独立)的一份拷贝,包括文本,数据和bss段、堆以及用户栈。子进程还获得与父进程任何打开文件描述符相同的拷贝。这就是意味着当父进程调用fork时候,子进程还可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大区别在于他们有着不同的PID。

  进程的创建过程大致是父进程通过fork系统调用进入内核_ do_fork函数,如下图所示复制进程描述符及相关进程资源(采用写时复制技术)、分配子进程的內核堆栈并对內核堆栈和 thread等进程关键上下文进行初始化,最后将子进程放入就绪队列,fork系统调用返回;而子进程则在被调度执行时根据设置的內核堆栈和thread等进程关键上下文开始执行。具体相关函数分析如下所示。

相关方法分析

对于do_fork得实现如下:

long do_fork(unsigned long clone_flags,
          unsigned long stack_start,
          unsigned long stack_size,
          int __user *parent_tidptr,
          int __user *child_tidptr)
{
    struct task_struct *p;
    int trace = 0;
    long nr;

    // ...

    // 复制进程描述符,返回创建的task_struct的指针
    p = copy_process(clone_flags, stack_start, stack_size,
             child_tidptr, NULL, trace);

    if (!IS_ERR(p)) {
        struct completion vfork;
        struct pid *pid;

        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, parent_tidptr);

        // 如果使用的是vfork,那么必须采用某种完成机制,确保父进程后运行
        if (clone_flags & CLONE_VFORK) {
            p->vfork_done = &vfork;
            init_completion(&vfork);
            get_task_struct(p);
        }

        // 将子进程添加到调度器的队列,使得子进程有机会获得CPU
        wake_up_new_task(p);

        // ...

        // 如果设置了 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);
    } else {
        nr = PTR_ERR(p);
    }
    return nr;
}

copy_process()方法是负责子进程是通过复制父进程创建的,它的执行流程如下:

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

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

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

(4)调用copy_thread_tls,设置子进程的堆栈信息;  

(5)为子进程分配一个pid。

相关代码如下所示:

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 = security_task_alloc(p, clone_flags);
    if (retval)
        goto bad_fork_cleanup_audit;
    retval = copy_semundo(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_security;
    retval = copy_files(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_semundo;
    retval = copy_fs(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_files;
    retval = copy_sighand(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_fs;
    retval = copy_signal(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_sighand;
    retval = copy_mm(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_signal;
    retval = copy_namespaces(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_mm;
    retval = copy_io(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_namespaces;
    retval = copy_thread_tls(clone_flags, args->stack, args->stack_size, p,
                 args->tls);  
    if (retval)
        goto bad_fork_cleanup_io;
    ...
    return p;
    ...

特殊之处

正常的⼀个系统调⽤都是陷⼊内核态,再返回到⽤户态,然后继续执⾏系统调⽤后的下⼀条指令。

fork和其他系统调⽤不同之处是它在陷⼊内核态之后有两次返回,第⼀次返回到原来的⽗进程的位置继续向下执⾏,这和其他的系统调⽤是⼀样的。

在⼦进程中fork也返回了⼀次,会返回到⼀个特 定的点——ret_from_fork,通过内核构造的堆栈环境,它可以正常系统调⽤返回到⽤户态。

 

execve系统调用

系统调用栈

__x64_sys_execve
-> do_execve()
–> do_execveat_common()
-> __do_execve_file
-> exec_binprm()
-> search_binary_handler()
-> load_elf_binary()
-> start_thread()
  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上下文环境 

代码分析

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

int do_execve(char * filename, char __user *__user *argv,
        char __user *__user *envp,     struct pt_regs * regs)
{
    struct linux_binprm *bprm;        // 保存和要执行的文件相关的数据
    struct file *file;
    int retval;
    int i;
    retval = -ENOMEM;
    bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
    if (!bprm)
        goto out_ret;
    // 打开要执行的文件,并检查其有效性(这里的检查并不完备)
    file = open_exec(filename);
    retval = PTR_ERR(file);
    if (IS_ERR(file))
        goto out_kfree;
    // 在多处理器系统中才执行,用以分配负载最低的CPU来执行新程序
    // 该函数在include/linux/sched.h文件中被定义如下:
    // #ifdef CONFIG_SMP
    // extern void sched_exec(void);
    // #else
    // #define sched_exec() {}
    // #endif
    sched_exec();
    // 填充linux_binprm结构
    bprm->p = PAGE_SIZE*MAX_ARG_PAGES-sizeof(void *);
    bprm->file = file;
    bprm->filename = filename;
    bprm->interp = filename;
    bprm->mm = mm_alloc();
    retval = -ENOMEM;
    if (!bprm->mm)
        goto out_file;
    // 检查当前进程是否在使用LDT,如果是则给新进程分配一个LDT
    retval = init_new_context(current, bprm->mm);
    if (retval  0)
        goto out_mm;
    // 继续填充linux_binprm结构
    bprm->argc = count(argv, bprm->p / sizeof(void *));
    if ((retval = bprm->argc)  0)
        goto out_mm;
    bprm->envc = count(envp, bprm->p / sizeof(void *));
    if ((retval = bprm->envc)  0)
        goto out_mm;
    retval = security_bprm_alloc(bprm);
    if (retval)
        goto out;
    // 检查文件是否可以被执行,填充linux_binprm结构中的e_uid和e_gid项
    // 使用可执行文件的前128个字节来填充linux_binprm结构中的buf项
    retval = prepare_binprm(bprm);
    if (retval  0)
        goto out;
    // 将文件名、环境变量和命令行参数拷贝到新分配的页面中
    retval = copy_strings_kernel(1, &bprm->filename, bprm);
    if (retval  0)
        goto out;
    bprm->exec = bprm->p;
    retval = copy_strings(bprm->envc, envp, bprm);
    if (retval  0)
        goto out;
    retval = copy_strings(bprm->argc, argv, bprm);
    if (retval  0)
        goto out;
    // 查询能够处理该可执行文件格式的处理函数,并调用相应的load_library方法进行处理
    retval = search_binary_handler(bprm,regs);
    if (retval >= 0) {
        free_arg_pages(bprm);
        // 执行成功
        security_bprm_free(bprm);
        acct_update_integrals(current);
        kfree(bprm);
        return retval;
    }
out:
    // 发生错误,返回inode,并释放资源
    for (i = 0 ; i  MAX_ARG_PAGES ; i++) {
        struct page * page = bprm->page;
        if (page)
            __free_page(page);
    }
    if (bprm->security)
        security_bprm_free(bprm);
out_mm:
    if (bprm->mm)
        mmdrop(bprm->mm);
out_file:
    if (bprm->file) {
        allow_write_access(bprm->file);
        fput(bprm->file);
    }
out_kfree:
    kfree(bprm);
out_ret:
    return retval;
}

特殊之处

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

Linux系统的一般执行过程

  1.首先我们在运行的⽤户态进程X,发生中断(包括异常、系统调用等),即跳转到中断处理程序⼊⼝。

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

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

  4.中断上下文恢复。

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

  6. 继续运行用户态进程Y。

标签:fork,goto,切换,Linux,进程,bprm,上下文,retval,内核
来源: https://www.cnblogs.com/BottleBattle/p/13124537.html