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

1.主要内容

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

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

 

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

fork系统调用:

库函数fork是⽤户态创建⼀个⼦进程的系统调⽤API接⼝。对于判断fork函数的返回值,初学者可能会很迷惑, 因为fork在正常执⾏后,if条件判断中除了if (pid < 0)异常处理没被执⾏,else if (pid == 0)和else两段代码都被 执⾏了,这看起来确实匪夷所思。

实际上fork系统调⽤把当前进程⼜复制了⼀个⼦进程,也就⼀个进程变成了两个进程,两个进程执⾏相同的代 码,只是fork系统调⽤在⽗进程和⼦进程中的返回值不同。可是从Shell终端输出信息看两个进程是混合在⼀起 的,会让⼈误以为if语句的执⾏产⽣了错误。其实是if语句在两个进程中各执⾏了⼀次,由于判断条件不同,输 出的信息也就不同。⽗进程没有打破if else的条件分⽀的结构,在⼦进程⾥⾯也没有打破这个结构,只是在 Shell命令⾏下好像两个都输出了,好像打破了条件分⽀结构,实际上背后是两个进程。fork之后,⽗⼦进程的 执⾏顺序和调度算法密切相关,多次执⾏有时可以看到⽗⼦进程的执⾏顺序并不是确定的。

 

fork系统调用过程:

fork也是⼀个系统调⽤,和⼀般的系统调⽤执⾏过程⼤致是⼀样的。尤其从⽗进程的⻆度来看, fork的执⾏过程与前述描述完全⼀致,但问题是:fork系统调⽤创建了⼀个⼦进程,⼦进程复制了⽗ 进程中所有的进程信息,包括内核堆栈、进程描述符等,⼦进程作为⼀个独⽴的进程也会被调度, 当⼦进程获得CPU开始运⾏时,它是从哪⾥开始运⾏的呢?从⽤户态空间来看,就是fork系统调⽤ 的下⼀条指令。但fork系统调⽤在⼦进程当中也是返回的,也就是说fork系统调⽤在内核⾥⾯变成了 ⽗⼦两个进程,⽗进程正常fork系统调⽤返回到⽤户态,fork出来的⼦进程也要从内核⾥返回到⽤户 态。那么对于⼦进程来讲,fork系统调⽤在内核处理程序中是从何处开始执⾏的呢?⼀个新创建的 ⼦进程是从哪⾏代码开始执⾏的,这是⼀个关键问题。下⾯带着这个问题来仔细分析fork系统调⽤ 的内核处理过程。

1.首先是发生一个软中断,执行系统调用,⽗进程通过fork系统调⽤进⼊内核_do_fork函数,具体过程如下:首先是用户程序调用fork(),然后是库函数fork(),系统调用fork(通过系统调用号),通过sys_call_table中寻到sys_fork()的函数地址,调用sys_fork,最后调用do_fork();

2._do_fork函数主要完成了调⽤copy_process()复制⽗进程、获得pid、调⽤wake_up_new_task将⼦进程加⼊就绪队列等待调度执⾏等。

3.copy_process()是创建⼀个进程的主要的代码,copy_process函数主要完成了调⽤ dup_task_struct复制当前进程(⽗进程)描述 符task_struct、信息检查、初始化、把进程状态 设置为TASK_RUNNING(此时⼦进程置为就绪 态)、采⽤写时复制技术逐⼀复制所有其他进程资源、调⽤copy_thread_tls初始化⼦进程内核栈、设置⼦进程pid等。其中最关键的就是 dup_task_struct复制当前进程(⽗进程)描述 符task_struct和copy_thread_tls初始化⼦进程内核栈。

4.⼦进程创建好了进程描述符、内核堆栈等,就可以通过 wake_up_new_task(p)将⼦进程添加到就绪队列,使之有机会被调度 执⾏,进程的创建⼯作就完成了,⼦进程就可以等待调度执⾏,⼦进程的执⾏从这⾥设定的ret_from_fork开始。

 

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

fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:

    1)在父进程中,fork返回新创建子进程的进程ID;

    2)在子进程中,fork返回0;

    3)如果出现错误,fork返回一个负值;

创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。此时,两个进程都从fork开始往下执行,只是pid不同

 

execve系统调用:

execve() 系统调用的作用是运行另外一个指定的程序。它会把新程序加载到当前进程的内存空间内,当前的进程会被丢弃,它的堆、栈和所有的段数据都会被新进程相应的部分代替,然后会从新程序的初始化代码和 main 函数开始运行。同时,进程的 ID 将保持不变。execve() 系统调用通常与 fork() 系统调用配合使用。从一个进程中启动另一个程序时,通常是先 fork() 一个子进程,然后在子进程中使用 execve() 变身为运行指定程序的进程。 例如,当用户在 Shell 下输入一条命令启动指定程序时,Shell 就是先 fork() 了自身进程,然后在子进程中使用 execve() 来运行指定的程序。

execve系统调用的执行过程:

      1.陷入内核;

      2.加载新的可执行文件并进行可执行性检查;

      3.将新的可执行文件映射到当前运行进程的进程空间中,并覆盖原来的进程数据;

      4.将EIP的值设置为新的可执行程序的入口地址。如果可执行程序是静态链接的程序,或不需要其他的动态链接库,则新的入口地址就是新的可执行文件的main函数地址;如果可执行程序还需要其他的动态链接库,则入口地址是加载器ld的入口地址;

      5.返回用户态,程序从新的EIP出开始继续往下执行。至此,老进程的上下文已经被新的进程完全替代了,但是进程的PID还是原来的。从这个角度来看,新的运行进程中已经找不到原来的对execve调用的代码了,所以execve函数的一个特别之处是他从来不会成功返回,而总是实现了一次完全的变身。   

      sys_execve的核心是调用do_execve函数,传给do_execve的第一个参数是已经拷贝到内核空间的路径名filename,第二个和第三个参数仍然是系统调用execve的第二个参数argv和第三个参数envp,它们代表的传给可执行文件的参数和环境变量仍然保留在用户空间中。

 

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

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

 

linux系统执行的几种特殊情况:

• • 通过中断处理过程中的调度时机,内核线程之间互相切换。与最⼀般的情况⾮常类似,只是内核线程在运⾏过 程中发⽣中断,没有进程⽤户态和内核态的转换。⽐如两个内核线程之间切换,CS 段寄存器没有发⽣变化,没有⽤户 态与内核态的切换。

• • ⽤户进程向内核线程的切换。⽐最⼀般的情况更简略,内核线程不需要从内核态返回到⽤户态,如果该内线线 程是直接调⽤schedule主动让出CPU的,该内核线程被重新调度执⾏时也就没有中断上下⽂恢复的问题。

• • 内核线程向⽤户进程的切换。如果是内核线程主动调⽤schedule函数,只有进程上下⽂的切换,没有发⽣中断 上下⽂切换。它与最⼀般的情况也更简略,但⽤户进程从内核态返回到⽤户态时依然需要中断上下⽂的恢复。

• • 创建进程的系统调⽤在⼦进程中的执⾏起点较为特殊,单独创建了⼀个进程上下⽂。如fork⼀个⼦进程时,⼦ 进程不是从switch_to下⼀⾏代码(5.4.34)或者标号1的位置(3.18.6)开始执⾏的,⽽是从ret_from_fork开始执⾏的。

 

posted @ 2020-06-15 11:04  zhouxinghui  阅读(142)  评论(0编辑  收藏  举报