进程上下文

进程上下文是进程执行活动全过程的静态描述。

  • 我们把已执行过的进程指令和数据在相关寄存器与堆栈中的内容称为进程上文。
  • 把正在执行的指令和数据在寄存器与堆栈中的内容称为进程正文。
  • 把待执行的指令和数据在寄存器与堆栈中的内容称为进程下文。

实际上linux内核中,进程上下文包括进程的虚拟地址空间和硬件上下文。

进程硬件上下文包含了当前cpu的一组寄存器的集合,arm64中使用 task_struct (linux 中进程和线程都是使用 task_struct 描述)结构的 thread 成员的 cpu_context 成员来描述,包括x19-x28,sp, pc等。

进程上下文切换

进程上下文切换需要一次进行两个操作:

  1. 进程地址空间的切换
  2. 处理器状态(硬件上下文)切换

进程地址空间的切换

vma

一个进程具有多个 vma,所有的VMA是以链表的形式串联在一块形成一个进程的虚拟地址空间完整的描述。一个进程基本上可以分成以下几个区域:

  • 代码VMA,权限只读,可执行,有映像文件
  • 数据VMA,权限可读写,可执行,有映像文件
  • 堆VMA,权限可读写,可执行,无映像文件,匿名,地址向上生长;
  • 栈VMA,权限可读写,不可执行,无映像文件,匿名,地址向下生长。

地址空间切换

进程地址空间使用 mm_struct 结构体来描述,这个结构体被嵌入到进程描述符(我们通常所说的进程控制块 PCB )task_struct 中,mm_struct 结构体将各个 vma(虚拟内存区域)组织起来进行管理,其中有一个成员 pgd 至关重要,地址空间切换中最重要的是pgd 的设置。

pgd 中保存的是进程的页全局目录的虚拟地址,记住保存的是虚拟地址,那么 pgd 的值是何时被设置的呢?答案是 fork 的时候,如果是创建进程,需要分配设置 mm_struct,其中会分配进程页全局目录所在的页,然后将首地址赋值给 pgd。

cpu 在切换地址空间时,将进程的 pgd 虚拟地址转化为物理地址存放在 cpu 的一个基址寄存器中这是用户空间的页表基址寄存器,当访问用户空间地址的时候 mmu 会通过这个寄存器来做遍历页表获得物理地址。

地址空间切换过程中,还会清空 tlb,防止当前进程虚拟地址转化过程中命中上一个进程的 tlb 表项,一般会将所有的 tlb 无效,但是这会导致很大的性能损失,因为新进程被切换进来的时候面对的是全新的空的 tlb,造成很大概率的 tlb miss,需要重新遍历多级页表,所以 arm64 在 tlb 表项中增加了非全局(nG)位区分内核和进程的页表项,使用 ASID 区分不同进程的页表项,来保证可以在切换地址空间的时候可以不刷 tlb。

处理器状态切换

执行完地址空间的切换,进程执行的内核栈还是前一个进程的,当前执行流也还是前一个进程的,需要做切换。

大致的说,会将当前内核寄存器中的数据保存到进程描述符的 cpu_contex 中,然后将即将执行的进程的描述符的cpu_contex 的寄存器数据恢复到相应寄存器中,而且将即将执行的进程的进程描述符 task_struct 地址存放在寄存器中,用于通过 current 找到当前进程,这样就完成了处理器的状态切换。

实际上,处理器状态切换就是将前一个进程的寄存器的值保存到一块内存上,然后将即将执行的进程的寄存器的值从另一块内存中恢复到相应寄存器中,恢复sp完成了进程内核栈的切换,恢复pc完成了指令执行流的切换。其中保存/恢复所用到的那块内存需要被进程所标识,这块内存这就是 cpu_contex 这个结构的位置(进程切换都是在内核空间完成)。

由于用户空间通过异常/中断进入内核空间的时候都需要保存现场,也就是保存发生异常/中断时的所有通用寄存器的值,内核会把 “现场” 保存到每个进程特有的进程内核栈中,并用 pt_regs 结构来描述,当异常/中断处理完成之后会返回用户空间,返回之前会恢复之前保存的“现场”,用户程序继续执行。

所以当进程切换的时候,当前进程被时钟中断打断,将发生中断时的现场保存到进程内核栈,然后会切换到下一个进程,当再次回切换回来的时候,返回用户空间的时候会恢复之前的现场,进程就可以继续执行(执行之前被中断打断的下一条指令,继续使用自己用户态sp),这对于用户进程来说是透明的。