结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程
1.以fork和execve系统调用为例分析中断上下文的切换;
2.分析execve系统调用中断上下文的特殊之处;
3.分析fork子进程启动执行时进程上下文的特殊之处;
4.以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程。
一.中断上下文的切换和进程上下文切换
整个linux内部结构可以分为三部分,从最底层到最上层依次是:硬件-->内核空间-->用户空间。如下网络图片所示:
程序在执行过程中通常有用户态和内核态两种状态,CPU对处于内核态根据上下文环境进一步细分,因此有了下面三种状态:
(1)内核态,运行于进程上下文,内核代表进程运行于内核空间。
(2)内核态,运行于中断上下文,内核代表硬件运行于内核空间。
(3)用户态,运行于用户空间。
当一个任务(进程)执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态)。
此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。
当正在执行用户程序而突然被中断程序中断时,此时用户程序也可以象征性地称为处于进程的内核态。
进程上下文切换:
用户空间的应用程序,通过系统调用,进入内核空间。这个时候用户空间的进程要传递 很多变量、参数的值给内核,内核态运行的时候也要保存用户进程的
一些寄存 器值、变量等。所谓的“进程上下文”,可以看作是用户进程传递给内核的这些参数以及内核要保存的那一整套的变量和寄存器值和当时的环境等。
相对于进程而言,就是进程执行时的环境。具体来说就是各个变量和数据,包括所有的寄存器变量、进程打开的文件、内存信息等。一个进程的上下文可以
分为三个部分:用户级上下文、寄存器上下文以及系统级上下文。
中断上下文的切换:
硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的 一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。所谓
的“ 中断上下文”,其实也可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被打断执行的进程环境)。
二.分析execve系统调用中断上下文的特殊之处
execve系统调用的作用是运行另外一个指定的程序。它会把新程序加载到当前进程的内存空间内,当前的进程会被丢弃,它的堆、栈和所有的段数据都会被新进程相应的部分代替,
然后会从新程序的初始化代码和 main 函数开始运行。同时,进程的 ID 将保持不变。execve系统调用通常与 fork系统调用配合使用。从一个进程中启动另一个程序时,通常是先fork一个
子进程,然后在子进程中使用 execve变为运行指定程序的进程。
系统调用过程
sys_execve -> do_execve() –>do_execveat_common() -> __do_execve_file -> exec_binprm()-> search_binary_handler() ->load_binary-> load_elf_binary->start_thread()。
load_elf_binary()里面
/ 为进程分配用户态堆栈,并塞入参数和环境变量 retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP), executable_stack); current->mm->start_stack = bprm->p; // 修改保存在内核堆栈,但属于用户态的eip和esp start_thread(regs, elf_entry, bprm->p); retval = 0;
初始化ELF进程环境
将系统调用的返回地址修改为ELF可执行程序的入口点,这个入口点取决于程序的连接方式,对于静态链接的程序其入口就是e_entry,而动态链接的程序其入口是动态链接器
最后调用start_thread
,修改保存在内核堆栈,但属于用户态的eip
和esp
execve系统调用过程:陷入内核,加载新的进程,将新的进程,完全覆盖原先进程的数据空间,将 IP 值设置为新的进程的入口地址,将 IP 值设置为新的进程的入口地址,返回用户态,新程序继续执行下去。老进程的上下文被完全替换,但进程的 pid 不变,所以 execve 系统调用不会返回原进程,而是返回新进程。
三.分析fork子进程启动执行时进程上下文的特殊之处
fork系统调用用于从已存在进程中创建一个新进程,新进程称为子进程,而原进程称为父进程。fork调用一次,返回两次,这两个返回分别带回它们各自的返回值,其中在父进程中的返回值是子进程的进程号,而子进程中的返回值则返回 0。因此,可以通过返回值来判定该进程是父进程还是子进程。
调用过程:sys_fork系统调用-> clone系统调用->do_fork函数
do_fork()调用了 copy_process 函数,复制当前进程产生子进程,并且传入关键参数为子进程设置响应进程上下文
copy_process调用copy_thread,设置子进程的堆栈信息;子进程从ret_from_fork开始执行,所以它的地址赋给thread.ip,也就是将来的eip寄存器
四.以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程
进程的切换和系统的一般执行过程进程调度的时机:
- (1)中断处理过程(包括时钟中断、I/O中断、系统调用和异常),直接调用schedule(),或者通过need_resched标记调度的schedule()
- (2)用户态进程只能被动调度,即在中断处理过程中进行调度
- (3)内核线程可以直接调用schedule(),也可以在中断处理过程中进行调度,可以主动调度也可以被动调度
- 进程上下文切换:挂起正在cpu上运行的进程,与中断时保存现场是不同的,中断前后是在同一个上下文中,只是由用户态转向内核态,但是进程切换是两个进程间的切换
(1)需要切换的信息:用户地址空间、控制信息、硬件上下文(中断也要保存上下文)等
(2)schedule()中的context_switch中的switch_to切换寄存器和堆栈的状态、eip的位置
(3)thread.sp内核堆栈的栈底;thread.ip当前进程的eip
(4)next进程的第一条指令:next_ip一般是$1f,对于新创建的子进程是ret_from_fork
switch_to做了关键的进程上下文的切换
restore_all恢复现场
0-3G用户态,3gG以上内核态访问
Linux系统执行过程中的几个特殊情况
(1)内核线程通过中断处理过程中(用户态进程与内核线程间的切换以及内核进程和内核进程间的切换)没有权限的变化,cs段没有发生变化
(2)内核线程主动调度schedule(),只有进程上下文的切换,没有发生中断上下文的切换
(3)创建子进程的系统调用,子进程返回时的执行起点为ret_form_fork(next_ip=ret_form_fork)