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

一概述

处理器总处于以下三种状态之一:
1、内核态,运行于进程上下文,内核代表进程运行于内核空间;
2、内核态,运行于中断上下文,内核代表硬件运行于内核空间;
3、用户态,运行于用户空间。

用户空间的应用程序,通过系统调用,进入内核空间。这个时候用户空间的进程要传递 很多变量、参数的值给内核,内核态运行的时候也要保存用户进程的一些寄存 器值、变量等。所谓的“进程上下文”,可以看作是用户进程传递给内核的这些参数以及内核要保存的那一整套的变量和寄存器值和当时的环境等。

硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的 一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。所谓的“ 中断上下文”,其实也可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被打断执行的进程环境)。

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

Linux系统一般会提供了execl、execlp、execle、execv、execvp和execve等6个用以加载执行一个可执行文件的库函数,这些库函数统称为exec函数,差异在于对命令行参数和环境变量参数的传递方式不同。exec函数都是通过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() ->oad_elf_binary() -> start_thread()。

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

 其中,search_binary_handler() 函数实现了核心功能,即当前正在执行的进程内存空间会被加载进来的可执行程序所覆盖,并根据具体情况指向新可执行程序。

   a. 若新的可执行程序为静态链接的文件,main函数的入口地址为新进程的 IP 寄存器所指向的值;

   b. 若为动态链接,IP 值为加载器 ld 的入口地址,ld 负责动态链接库的处理工作。

  综上,通过 execve 系统调用执行的新进程,都会将原来的进程完全替换掉。

总结

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

  a. 陷入内核

  b. 加载新的进程

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

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

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

 三fork子进程启动执行时进程上下文的特殊之处

do_fork源码

do_fork() 主要做了如下工作:

   a. 调用了 copy_process 函数,复制当前进程产生子进程,并且传入关键参数为子进程设置响应进程上下文

   b. 调用 wake_up_new_task 函数,将子进程放入调度队列中,从而有机会 CPU 调度并得以运行。

关键函数 copy_process() 主要做了如下工作:

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

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

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

        d.调用copy_thread,设置子进程的堆栈信息;  

   e. 为子进程分配一个pid。

关键函数 copy_thread() 主要做了如下工作:

   a. 对子进程的thread.sp赋值,即子进程 esp 寄存器的值;

   b. 将父进程的寄存器信息复制给子进程;

   c. 将子进程的eax寄存器值设置为0,所以fork调用在子进程中的返回值为0;

   d. 子进程从ret_from_fork开始执行,所以它的地址赋给thread.ip,也就是将来的eip寄存器

四Linux系统的一般执行过程

(1)正在运行的用户态进程X


(2)发生中断(包括异常、系统调用等),CPU完成load cs:rip(entry of a specific ISR),即跳转到中断处理程序入口。


(3)中断上下文切换,具体包括如下几点:
    swapgs指令保存现场,可以理解CPU通过swapgs指令给当前CPU寄存器状态做了一个快照。
    rsp point to kernel stack,加载当前进程内核堆栈栈顶地址到RSP寄存器。快速系统调用是由系统调用入口处的汇编代码实现用户堆栈和内核堆栈的切换。
    save cs:rip/ss:rsp/rflags:将当前CPU关键上下文压入进程X的内核堆栈,快速系统调用是由系统调用入口处的汇编代码实现的。
 此时完成了中断上下文切换,即从进程X的用户态到进程X的内核态。


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


(5)switch_to调用了__switch_to_asm汇编代码做了关键的进程上下文切换。将当前进程X的内核堆栈切换到进程调度算法选出来的next进程(本例假定为进程Y)的内核堆
栈,并完成了进程上下文所需的指令指针寄存器状态切换。之后开始运行进程Y(这里进程Y曾经通过以上步骤被切换出去,因此可以从switch_to下一行代码继续执行)。


(6)中断上下文恢复,与(3)中断上下文切换相对应。注意这里是进程Y的中断处理过程中,而(3)中断上下文切换是在进程X的中断处理过程中,因为内核堆栈从进程X
切换到进程Y了。


(7)为了对应起⻅中断上下文恢复的最后一步单独拿出来(6的最后一步即是7)iret - pop cs:rip/ss:rsp/rflags,从Y进程的内核堆栈中弹出(3)中对应的压栈内容。此时完
成了中断上下文的切换,即从进程Y的内核态返回到进程Y的用户态。注意快速系统调用返回sysret与iret的处理略有不同。


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

要点

        • 中断和中断返回有中断上下文的切换,CPU和内核代码中断处理程序入口的汇编代码结合起来完成中断上下文的切换。
        • 进程调度过程中有进程上下文的切换,而进程上下文的切换完全由内核完成,具体包括:从一个进程的地址空间切换到另一个进程的地址空间;从一个进程的内核堆栈切换到另一个进程的内核堆栈;还有进程的CPU上下文的切换。

 

进程调度的时机

 

  • schedule()函数负责调度。
  • 中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();比如sleep,就可能直接调用了schedule
  • 内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度;
  • 用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。用户态进程只能被动调度。

 

posted @ 2020-06-08 16:06  小不点明  阅读(363)  评论(0编辑  收藏  举报