共計(jì) 9412 個(gè)字符,預(yù)計(jì)需要花費(fèi) 24 分鐘才能閱讀完成。
這篇文章主要講解了“Linux 內(nèi)核進(jìn)程上下文切換怎么理解”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著丸趣 TV 小編的思路慢慢深入,一起來(lái)研究和學(xué)習(xí)“Linux 內(nèi)核進(jìn)程上下文切換怎么理解”吧!
1. 進(jìn)程上下文的概念
進(jìn)程上下文是進(jìn)程執(zhí)行活動(dòng)全過(guò)程的靜態(tài)描述。我們把已執(zhí)行過(guò)的進(jìn)程指令和數(shù)據(jù)在相關(guān)寄存器與堆棧中的內(nèi)容稱(chēng)為進(jìn)程上文,把正在執(zhí)行的指令和數(shù)據(jù)在寄存器與堆棧中的內(nèi)容稱(chēng)為進(jìn)程正文,把待執(zhí)行的指令和數(shù)據(jù)在寄存器與堆棧中的內(nèi)容稱(chēng)為進(jìn)程下文。
實(shí)際上 linux 內(nèi)核中,進(jìn)程上下文包括進(jìn)程的虛擬地址空間和硬件上下文。
進(jìn)程硬件上下文包含了當(dāng)前 cpu 的一組寄存器的集合,arm64 中使用 task_struct 結(jié)構(gòu)的 thread 成員的 cpu_context 成員來(lái)描述,包括 x19-x28,sp, pc 等。
如下為硬件上下文存放示例圖:
2. 上下文切換詳細(xì)過(guò)程
進(jìn)程上下文切換主要涉及到兩部分主要過(guò)程:進(jìn)程地址空間切換和處理器狀態(tài)切換。地址空間切換主要是針對(duì)用戶(hù)進(jìn)程而言,而處理器狀態(tài)切換對(duì)應(yīng)于所有的調(diào)度單位。下面我們分別看下這兩個(gè)過(guò)程:
__schedule // kernel/sched/core.c - context_switch - switch_mm_irqs_off // 進(jìn)程地址空間切換 - switch_to // 處理器狀態(tài)切換
2.1 進(jìn)程地址空間切換
進(jìn)程地址空間指的是進(jìn)程所擁有的虛擬地址空間,而這個(gè)地址空間是假的,是 linux 內(nèi)核通過(guò)數(shù)據(jù)結(jié)構(gòu)來(lái)描述出來(lái)的,從而使得每一個(gè)進(jìn)程都感覺(jué)到自己擁有整個(gè)內(nèi)存的假象,cpu 訪(fǎng)問(wèn)的指令和數(shù)據(jù)最終會(huì)落實(shí)到實(shí)際的物理地址,對(duì)用進(jìn)程而言通過(guò)缺頁(yè)異常來(lái)分配和建立頁(yè)表映射。進(jìn)程地址空間內(nèi)有進(jìn)程運(yùn)行的指令和數(shù)據(jù),因此到調(diào)度器從其他進(jìn)程重新切換到我的時(shí)候,為了保證當(dāng)前進(jìn)程訪(fǎng)問(wèn)的虛擬地址是自己的必須切換地址空間。
實(shí)際上,進(jìn)程地址空間使用 mm_struct 結(jié)構(gòu)體來(lái)描述,這個(gè)結(jié)構(gòu)體被嵌入到進(jìn)程描述符(我們通常所說(shuō)的進(jìn)程控制塊 PCB)task_struct 中,mm_struct 結(jié)構(gòu)體將各個(gè) vma 組織起來(lái)進(jìn)行管理,其中有一個(gè)成員 pgd 至關(guān)重要,地址空間切換中最重要的是 pgd 的設(shè)置。
pgd 中保存的是進(jìn)程的頁(yè)全局目錄的虛擬地址(本文會(huì)涉及到頁(yè)表相關(guān)的一些概念,在此不是重點(diǎn),不清楚的可以查閱相關(guān)資料,后期有機(jī)會(huì)會(huì)講解進(jìn)程頁(yè)表),記住保存的是虛擬地址,那么 pgd 的值是何時(shí)被設(shè)置的呢? 答案是 fork 的時(shí)候,如果是創(chuàng)建進(jìn)程,需要分配設(shè)置 mm_struct,其中會(huì)分配進(jìn)程頁(yè)全局目錄所在的頁(yè),然后將首地址賦值給 pgd。
我們來(lái)看看進(jìn)程地址空間究竟是如何切換的,結(jié)果會(huì)讓你大吃一驚(這里暫且不考慮 asid 機(jī)制,后面有機(jī)會(huì)會(huì)在其他文章中講解):
代碼路徑如下:
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 行,最終將進(jìn)程的 pgd 虛擬地址轉(zhuǎn)化為物理地址存放在 ttbr0_el1 中,這是用戶(hù)空間的頁(yè)表基址寄存器,當(dāng)訪(fǎng)問(wèn)用戶(hù)空間地址的時(shí)候 mmu 會(huì)通過(guò)這個(gè)寄存器來(lái)做遍歷頁(yè)表獲得物理地址(ttbr1_el1 是內(nèi)核空間的頁(yè)表基址寄存器,訪(fǎng)問(wèn)內(nèi)核空間地址時(shí)使用,所有進(jìn)程共享,不需要切換)。完成了這一步,也就完成了進(jìn)程的地址空間切換,確切的說(shuō)是進(jìn)程的虛擬地址空間切換。
內(nèi)核處理的是不是很簡(jiǎn)單,很優(yōu)雅,別看只是設(shè)置了頁(yè)表基址寄存器,也就是將即將執(zhí)行的進(jìn)程的頁(yè)全局目錄的物理地址設(shè)置到頁(yè)表基址寄存器,他卻完成了地址空間切換的壯舉,有的小伙伴可能不明白為啥這就完成了地址空間切換? 試想如果進(jìn)程想要訪(fǎng)問(wèn)一個(gè)用戶(hù)空間虛擬地址,cpu 的 mmu 所做的工作,就是從頁(yè)表基址寄存器拿到頁(yè)全局目錄的物理基地址,然后和虛擬地址配合來(lái)查查找頁(yè)表,最終找到物理地址進(jìn)行訪(fǎng)問(wèn)(當(dāng)然如果 tlb 命中就不需要遍歷頁(yè)表),每次用戶(hù)虛擬地址訪(fǎng)問(wèn)的時(shí)候(內(nèi)核空間共享不考慮),由于頁(yè)表基地址寄存器內(nèi)存放的是當(dāng)前執(zhí)行進(jìn)程的頁(yè)全局目錄的物理地址,所以訪(fǎng)問(wèn)自己的一套頁(yè)表,拿到的是屬于自己的物理地址(實(shí)際上,進(jìn)程是訪(fǎng)問(wèn)虛擬地址空間的指令數(shù)據(jù)的時(shí)候不斷發(fā)生缺頁(yè)異常,然后缺頁(yè)異常處理程序?yàn)檫M(jìn)程分配實(shí)際的物理頁(yè),然后將頁(yè)幀號(hào)和頁(yè)表屬性填入自己的頁(yè)表?xiàng)l目中),就不會(huì)訪(fǎng)問(wèn)其他進(jìn)程的指令和數(shù)據(jù),這也是為何多個(gè)進(jìn)程可以訪(fǎng)問(wèn)相同的虛擬地址而不會(huì)出現(xiàn)差錯(cuò)的原因,而且做到的各個(gè)地址空間的隔離互不影響(共享內(nèi)存除外)。
其實(shí),地址空間切換過(guò)程中,還會(huì)清空 tlb,防止當(dāng)前進(jìn)程虛擬地址轉(zhuǎn)化過(guò)程中命中上一個(gè)進(jìn)程的 tlb 表項(xiàng),一般會(huì)將所有的 tlb 無(wú)效,但是這會(huì)導(dǎo)致很大的性能損失,因?yàn)樾逻M(jìn)程被切換進(jìn)來(lái)的時(shí)候面對(duì)的是全新的空的 tlb,造成很大概率的 tlb miss, 需要重新遍歷多級(jí)頁(yè)表,所以 arm64 在 tlb 表項(xiàng)中增加了非全局 (nG) 位區(qū)分內(nèi)核和進(jìn)程的頁(yè)表項(xiàng),使用 ASID 區(qū)分不同進(jìn)程的頁(yè)表項(xiàng),來(lái)保證可以在切換地址空間的時(shí)候可以不刷 tlb,后面會(huì)主要講解 ASID 技術(shù)。
還需要注意的是僅僅切換用戶(hù)地址空間,內(nèi)核地址空間由于是共享的不需要切換,也就是為何切換到內(nèi)核線(xiàn)程不需要也沒(méi)有地址空間的原因。
如下為進(jìn)程地址空間切換示例圖:
2.2 處理器狀態(tài) (硬件上下文) 切換
前面進(jìn)行了地址空間切換,只是保證了進(jìn)程訪(fǎng)問(wèn)指令數(shù)據(jù)時(shí)訪(fǎng)問(wèn)的是自己地址空間(當(dāng)然上下文切換的時(shí)候處于內(nèi)核空間,執(zhí)行的是內(nèi)核地址數(shù)據(jù),當(dāng)返回用戶(hù)空間的時(shí)候才有機(jī)會(huì)執(zhí)行用戶(hù)空間指令數(shù)據(jù) **,地址空間切換為進(jìn)程訪(fǎng)問(wèn)自己用戶(hù)空間做好了準(zhǔn)備 **),但是進(jìn)程執(zhí)行的內(nèi)核棧還是前一個(gè)進(jìn)程的,當(dāng)前執(zhí)行流也還是前一個(gè)進(jìn)程的,需要做切換。
arm64 中切換代碼如下:
switch_to - __switch_to ... // 浮點(diǎn)寄存器等的切換 - cpu_switch_to(prev, next) arch/arm64/kernel/entry.S: 1032 /* 1033 * Register switch for AArch74. The callee-saved registers need to be saved 1034 * and restored. On entry: 1035 * x0 = previous task_struct (must be preserved across the switch) 1036 * x1 = next task_struct 1037 * Previous and next are guaranteed not to be the same. 1038 * 1039 */ 1040 ENTRY(cpu_switch_to) 1041 mov x10, #THREAD_CPU_CONTEXT 1042 add x8, x0, x10 1043 mov x9, sp 1044 stp x19, x20, [x8], #16 // store callee-saved registers 1045 stp x21, x22, [x8], #16 1046 stp x23, x24, [x8], #16 1047 stp x25, x26, [x8], #16 1048 stp x27, x28, [x8], #16 1049 stp x29, x9, [x8], #16 1050 str lr, [x8] 1051 add x8, x1, x10 1052 ldp x19, x20, [x8], #16 // restore callee-saved registers 1053 ldp x21, x22, [x8], #16 1054 ldp x23, x24, [x8], #16 1055 ldp x25, x26, [x8], #16 1056 ldp x27, x28, [x8], #16 1057 ldp x29, x9, [x8], #16 1058 ldr lr, [x8] 1059 mov sp, x9 1060 msr sp_el0, x1 1061 ret 1062 ENDPROC(cpu_switch_to)
其中 x19-x28 是 arm64 架構(gòu)規(guī)定需要調(diào)用保存的寄存器,可以看到處理器狀態(tài)切換的時(shí)候?qū)⑶耙粋€(gè)進(jìn)程 (prev) 的 x19-x28,fp,sp,pc 保存到了進(jìn)程描述符的 cpu_contex 中,然后將即將執(zhí)行的進(jìn)程 (next) 描述符的 cpu_contex 的 x19-x28,fp,sp,pc 恢復(fù)到相應(yīng)寄存器中,而且將 next 進(jìn)程的進(jìn)程描述符 task_struct 地址存放在 sp_el0 中,用于通過(guò) current 找到當(dāng)前進(jìn)程,這樣就完成了處理器的狀態(tài)切換。
實(shí)際上,處理器狀態(tài)切換就是將前一個(gè)進(jìn)程的 sp,pc 等寄存器的值保存到一塊內(nèi)存上,然后將即將執(zhí)行的進(jìn)程的 sp,pc 等寄存器的值從另一塊內(nèi)存中恢復(fù)到相應(yīng)寄存器中,恢復(fù) sp 完成了進(jìn)程內(nèi)核棧的切換,恢復(fù) pc 完成了指令執(zhí)行流的切換。其中保存 / 恢復(fù)所用到的那塊內(nèi)存需要被進(jìn)程所標(biāo)識(shí),這塊內(nèi)存這就是 cpu_contex 這個(gè)結(jié)構(gòu)的位置(進(jìn)程切換都是在內(nèi)核空間完成)。
由于用戶(hù)空間通過(guò)異常 / 中斷進(jìn)入內(nèi)核空間的時(shí)候都需要保存現(xiàn)場(chǎng),也就是保存發(fā)生異常 / 中斷時(shí)的所有通用寄存器的值,內(nèi)核會(huì)把“現(xiàn)場(chǎng)”保存到每個(gè)進(jìn)程特有的進(jìn)程內(nèi)核棧中,并用 pt_regs 結(jié)構(gòu)來(lái)描述,當(dāng)異常 / 中斷處理完成之后會(huì)返回用戶(hù)空間,返回之前會(huì)恢復(fù)之前保存的“現(xiàn)場(chǎng)”,用戶(hù)程序繼續(xù)執(zhí)行。
所以當(dāng)進(jìn)程切換的時(shí)候,當(dāng)前進(jìn)程被時(shí)鐘中斷打斷,將發(fā)生中斷時(shí)的現(xiàn)場(chǎng)保存到進(jìn)程內(nèi)核棧(如:sp, lr 等),然后會(huì)切換到下一個(gè)進(jìn)程,當(dāng)再次回切換回來(lái)的時(shí)候,返回用戶(hù)空間的時(shí)候會(huì)恢復(fù)之前的現(xiàn)場(chǎng),進(jìn)程就可以繼續(xù)執(zhí)行(執(zhí)行之前被中斷打斷的下一條指令,繼續(xù)使用自己用戶(hù)態(tài) sp),這對(duì)于用戶(hù)進(jìn)程來(lái)說(shuō)是透明的。
如下為硬件上下文切換示例圖:
3.ASID 機(jī)制
前面講過(guò),進(jìn)程切換的時(shí)候,由于 tlb 中存放的可能是其他進(jìn)程的 tlb 表項(xiàng),所有才需要在進(jìn)程切換的時(shí)候進(jìn)行 tlb 的清空工作(清空即是使得所有的 tlb 表項(xiàng)無(wú)效,地址轉(zhuǎn)換需要遍歷多級(jí)頁(yè)表,找到頁(yè)表項(xiàng),然后重新加載頁(yè)表項(xiàng)到 tlb),有了 ASID 機(jī)制之后,命中 tlb 表項(xiàng),由虛擬地址和 ASID 共同決定(當(dāng)然還有 nG 位),可以減小進(jìn)程切換中 tlb 被清空的機(jī)會(huì)。
下面我們講解 ASID 機(jī)制,ASID(Address Space Identifer 地址空間標(biāo)識(shí)符),用于區(qū)別不同進(jìn)程的頁(yè)表項(xiàng),arm64 中,可以選擇兩種 ASID 長(zhǎng)度 8 位或者 16 位,這里以 8 位來(lái)講解。
如果 ASID 長(zhǎng)度為 8 位,那么 ASID 有 256 個(gè)值,但是由于 0 是保留的,所有可以分配的 ASID 范圍就為 1 -255,那么可以標(biāo)識(shí) 255 個(gè)進(jìn)程,當(dāng)超出 255 個(gè)進(jìn)程的時(shí)候,會(huì)出現(xiàn)兩個(gè)進(jìn)程的 ASID 相同的情況,因此內(nèi)核使用了 ASID 版本號(hào)。
內(nèi)核中處理如下(參考 arch/arm64/mm/context.c):
1)內(nèi)核為每個(gè)進(jìn)程分配一個(gè) 64 位的軟件 ASID,其中低 8 位為硬件 ASID,高 56 位為 ASID 版本號(hào),這個(gè)軟件 ASID 存放放在進(jìn)程的 mm_struct 結(jié)構(gòu)的 context 結(jié)構(gòu)的 id 中, 進(jìn)程創(chuàng)建的時(shí)候會(huì)初始化為 0。
2)內(nèi)核中有一個(gè) 64 位的全局變量 asid_generation,同樣它的高 56 位為 ASID 版本號(hào),用于標(biāo)識(shí)當(dāng)前 ASID 分配的批次。
3)當(dāng)進(jìn)程調(diào)度,由 prev 進(jìn)程切換到 next 進(jìn)程的時(shí)候,如果不是內(nèi)核線(xiàn)程則進(jìn)行地址空間切換調(diào)用 check_and_switch_context,此函數(shù)會(huì)判斷 next 進(jìn)程的 ASID 版本號(hào)是否和全局的 ASID 版本號(hào)相同(是否處于同一批次),如果相同則不需要為 next 進(jìn)程分配 ASID, 不相同則需要分配 ASID。
4)內(nèi)核使用 asid_map 位圖來(lái)管理硬件 ASID 的分配,asid_bits 記錄使用的 ASID 的長(zhǎng)度,每處理器變量 active_asids 保存當(dāng)前分配的硬件 ASID,每處理器變量 reserved_asids 存放保留的 ASID,tlb_flush_pending 位圖記錄需要清空 tlb 的 cpu 集合。
硬件 ASID 分配策略如下:
(1)如果進(jìn)程的 ASID 版本號(hào)和當(dāng)前全局的 ASID 版本號(hào)相同(同批次情況),則不需要重新分配 ASID。
(2)如果進(jìn)程的 ASID 版本號(hào)和當(dāng)前全局的 ASID 版本號(hào)不相同(不同批次情況),且進(jìn)程原本的硬件 ASID 已經(jīng)被分配,則重新分配新的硬件 ASID,并將當(dāng)前全局的 ASID 版本號(hào)組合新分配的硬件 ASID 寫(xiě)到進(jìn)程的軟件 ASID 中。
(3)如果進(jìn)程的 ASID 版本號(hào)和當(dāng)前全局的 ASID 版本號(hào)不相同(不同批次情況),且進(jìn)程原本的硬件 ASID 還沒(méi)有被分配,則不需要重新分配新的硬件 ASID,只需要更新進(jìn)程軟件 ASID 版本號(hào),并將當(dāng)前全局的 ASID 版本號(hào)組合進(jìn)程原來(lái)的硬件 ASID 寫(xiě)到進(jìn)程的軟件 ASID 中。
(4)如果進(jìn)程的 ASID 版本號(hào)和當(dāng)前全局的 ASID 版本號(hào)不相同(不同批次情況),需要分配硬件 ASID 時(shí),發(fā)現(xiàn)硬件 ASID 已經(jīng)被其他進(jìn)程分配完(asid_map 位圖中查找,發(fā)現(xiàn)位圖全 1),則這個(gè)時(shí)候需要遞增全局的 ASID 版本號(hào), 清空所有 cpu 的 tlb, 清空 asid_map 位圖,然后分配硬件 ASID,并將當(dāng)前全局的 ASID 版本號(hào)組合新分配的硬件 ASID 寫(xiě)到進(jìn)程的軟件 ASID 中。
下面我們以實(shí)例來(lái)看 ASID 的分配過(guò)程:
如下圖:
我們假設(shè)圖中從 A 進(jìn)程到 D 進(jìn)程,有 255 個(gè)進(jìn)程,剛好分配完了 asid,,從 A 到 D 的切換過(guò)程中使用的都是同一批次的 asid 版本號(hào)。
則這個(gè)過(guò)程中,有進(jìn)程會(huì)創(chuàng)建的時(shí)候被切換到,假設(shè)不超出 255 個(gè)進(jìn)程,在切換過(guò)程中會(huì)為新進(jìn)程分配硬件的 ASID,分配完后下次切換到他時(shí)由于他的 ASID 版本號(hào)和當(dāng)前的全局的 ASID 版本號(hào)相同,所以不需要再次分配 ASID,當(dāng)然也不需要清空 tlb。
注:這里說(shuō)的 ASID 即為硬件 ASID 區(qū)別于 ASID 版本號(hào)。
情況 1 -ASID 版本號(hào)不變 屬于策略(1):從 C 進(jìn)程到 D 進(jìn)程切換,內(nèi)核判斷 D 進(jìn)程的 ASID 版本號(hào)和當(dāng)前的全局的 ASID 版本號(hào)相同,所以不需要為他分配 ASID(執(zhí)行快速路徑 switch_mm_fastpath 去設(shè)置 ttbrx_el1))。
情況 2 - 硬件 ASID 全部分配完 屬于策略(4):假設(shè)到達(dá) D 進(jìn)程時(shí),asid 已經(jīng)全部分配完(系統(tǒng)中有 255 個(gè)進(jìn)程都分配到了硬件 asid 號(hào)),這個(gè)時(shí)候新創(chuàng)建的進(jìn)程 E 被調(diào)度器選中,切換到 E,由于新創(chuàng)建的進(jìn)程的軟件 ASID 被初始化為 0,所以和當(dāng)前的全局的 ASID 版本號(hào)不同(不在同一批次),則這個(gè)時(shí)候會(huì)執(zhí)行 new_context 為進(jìn)程分配 ASID,但是由于沒(méi)有可以分配的 ASID,所以會(huì)將全局的 ASID 版本號(hào)加 1(發(fā)生 ASID 回繞),這個(gè)時(shí)候全局的 ASID 為 801,然后清空 asid_map, 置位 tlb_flush_pending 所有 bit 用于清空所有 cpu 的 tlb,然后再次去分配硬件 ASID 給 E 進(jìn)程,這個(gè)時(shí)候分配到了 1 給他(將 ASID 版本號(hào))。
情況 3 -ASID 版本號(hào)發(fā)生變化,進(jìn)程的硬件 ASID 可以再次使用 屬于策略(3):假設(shè)從 E 切換到了 B 進(jìn)程,而 B 進(jìn)程之前已經(jīng)在全局的 ASID 版本號(hào)為 800 的批次上分配了編號(hào)為 5 的硬件 ASID,但是 B 進(jìn)程的 ASID 版本號(hào) 800 和現(xiàn)在全局的 ASID 版本號(hào) 801 不相同,所有需要 new_context 為進(jìn)程分配 ASID,分配的時(shí)候發(fā)現(xiàn) asid_map 中編號(hào)為 5 沒(méi)有被置位,也就是沒(méi)有其他進(jìn)程分配了 5 這個(gè) ASID,所有可以繼續(xù)使用原來(lái)分配的硬件 ASID 5。
情況 4 – ASID 版本號(hào)發(fā)生變化,有其他進(jìn)程已經(jīng)分配了相同的硬件 ASID 屬于策略(2): 假設(shè)從 B 進(jìn)程切換到 A 進(jìn)程,而 B 進(jìn)程之前已經(jīng)在全局的 ASID 版本號(hào)為 800 的批次上分配了編號(hào)為 1 的硬件 ASID,但是 B 進(jìn)程的 ASID 版本號(hào) 800 和現(xiàn)在全局的 ASID 版本號(hào) 801 不相同,所有需要 new_context 為進(jìn)程分配 ASID,分配的時(shí)候發(fā)現(xiàn) asid_map 中編號(hào)為 1 已經(jīng)被置位,也就是其他進(jìn)程已經(jīng)分配了 1 這個(gè) ASID,需要從 asid_map 尋找下一個(gè)空閑的 ASID,則分配了新的 ASID 為 6。
假設(shè)從 A 到 E,由于 E 的 ASID 版本號(hào)和全局的 ASID 版本號(hào)(同一批次),和情況 1 相同,不需要分配 ASID。但是之前原來(lái)處于 800 這個(gè) ASID 版本號(hào)批次的進(jìn)程都需要重新分配 ASID,有的可以使用原來(lái)的硬件 ASID,有的重新分配硬件 ASID,但是都將 ASID 版本號(hào)修改為了現(xiàn)在全局的 ASID 版本號(hào) 801。但是,隨著硬件 ASID 的不斷分配,最終處于 801 這一批次的硬件 ASID 也會(huì)分配完,這個(gè)時(shí)候就是上面的情況 2,要情況所有 cpu 的 tlb。
我可以看到有了 ASID 機(jī)制之后,由于只有當(dāng)硬件 ASID 被分配完了(如被 255 個(gè)進(jìn)程使用),發(fā)生回繞的時(shí)候才會(huì)清空所有 cpu 的 tlb, 大大提高了系統(tǒng)的性能(沒(méi)有 ASID 機(jī)制的情況下每次進(jìn)程切換需要地址空間切換的時(shí)候都需要清空 tlb)。
4. 普通用戶(hù)進(jìn)程、普通用戶(hù)線(xiàn)程、內(nèi)核線(xiàn)程切換的差別
內(nèi)核地址空間切換的時(shí)候有一下原則:看的是進(jìn)程描述符的 mm_struct 結(jié)構(gòu),即是成員 mm:
1)如果 mm 為 NULL, 則表示即將切換的是內(nèi)核線(xiàn)程,不需要切換地址空間(所有任務(wù)共享內(nèi)核地址空間)。
2)內(nèi)核線(xiàn)程會(huì)借用前一個(gè)用戶(hù)進(jìn)程的 mm,賦值到自己的 active_mm(本身的 mm 為空),進(jìn)程切換的時(shí)候就會(huì)比較前一個(gè)進(jìn)程的 active_mm 和當(dāng)前進(jìn)程的 mm。
3)如果前一個(gè)任務(wù)的和即將切換的任務(wù),具有相同的 mm 成員,也就是共享地址空間的線(xiàn)程則也不需要切換地址空間。
– 所有的進(jìn)程線(xiàn)程之間進(jìn)行切換都需要切換處理器狀態(tài)。
– 對(duì)于普通的用戶(hù)進(jìn)程之間進(jìn)行切換需要切換地址空間。
– 同一個(gè)線(xiàn)程組中的線(xiàn)程之間切換不需要切換地址空間,因?yàn)樗麄児蚕硐嗤牡刂房臻g。
– 內(nèi)核線(xiàn)程在上下文切換的時(shí)候不需要切換地址空間,僅僅是借用上一個(gè)進(jìn)程 mm_struct 結(jié)構(gòu)。
有一下場(chǎng)景:
約定:我們將進(jìn)程 / 線(xiàn)程統(tǒng)稱(chēng)為任務(wù),其中 U 表示用戶(hù)任務(wù)(進(jìn)程 / 線(xiàn)程),K 表示內(nèi)核線(xiàn)程,帶有數(shù)字表示同一個(gè)線(xiàn)程組中的線(xiàn)程。
有以下任務(wù):Ua1 Ua2 Ub Uc Ka Kb (eg:Ua1 為用戶(hù)進(jìn)程, Ua2 為和 Ua1 在同一線(xiàn)程組的用戶(hù)進(jìn)程,Ub 普通的用戶(hù)進(jìn)程,Ka 普通的內(nèi)核線(xiàn)程)。
如果調(diào)度順序如下:
Uc – Ua1 – Ua2 – Ub – Ka – Kb – Ub
從 Uc – Ua1 由于是不同的進(jìn)程,需要切換地址空間。
從 Ua1 – Ua2 由于是相同線(xiàn)程組中的不同線(xiàn)程,共享地址空間,在切換到 Ua1 的時(shí)候已經(jīng)切換了地址空間,所有不需要切換地址空間。
從 Ua2 – Ub 由于是不同的進(jìn)程,需要切換地址空間。
從 Ub – Ka 由于切換到內(nèi)核線(xiàn)程,所以不需要切換地址空間。
從 Ka – Kb 倆內(nèi)核線(xiàn)程之前切換,不需要切換地址空間。
從 Kb – Ub 從內(nèi)核線(xiàn)程切換到用戶(hù)進(jìn)程,由于 Ka 和 Kb 都是借用 Ub 的 active_mm,而 Ub 的 active_mm 等于 Ub 的 mm, 所以這個(gè)時(shí)候 Kb 的 active_mm 和 Ub 的 mm 相同,所有也不會(huì)切換地址空間。
如下為多任務(wù)地址空間切換示例圖:
5. 進(jìn)程切換全景視圖
我們以下場(chǎng)景為例:
A,B 兩個(gè)進(jìn)程都是普通的用戶(hù)進(jìn)程,從進(jìn)程 A 切換到進(jìn)程 B, 簡(jiǎn)單起見(jiàn)我們?cè)谶@里不考慮其他的搶占時(shí)機(jī),我們假設(shè) A,B 進(jìn)程只是循環(huán)進(jìn)行一些基本的運(yùn)算操作,從來(lái)不調(diào)用任何系統(tǒng)調(diào)用,只考慮被時(shí)鐘中斷,返回用戶(hù)空間之前被搶占的情況。
下面給出進(jìn)程切換的全景視圖:
視圖中已經(jīng)講解很清楚,需要強(qiáng)調(diào) 3 個(gè)關(guān)鍵點(diǎn):
1. 發(fā)生中斷時(shí)的保存現(xiàn)場(chǎng),將發(fā)生中斷時(shí)的所有通用寄存器保存到進(jìn)程的內(nèi)核棧,使用 struct pt_regs 結(jié)構(gòu)。
2. 地址空間切換將進(jìn)程自己的頁(yè)全局目錄的基地址 pgd 保存在 ttbr0_le1 中,用于 mmu 的頁(yè)表遍歷的起始點(diǎn)。
3. 硬件上下文切換的時(shí)候,將此時(shí)的調(diào)用保存寄存器和 pc, sp 保存到 struct cpu_context 結(jié)構(gòu)中。做好了這幾個(gè)保存工作,當(dāng)進(jìn)程再次被調(diào)度回來(lái)的時(shí)候,通過(guò) cpu_context 中保存的 pc 回到了 cpu_switch_to 的下一條指令繼續(xù)執(zhí)行,而由于 cpu_context 中保存的 sp 導(dǎo)致當(dāng)前進(jìn)程回到自己的內(nèi)核棧,經(jīng)過(guò)一系列的內(nèi)核棧的出棧處理,最后將原來(lái)保存在 pt_regs 中的通用寄存器的值恢復(fù)到了通用寄存器,這樣進(jìn)程回到用戶(hù)空間就可以繼續(xù)沿著被中斷打斷的下一條指令開(kāi)始執(zhí)行,用戶(hù)棧也回到了被打斷之前的位置,而進(jìn)程訪(fǎng)問(wèn)的指令數(shù)據(jù)做地址轉(zhuǎn)化 (VA 到 PA) 也都是從自己的 pgd 開(kāi)始進(jìn)行,一切對(duì)用戶(hù)來(lái)說(shuō)就好像沒(méi)有發(fā)生一樣,簡(jiǎn)直天衣無(wú)縫。
感謝各位的閱讀,以上就是“Linux 內(nèi)核進(jìn)程上下文切換怎么理解”的內(nèi)容了,經(jīng)過(guò)本文的學(xué)習(xí)后,相信大家對(duì) Linux 內(nèi)核進(jìn)程上下文切換怎么理解這一問(wèn)題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是丸趣 TV,丸趣 TV 小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!