深入理解Linux內(nèi)核進(jìn)程上下文切換
我都知道操作系統(tǒng)的一個(gè)重要功能就是進(jìn)行進(jìn)程管理,而進(jìn)程管理就是在合適的時(shí)機(jī)選擇合適的進(jìn)程來(lái)執(zhí)行,在單個(gè)cpu運(yùn)行隊(duì)列上各個(gè)進(jìn)程宏觀并行微觀串行執(zhí)行,多個(gè)cpu運(yùn)行隊(duì)列上的各個(gè)進(jìn)程之間完全的并行執(zhí)行。進(jìn)程管理是個(gè)復(fù)雜的過(guò)程,例如進(jìn)程的描述、創(chuàng)建和銷(xiāo)毀、生命周期管理、進(jìn)程切換、進(jìn)程搶占、調(diào)度策略、負(fù)載均衡等等。本文主要關(guān)注進(jìn)程管理的一個(gè)切入點(diǎn),那就是進(jìn)程的上下文切換,來(lái)理解linux內(nèi)核是如何進(jìn)程進(jìn)程上下文切換的,從而揭開(kāi)上下文切換的神秘面紗。
(注意:本文以linux-5.0內(nèi)核源碼講解,采用arm64架構(gòu))
本文目錄:
1.進(jìn)程上下文的概念
2.上下文切換詳細(xì)過(guò)程
2.1 進(jìn)程地址空間切換
2.2 處理器狀態(tài)(硬件上下文)切換
3.ASID機(jī)制
4.普通用戶(hù)進(jìn)程、普通用戶(hù)線程、內(nèi)核線程切換的差別
5.進(jìn)程切換全景視圖
6.總結(jié)
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訪問(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)程訪問(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)訪問(wèn)用戶(hù)空間地址的時(shí)候mmu會(huì)通過(guò)這個(gè)寄存器來(lái)做遍歷頁(yè)表獲得物理地址(ttbr1_el1是內(nèi)核空間的頁(yè)表基址寄存器,訪問(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)程想要訪問(wèn)一個(gè)用戶(hù)空間虛擬地址,cpu的mmu所做的工作,就是從頁(yè)表基址寄存器拿到頁(yè)全局目錄的物理基地址,然后和虛擬地址配合來(lái)查查找頁(yè)表,最終找到物理地址進(jìn)行訪問(wèn)(當(dāng)然如果tlb命中就不需要遍歷頁(yè)表),每次用戶(hù)虛擬地址訪問(wèn)的時(shí)候(內(nèi)核空間共享不考慮),由于頁(yè)表基地址寄存器內(nèi)存放的是當(dāng)前執(zhí)行進(jìn)程的頁(yè)全局目錄的物理地址,所以訪問(wèn)自己的一套頁(yè)表,拿到的是屬于自己的物理地址(實(shí)際上,進(jìn)程是訪問(wèn)虛擬地址空間的指令數(shù)據(jù)的時(shí)候不斷發(fā)生缺頁(yè)異常,然后缺頁(yè)異常處理程序?yàn)檫M(jìn)程分配實(shí)際的物理頁(yè),然后將頁(yè)幀號(hào)和頁(yè)表屬性填入自己的頁(yè)表?xiàng)l目中),就不會(huì)訪問(wèn)其他進(jìn)程的指令和數(shù)據(jù),這也是為何多個(gè)進(jìn)程可以訪問(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)核線程不需要也沒(méi)有地址空間的原因。
如下為進(jìn)程地址空間切換示例圖:
2.2 處理器狀態(tài)(硬件上下文)切換
前面進(jìn)行了地址空間切換,只是保證了進(jìn)程訪問(wèn)指令數(shù)據(jù)時(shí)訪問(wèn)的是自己地址空間(當(dāng)然上下文切換的時(shí)候處于內(nèi)核空間,執(zhí)行的是內(nèi)核地址數(shù)據(jù),當(dāng)返回用戶(hù)空間的時(shí)候才有機(jī)會(huì)執(zhí)行用戶(hù)空間指令數(shù)據(jù)**,地址空間切換為進(jìn)程訪問(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 AArch64. 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)核線程則進(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(niǎo)進(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(niǎo)進(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ù)線程、內(nèi)核線程切換的差別
內(nèi)核地址空間切換的時(shí)候有一下原則:看的是進(jìn)程描述符的mm_struct結(jié)構(gòu),即是成員mm:
1)如果mm為NULL,則表示即將切換的是內(nèi)核線程,不需要切換地址空間(所有任務(wù)共享內(nèi)核地址空間)。
2)內(nèi)核線程會(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成員,也就是共享地址空間的線程則也不需要切換地址空間。
->所有的進(jìn)程線程之間進(jìn)行切換都需要切換處理器狀態(tài)。
->對(duì)于普通的用戶(hù)進(jìn)程之間進(jìn)行切換需要切換地址空間。
->同一個(gè)線程組中的線程之間切換不需要切換地址空間,因?yàn)樗麄児蚕硐嗤牡刂房臻g。
-> 內(nèi)核線程在上下文切換的時(shí)候不需要切換地址空間,僅僅是借用上一個(gè)進(jìn)程mm_struct結(jié)構(gòu)。
有一下場(chǎng)景:
約定:我們將進(jìn)程/線程統(tǒng)稱(chēng)為任務(wù),其中U表示用戶(hù)任務(wù)(進(jìn)程/線程),K表示內(nèi)核線程,帶有數(shù)字表示同一個(gè)線程組中的線程。
有以下任務(wù):Ua1 Ua2 Ub Uc Ka Kb (eg:Ua1為用戶(hù)進(jìn)程, Ua2為和Ua1在同一線程組的用戶(hù)進(jìn)程,Ub普通的用戶(hù)進(jìn)程,Ka普通的內(nèi)核線程 )。
如果調(diào)度順序如下:
Uc -> Ua1 -> Ua2 -> Ub -> Ka -> Kb -> Ub
從Uc -> Ua1 由于是不同的進(jìn)程,需要切換地址空間。
從 Ua1 -> Ua2 由于是相同線程組中的不同線程,共享地址空間,在切換到Ua1的時(shí)候已經(jīng)切換了地址空間,所有不需要切換地址空間。
從 Ua2 -> Ub 由于是不同的進(jìn)程,需要切換地址空間。
從 Ub -> Ka 由于切換到內(nèi)核線程,所以不需要切換地址空間。
從Ka -> Kb 倆內(nèi)核線程之前切換,不需要切換地址空間。
從Kb -> Ub 從內(nèi)核線程切換到用戶(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)程訪問(wèn)的指令數(shù)據(jù)做地址轉(zhuǎn)化(VA到PA)也都是從自己的pgd開(kāi)始進(jìn)行,一切對(duì)用戶(hù)來(lái)說(shuō)就好像沒(méi)有發(fā)生一樣,簡(jiǎn)直天衣無(wú)縫。
6. 總結(jié)
進(jìn)程管理中最重要的一步要進(jìn)行進(jìn)程上下文切換,其中主要有兩大步驟:地址空間切換和處理器狀態(tài)切換(硬件上下文切換),前者保證了進(jìn)程回到用戶(hù)空間之后能夠訪問(wèn)到自己的指令和數(shù)據(jù)(其中包括減小tlb清空的ASID機(jī)制),后者保證了進(jìn)程內(nèi)核棧和執(zhí)行流的切換,會(huì)將當(dāng)前進(jìn)程的硬件上下文保存在進(jìn)程所管理的一塊內(nèi)存,然后將即將執(zhí)行的進(jìn)程的硬件上下文從內(nèi)存中恢復(fù)到寄存器,有了這兩步的切換過(guò)程保證了進(jìn)程運(yùn)行的有條不紊,當(dāng)然切換的過(guò)程是在內(nèi)核空間完成,這對(duì)于進(jìn)程來(lái)說(shuō)是透明的。
作者簡(jiǎn)介
韓傳華,就職于南京大魚(yú)半導(dǎo)體有限公司,主要從事linux相關(guān)系統(tǒng)軟件開(kāi)發(fā)工作,負(fù)責(zé)Soc芯片BringUp及系統(tǒng)軟件開(kāi)發(fā),樂(lè)于分享喜歡學(xué)習(xí),喜歡專(zhuān)研Linux內(nèi)核源代碼。
本文轉(zhuǎn)載自微信公眾號(hào)「Linux閱碼場(chǎng)」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系Linux閱碼場(chǎng)公眾號(hào)。