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

Posted 真理长眠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程相关的知识,希望对你有一定的参考价值。

一、实验目标

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

 

二、fork系统调用

fork() 系统调用将创建一个与父进程几乎一样的新进程,之后继续执行下面的指令。程序可以根据 fork() 的返回值,确定当前处于父进程中,还是子进程中——在父进程中,返回值为新创建子进程的进程 ID,在子进程中,返回值是 0。一些使用多进程模型的服务器程序(比如 sshd),就是通过 fork() 系统调用来实现的,每当新用户接入时,系统就会专门创建一个新进程,来服务该用户。

fork() 系统调用所创建的新进程,与其父进程的内存布局和数据几乎一模一样。在内核中,它们的代码段所在的只读存储区会共享相同的物理内存页,可读可写的数据段、堆及栈等内存,内核会使用写时拷贝技术,为每个进程独立创建一份。

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

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

特殊之处:fork在陷?内核态之后有两次返回,第?次返回到原来的?进程的位置继续执?,但是在?进程中fork也返回了?次,会返回到?个特 定的点——ret_from_fork,所以它可以正常系统调?返回到?户态。

 

三、execve系统调用

execve() 系统调用的作用是运行另外一个指定的程序。它会把新程序加载到当前进程的内存空间内,当前的进程会被丢弃,它的堆、栈和所有的段数据都会被新进程相应的部分代替,然后会从新程序的初始化代码和 main 函数开始运行。同时,进程的 ID 将保持不变。

execve() 系统调用通常与 fork() 系统调用配合使用。从一个进程中启动另一个程序时,通常是先 fork() 一个子进程,然后在子进程中使用 execve() 变身为运行指定程序的进程。 例如,当用户在 Shell 下输入一条命令启动指定程序时,Shell 就是先 fork() 了自身进程,然后在子进程中使用 execve() 来运行指定的程序。

asmlinkage int sys_execve(struct pt_regs regs)
{
    int error;
    char * filename;
    // 将可执行文件的名称装入到一个新分配的页面中
    filename = getname((char __user *) regs.ebx);
    error = PTR_ERR(filename);
    if (IS_ERR(filename))
        goto out;
    // 执行可执行文件
    error = do_execve(filename,
            (char __user * __user *) regs.ecx,
            (char __user * __user *) regs.edx,
            &regs);
    if (error == 0) {
        task_lock(current);
        current->ptrace &= ~PT_DTRACE;
        task_unlock(current);
        /* Make sure we don‘t return using sysenter.. */
        set_thread_flag(TIF_IRET);
    }
    putname(filename);
    out:
    return error;
}

execve系统调用执行过程:

  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上下文环境

特殊之处:
当execve在调用时陷入内核态,就执行do_execve文件,覆盖当前的可执行程序,所以 返回的是新的可执行程序的起点。main函数位置是静态链接的可执行文件,动态链接的可执行文件需要连接动态链接库后在开始执行。

 

四、Linux系统的一般执行过程

  • 正在运行的用户态进程X
  • 发生中断——save cs:eip/esp/eflags(current) to kernel stack,then load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack).
  • SAVE_ALL //保存现场
  • 中断处理过程中或中断返回前调用了schedule(),其中的switch_to做了关键的进程上下文切换
  • 标号1之后开始运行用户态进程Y(这里Y曾经通过以上步骤被切换出去过因此可以从标号1继续执行)
  • restore_all //恢复现场
  • iret - pop cs:eip/ss:esp/eflags from kernel stack
  • 继续运行用户态进程Y

几种特殊的情况:

  1. 通过中断处理过程中的调度时机,用户态进程与内核线程之间互相切换和内核线程之间互相切换,与最一般的情况非常类似,只是内核线程运行过程中发生中断没有进程用户态和内核态的转换;

  2. 内核线程主动调用schedule(),只有进程上下文的切换,没有发生中断上下文的切换,与最一般的情况略简略;

  3. 创建子进程的系统调用在子进程中的执行起点及返回用户态,如fork;

  4. 加载一个新的可执行程序后返回到用户态的情况,如execve;

 

以上是关于结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程的主要内容,如果未能解决你的问题,请参考以下文章

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

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

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

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

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

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