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

一.fork系统调用

在Linux内核中,一般用fork系统调用创建新进程,被创建的进程称之为子进程。linux下fork系统调用是通过_do_fork()来实现的。进程的创建过程大致是父进程通过fork系统调用进入内核_do_fork函数,复制进程描述符以及相关进程资源,为子进程分配内核堆栈,并对内核堆栈和thread等进程关键上下文进行初始化,最后将子进程放入就绪队列,fork系统调用返回。子进程则在被调度执行时根据设置的内核堆栈和thread等进程关键上下文开始执行。具体的过程如下图所示(这个图是老师ppt上的):

结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程_第1张图片

 

 

我们知道fork,vfork和clone,do_fork,kernel_thread内核函数都可以创建一个新进程,而且都是通过_do_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;
}

_do_fork函数主要完成了调用copy_process()复制父进程,获得pid,调用wake_up_new_task将子进程加入就绪队列等待调度执行等。copy_process()具体函数如下:

 

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函数主要完成了调用dup_task_struct复制当前进程描述符task_struct,信息检查,初始化,把进程状态设置为TASK_RUNNING(此时⼦进程置为就绪态)、采⽤写时复制技术逐⼀复制所有其他进程资源、调⽤copy_thread_tls初始化⼦进程内核栈、设置子进程pid等。

二.execve系统调用

内核装载可执行程序的过程,实际上是执行一个系统调用的execve,和前面分析的fork系统调用的主要过程是一样的。但是execve这个系统调用还是比较特殊的。当前的可执行程序在执行,执行到execve系统调⽤时陷⼊内核态,在内核里面用do_execve加载可执行文件,把当前进程的可执⾏程序给覆盖掉。当execve系统调⽤返回时,返回的已经不是原来的那个可执⾏程序了,而是新的可执⾏程序。execve返回的是新的可执行程序执行的起点,静态链接的可执行文件也就是main函数的⼤致位置,动态链接的可执行文件还需要ld链接好动态链接库再从main函数开始执⾏。

 

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

 

以上是其中exec_binprm,它实际执行了文件,关键是调用search_binary_handler,它对formats链表进行了逐个扫描,并尽力应用每个元素的load_binary方法。找到对应的可执行文件的时候,会调用load_elf_binary()函数来加载新的可执行文件,并最后调用start_thread()开始执行。在执行完成后返回用户进程时,会将new_ip和new_sp赋值给ip和sp指针。

三.fork,execve和普通的系统调用

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

同样,execve也⽐较特殊。当前的可执行程序在执行,执行到execve系统调⽤时陷⼊内核态,在内核⾥⾯⽤do_execve加载可执行文件,把当前进程的可执⾏程序给覆盖掉。当execve系统调⽤返回时,返回的已经不是原来的那个可执行程序了,而是新的可执行程序。execve返回的是新的可执⾏程序执⾏的起点,静态链接的可执行文件也就是main函数的⼤致位置,动态链接的可执行⽂件还需要ld链接好动态链接库再从main函数开始执行。

四.分析Linux系统的一般执行过程

(1)正在用户空间运行进程X

(2)发生中断(包括异常、系统调用等)

(3)保存现场,此时完成了中断上下文切换,即从进程X的⽤户态到进程X的内核态

(4)将当前进程X的内核堆栈切换到进程调度算法选出来的next进程的内核堆栈(假定为进程Y),并完成了进程上下文所需的EIP等寄存器状态切换

(5)开始运行进程Y

(6)中断上下文恢复

(7)中断上下文的切换,即从进程Y的内核态返回到进程Y的用户态

(8)继续运行户态进程Y

 

 

 

你可能感兴趣的:(结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程)