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

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

  fork系统调用用于创建一个新进程,称为子进程,它与进行fork()调用的进程(父进程)并发运行。创建新的子进程后,两个进程都将执行fork()系统调用之后的下一条指令。子进程使用相同的PC(程序计数器),相同的CPU寄存器,相同的打开文件,这些文件在父进程中使用。
  
#include <stdio.h> 
#include <sys/types.h> 
int main() 
{ 
    fork(); 
    fork(); 
    fork(); 
    printf("hello\n"); 
    return 0; 
} 

 如下结果

hello
hello
hello
hello
hello
hello
hello
hello
因此总共有八个进程(新子进程和一个原始进程)。
接下来进行关键源码分析
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;
 
    <strong>p = copy_process(clone_flags, stack_start, stack_size,
             child_tidptr, NULL, trace);
</strong>
    if (!IS_ERR(p)) {
        struct completion vfork;
        struct pid *pid;
 
        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, parent_tidptr);
 
        if (clone_flags & CLONE_VFORK) {
            p->vfork_done = &vfork;
            init_completion(&vfork);
            get_task_struct(p);
        }
 
        <strong>wake_up_new_task(p);
</strong>
        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;
}

父进程进行fork系统调用时,fork所做工作如下:

① 为新进程分配task_struct任务结构体内存空间;

② 把父进程task_struct任务结构体复制到子进程task_struct任务结构体;

③ 为新进程在其内存上建立内核堆栈;

④ 对子进程task_struct任务结构体中部分变量进行初始化设置;

⑤ 把父进程的有关信息复制给子进程,建立共享关系;

⑥ 把子进程加入到可运行队列中;

⑦ 结束fork()函数,返回子进程ID值给父进程中栈段变量id;

⑧ 当子进程开始运行时,操作系统返回0给子进程中栈段变量id。

  通过 fork 系统调用创建的进程,基本流程为父进程将信息复制给子进程,子进程再对其中的一些关键参数做出相应修改,子进程的进程上下文是基于父进程的进程上下文而形成的。

二 分析execve系统调用中断上下文的特殊之处

execve() 系统调用的作用是运行另外一个指定的程序。它会把新程序加载到当前进程的内存空间内,当前的进程会被丢弃,它的堆、栈和所有的段数据都会被新进程相应的部分代替,然后会从新程序的初始化代码和 main 函数开始运行。同时,进程的 ID 将保持不变。
 
execve() 系统调用的函数原型为 do_execve_common,下面为源码
static int do_execve_common(struct filename *filename,struct user_arg_ptr argv,struct user_arg_ptr envp)
{
    struct linux_binprm *bprm;  // 用于解析ELF文件的结构 
    struct file *file;
    struct files_struct *displaced;
    int retval;
    current->flags &= ~PF_NPROC_EXCEEDED;  // 标记程序已被执行
    retval = unshare_files(&displaced);  // 拷贝当前运行进程的fd到displaced中
    bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
    retval = prepare_bprm_creds(bprm);   // 创建一个新的凭证
    check_unsafe_exec(bprm);             // 安全检查
    current->in_execve = 1;
    file = do_open_exec(filename);       // 打开要执行的文件
    sched_exec();
    bprm->file = file;
    bprm->filename = bprm->interp = filename->name;
    retval = bprm_mm_init(bprm);       // 为ELF文件分配内存
    bprm->argc = count(argv, MAX_ARG_STRINGS);
    bprm->envc = count(envp, MAX_ARG_STRINGS);
    retval = prepare_binprm(bprm);     // 从打开的可执行文件中读取信息,填充bprm结构
     // 下面的4句是将运行参数和环境变量都拷贝到bprm结构的内存空间中
    retval = copy_strings_kernel(1, &bprm->filename, bprm);
    bprm->exec = bprm->p;
    retval = copy_strings(bprm->envc, envp, bprm);
    retval = copy_strings(bprm->argc, argv, bprm);
    // 开始执行加载到内存中的ELF文件
    <strong>retval = exec_binprm(bprm);</strong>
    // 执行完毕
    current->fs->in_exec = 0;
    current->in_execve = 0;
    acct_update_integrals(current);
    task_numa_free(current);
    free_bprm(bprm);
    putname(filename);
    if (displaced)
        put_files_struct(displaced);
    return retval;
}

  先通过open_err()函数找到并打开可执行文件,然后要从打开的文件中将可执行文件的信息装入一个数据结构linux_binprm,do_execve先对参数和环境变量的技术,并通过prepare_binprm读入开头的128个字节到linux_binprm结构的bprm缓冲区,最后将执行的参数从用户空间拷贝到数据结构bprm中。内核中有一个formats队列,该队列的每个成员认识并只处理一种格式的可执行文件,bprm缓冲区中的128个字节中有格式信息,便要通过这个队列去辨认。do_execve()中的关键是最后执行一个search_binary_handler()函数,找到对应的执行文件格式,并返回一个值,这样程序就可以执行了。函数代码:

static int exec_binprm(struct linux_binprm *bprm)
{
    pid_t old_pid, old_vpid;   
    int ret;
    old_pid = current->pid;
    rcu_read_lock();
    old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent));
    rcu_read_unlock();
    <strong>ret = search_binary_handler(bprm);</strong>
    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;
}   

execve系统调用过程及其上下文的变化情况

  a. 陷入内核

  b. 加载新的进程

  c. 将新的进程,完全覆盖原先进程的数据空间

  d. 将 IP 值设置为新的进程的入口地址

  e. 返回用户态,新程序继续执行下去。老进程的上下文被完全替换,但进程的 pid 不变,所以 execve 系统调用不会返回原进程,而是返回新进程。

 

以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程

  中断和中断返回有中断上下文的切换, CPU和内核代码中断处理程序入口的汇编代码结合起来完成中断上下文的切换。 

  进程调度过程中有进程上下文的切换,进程上下文的切换完全由内核完成,具体包括:从一个进程的地址空间切换到另一个进程的地址空间;从一个进程的内核堆栈切换到另一个进程的内核堆栈;还有进程的CPU上下文的切换。

中断上下文和进程上下文的文个关键区别是堆栈切换的方法。中断是由CPU实现的,所以中断上下文切换过程中最关键的栈顶寄存器sp和指令指针寄存器ip是由CPU协助完成的;进程切换是由内核实现的,所以进程上下文切换过程

中最关键的栈顶寄存器sp切换是通过进程描述符的thread.sp实现的,指令指针寄存器ip的切换是在内核堆栈切换的基础上巧妙利⽤call/ret指令实现的。