进程切换上下文需要那些操作呢?
- 保存当前进程的上下文
- 恢复某个先前被抢占的进程被保存的上下文
- 将控制传递给这个新恢复的进程。
具体:
进程地址空间指的是进程所拥有的虚拟地址空间,而这个地址空间是假的,是linux内核通过数据结构来描述出来的,从而使得每一个进程都感觉到自己拥有整个内存的假象,cpu访问的指令和数据最终会落实到实际的物理地址,对用进程而言通过缺页异常来分配和建立页表映射。进程地址空间内有进程运行的指令和数据,因此到调度器从其他进程重新切换到我的时候,为了保证当前进程访问的虚拟地址是自己的必须切换地址空间。
实际上,进程地址空间使用mm_struct结构体来描述,这个结构体被嵌入到进程描述符(我们通常所说的进程控制块PCB)task_struct中,mm_struct结构体将各个vma组织起来进行管理,其中有一个成员pgd至关重要,地址空间切换中最重要的是pgd的设置。
pgd中保存的是进程的页全局目录的虚拟地址,记住保存的是虚拟地址,那么pgd的值是何时被设置的呢?答案是fork的时候,如果是创建进程,需要分配设置mm_struct,其中会分配进程页全局目录所在的页,然后将首地址赋值给pgd。
我们来看看进程地址空间究竟是如何切换的,结果会让你大吃一惊
代码路径如下:
context_switch // kernel/sched/core.c
->switch_mm_irqs_off
->switch_mm
->__switch_mm
->check_and_switch_context
->cpu_switch_mm
->cpu_do_switch_mm(virt_to_phys(pgd),mm) //arch/arm64/include/asm/mmu_context.h
arch/arm64/mm/proc.S
158 /*
159 * cpu_do_switch_mm(pgd_phys, tsk)
160 *
161 * Set the translation table base pointer to be pgd_phys.
162 *
163 * - pgd_phys - physical address of new TTB
164 */
165 ENTRY(cpu_do_switch_mm)
166 mrs x2, ttbr1_el1
167 mmid x1, x1 // get mm->context.id
168 phys_to_ttbr x3, x0
169
170 alternative_if ARM64_HAS_CNP
171 cbz x1, 1f // skip CNP for reserved ASID
172 orr x3, x3, #TTBR_CNP_BIT
173 1:
174 alternative_else_nop_endif
175 #ifdef CONFIG_ARM64_SW_TTBR0_PAN
176 bfi x3, x1, #48, #16 // set the ASID field in TTBR0
177 #endif
178 bfi x2, x1, #48, #16 // set the ASID
179 msr ttbr1_el1, x2 // in TTBR1 (since TCR.A1 is set)
180 isb
181 msr ttbr0_el1, x3 // now update TTBR0
182 isb
183 b post_ttbr_update_workaround // Back to C code...
184 ENDPROC(cpu_do_switch_mm)
代码中最核心的为181行,最终将进程的pgd虚拟地址转化为物理地址存放在ttbr0_el1中,这是用户空间的页表基址寄存器,当访问用户空间地址的时候mmu会通过这个寄存器来做遍历页表获得物理地址(ttbr1_el1是内核空间的页表基址寄存器,访问内核空间地址时使用,所有进程共享,不需要切换)。完成了这一步,也就完成了进程的地址空间切换,确切的说是进程的虚拟地址空间切换。
内核处理的是不是很简单,很优雅,别看只是设置了页表基址寄存器,也就是将即将执行的进程的页全局目录的物理地址设置到页表基址寄存器,他却完成了地址空间切换的壮举,有的小伙伴可能不明白为啥这就完成了地址空间切换?试想如果进程想要访问一个用户空间虚拟地址,cpu的mmu所做的工作,就是从页表基址寄存器拿到页全局目录的物理基地址,然后和虚拟地址配合来查查找页表,最终找到物理地址进行访问(当然如果tlb命中就不需要遍历页表),每次用户虚拟地址访问的时候(内核空间共享不考虑),由于页表基地址寄存器内存放的是当前执行进程的页全局目录的物理地址,所以访问自己的一套页表,拿到的是属于自己的物理地址(实际上,进程是访问虚拟地址空间的指令数据的时候不断发生缺页异常,然后缺页异常处理程序为进程分配实际的物理页,然后将页帧号和页表属性填入自己的页表条目中),就不会访问其他进程的指令和数据,这也是为何多个进程可以访问相同的虚拟地址而不会出现差错的原因,而且做到的各个地址空间的隔离互不影响(共享内存除外)。
其实,地址空间切换过程中,还会清空tlb,防止当前进程虚拟地址转化过程中命中上一个进程的tlb表项,一般会将所有的tlb无效,但是这会导致很大的性能损失,因为新进程被切换进来的时候面对的是全新的空的tlb,造成很大概率的tlb miss,需要重新遍历多级页表。
还需要注意的是仅仅切换用户地址空间,内核地址空间由于是共享的不需要切换,也就是为何切换到内核线程不需要也没有地址空间的原因。