您好,登錄后才能下訂單哦!
這篇文章主要講解了“Linux內核進程上下文切換怎么理解”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“Linux內核進程上下文切換怎么理解”吧!
1.進程上下文的概念
進程上下文是進程執行活動全過程的靜態描述。我們把已執行過的進程指令和數據在相關寄存器與堆棧中的內容稱為進程上文,把正在執行的指令和數據在寄存器與堆棧中的內容稱為進程正文,把待執行的指令和數據在寄存器與堆棧中的內容稱為進程下文。
實際上linux內核中,進程上下文包括進程的虛擬地址空間和硬件上下文。
進程硬件上下文包含了當前cpu的一組寄存器的集合,arm64中使用task_struct結構的thread成員的cpu_context成員來描述,包括x19-x28,sp, pc等。
如下為硬件上下文存放示例圖:
2.上下文切換詳細過程
進程上下文切換主要涉及到兩部分主要過程:進程地址空間切換和處理器狀態切換。地址空間切換主要是針對用戶進程而言,而處理器狀態切換對應于所有的調度單位。下面我們分別看下這兩個過程:
__schedule // kernel/sched/core.c ->context_switch ->switch_mm_irqs_off //進程地址空間切換 ->switch_to //處理器狀態切換
2.1 進程地址空間切換
進程地址空間指的是進程所擁有的虛擬地址空間,而這個地址空間是假的,是linux內核通過數據結構來描述出來的,從而使得每一個進程都感覺到自己擁有整個內存的假象,cpu訪問的指令和數據最終會落實到實際的物理地址,對用進程而言通過缺頁異常來分配和建立頁表映射。進程地址空間內有進程運行的指令和數據,因此到調度器從其他進程重新切換到我的時候,為了保證當前進程訪問的虛擬地址是自己的必須切換地址空間。
實際上,進程地址空間使用mm_struct結構體來描述,這個結構體被嵌入到進程描述符(我們通常所說的進程控制塊PCB)task_struct中,mm_struct結構體將各個vma組織起來進行管理,其中有一個成員pgd至關重要,地址空間切換中最重要的是pgd的設置。
pgd中保存的是進程的頁全局目錄的虛擬地址(本文會涉及到頁表相關的一些概念,在此不是重點,不清楚的可以查閱相關資料,后期有機會會講解進程頁表),記住保存的是虛擬地址,那么pgd的值是何時被設置的呢?答案是fork的時候,如果是創建進程,需要分配設置mm_struct,其中會分配進程頁全局目錄所在的頁,然后將首地址賦值給pgd。
我們來看看進程地址空間究竟是如何切換的,結果會讓你大吃一驚(這里暫且不考慮asid機制,后面有機會會在其他文章中講解):
代碼路徑如下:
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,需要重新遍歷多級頁表,所以arm64在tlb表項中增加了非全局(nG)位區分內核和進程的頁表項,使用ASID區分不同進程的頁表項,來保證可以在切換地址空間的時候可以不刷tlb,后面會主要講解ASID技術。
還需要注意的是僅僅切換用戶地址空間,內核地址空間由于是共享的不需要切換,也就是為何切換到內核線程不需要也沒有地址空間的原因。
如下為進程地址空間切換示例圖:
2.2 處理器狀態(硬件上下文)切換
前面進行了地址空間切換,只是保證了進程訪問指令數據時訪問的是自己地址空間(當然上下文切換的時候處于內核空間,執行的是內核地址數據,當返回用戶空間的時候才有機會執行用戶空間指令數據**,地址空間切換為進程訪問自己用戶空間做好了準備**),但是進程執行的內核棧還是前一個進程的,當前執行流也還是前一個進程的,需要做切換。
arm64中切換代碼如下:
switch_to ->__switch_to ... //浮點寄存器等的切換 ->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 架構規定需要調用保存的寄存器,可以看到處理器狀態切換的時候將前一個進程(prev)的x19-x28,fp,sp,pc保存到了進程描述符的cpu_contex中,然后將即將執行的進程(next)描述符的cpu_contex的x19-x28,fp,sp,pc恢復到相應寄存器中,而且將next進程的進程描述符task_struct地址存放在sp_el0中,用于通過current找到當前進程,這樣就完成了處理器的狀態切換。
實際上,處理器狀態切換就是將前一個進程的sp,pc等寄存器的值保存到一塊內存上,然后將即將執行的進程的sp,pc等寄存器的值從另一塊內存中恢復到相應寄存器中,恢復sp完成了進程內核棧的切換,恢復pc完成了指令執行流的切換。其中保存/恢復所用到的那塊內存需要被進程所標識,這塊內存這就是cpu_contex這個結構的位置(進程切換都是在內核空間完成)。
由于用戶空間通過異常/中斷進入內核空間的時候都需要保存現場,也就是保存發生異常/中斷時的所有通用寄存器的值,內核會把“現場”保存到每個進程特有的進程內核棧中,并用pt_regs結構來描述,當異常/中斷處理完成之后會返回用戶空間,返回之前會恢復之前保存的“現場”,用戶程序繼續執行。
所以當進程切換的時候,當前進程被時鐘中斷打斷,將發生中斷時的現場保存到進程內核棧(如:sp, lr等),然后會切換到下一個進程,當再次回切換回來的時候,返回用戶空間的時候會恢復之前的現場,進程就可以繼續執行(執行之前被中斷打斷的下一條指令,繼續使用自己用戶態sp),這對于用戶進程來說是透明的。
如下為硬件上下文切換示例圖:
3.ASID機制
前面講過,進程切換的時候,由于tlb中存放的可能是其他進程的tlb表項,所有才需要在進程切換的時候進行tlb的清空工作(清空即是使得所有的tlb表項無效,地址轉換需要遍歷多級頁表,找到頁表項,然后重新加載頁表項到tlb),有了ASID機制之后,命中tlb表項,由虛擬地址和ASID共同決定(當然還有nG位),可以減小進程切換中tlb被清空的機會。
下面我們講解ASID機制,ASID(Address Space Identifer 地址空間標識符),用于區別不同進程的頁表項,arm64中,可以選擇兩種ASID長度8位或者16位,這里以8位來講解。
如果ASID長度為8位,那么ASID有256個值,但是由于0是保留的,所有可以分配的ASID范圍就為1-255,那么可以標識255個進程,當超出255個進程的時候,會出現兩個進程的ASID相同的情況,因此內核使用了ASID版本號。
內核中處理如下(參考arch/arm64/mm/context.c):
1)內核為每個進程分配一個64位的軟件ASID,其中低8位為硬件ASID,高56位為ASID版本號,這個軟件ASID存放放在進程的mm_struct結構的context結構的id中,進程創建的時候會初始化為0。
2)內核中有一個64位的全局變量asid_generation,同樣它的高56位為ASID版本號,用于標識當前ASID分配的批次。
3)當進程調度,由prev進程切換到next進程的時候,如果不是內核線程則進行地址空間切換調用check_and_switch_context,此函數會判斷next進程的ASID版本號是否和全局的ASID版本號相同(是否處于同一批次),如果相同則不需要為next進程分配ASID,不相同則需要分配ASID。
4)內核使用asid_map位圖來管理硬件ASID的分配,asid_bits記錄使用的ASID的長度,每處理器變量active_asids保存當前分配的硬件ASID,每處理器變量reserved_asids存放保留的ASID,tlb_flush_pending位圖記錄需要清空tlb的cpu集合。
硬件ASID分配策略如下:
(1)如果進程的ASID版本號和當前全局的ASID版本號相同(同批次情況),則不需要重新分配ASID。
(2)如果進程的ASID版本號和當前全局的ASID版本號不相同(不同批次情況),且進程原本的硬件ASID已經被分配,則重新分配新的硬件ASID,并將當前全局的ASID版本號組合新分配的硬件ASID寫到進程的軟件ASID中。
(3)如果進程的ASID版本號和當前全局的ASID版本號不相同(不同批次情況),且進程原本的硬件ASID還沒有被分配,則不需要重新分配新的硬件ASID,只需要更新進程軟件ASID版本號,并將當前全局的ASID版本號組合進程原來的硬件ASID寫到進程的軟件ASID中。
(4)如果進程的ASID版本號和當前全局的ASID版本號不相同(不同批次情況),需要分配硬件ASID時,發現硬件ASID已經被其他進程分配完(asid_map位圖中查找,發現位圖全1),則這個時候需要遞增全局的ASID版本號, 清空所有cpu的tlb, 清空asid_map位圖,然后分配硬件ASID,并將當前全局的ASID版本號組合新分配的硬件ASID寫到進程的軟件ASID中。
下面我們以實例來看ASID的分配過程:
如下圖:
我們假設圖中從A進程到D進程,有255個進程,剛好分配完了asid, ,從A到D的切換過程中使用的都是同一批次的asid版本號。
則這個過程中,有進程會創建的時候被切換到,假設不超出255個進程,在切換過程中會為新進程分配硬件的ASID,分配完后下次切換到他時由于他的ASID版本號和當前的全局的ASID版本號相同,所以不需要再次分配ASID,當然也不需要清空tlb。
注:這里說的ASID即為硬件ASID區別于ASID版本號。
情況1-ASID版本號不變 屬于策略(1):從C進程到D進程切換,內核判斷D進程的ASID版本號和當前的全局的ASID版本號相同,所以不需要為他分配ASID(執行快速路徑switch_mm_fastpath去設置ttbrx_el1))。
情況2 -硬件ASID全部分配完 屬于策略(4):假設到達D進程時,asid已經全部分配完(系統中有255個進程都分配到了硬件asid號),這個時候新創建的進程E被調度器選中,切換到E,由于新創建的進程的軟件ASID被初始化為0,所以和當前的全局的ASID版本號不同(不在同一批次),則這個時候會執行new_context為進程分配ASID,但是由于沒有可以分配的ASID,所以會將全局的ASID版本號加1(發生ASID回繞),這個時候全局的ASID為801,然后清空asid_map,置位tlb_flush_pending所有bit用于清空所有cpu的tlb,然后再次去分配硬件ASID給E進程,這個時候分配到了1給他(將ASID版本號)。
情況3-ASID版本號發生變化,進程的硬件ASID可以再次使用 屬于策略(3):假設從E切換到了B進程,而B進程之前已經在全局的ASID版本號為800的批次上分配了編號為5的硬件ASID,但是B進程的ASID版本號800和現在全局的ASID版本號801不相同,所有需要new_context為進程分配ASID,分配的時候發現asid_map中編號為5沒有被置位,也就是沒有其他進程分配了5這個ASID,所有可以繼續使用原來分配的硬件ASID 5。
情況4 - ASID版本號發生變化,有其他進程已經分配了相同的硬件ASID 屬于策略(2): 假設從B進程切換到A進程,而B進程之前已經在全局的ASID版本號為800的批次上分配了編號為1的硬件ASID,但是B進程的ASID版本號800和現在全局的ASID版本號801不相同,所有需要new_context為進程分配ASID,分配的時候發現asid_map中編號為1已經被置位,也就是其他進程已經分配了1這個ASID,需要從asid_map尋找下一個空閑的ASID,則分配了新的ASID為6。
假設從A到E,由于E的ASID版本號和全局的ASID版本號(同一批次),和情況1相同,不需要分配ASID。但是之前原來處于800這個ASID版本號批次的進程都需要重新分配ASID,有的可以使用原來的硬件ASID,有的重新分配硬件ASID,但是都將ASID版本號修改為了現在全局的ASID版本號801。但是,隨著硬件ASID的不斷分配,最終處于801這一批次的硬件ASID也會分配完,這個時候就是上面的情況2,要情況所有cpu的tlb。
我可以看到有了ASID機制之后,由于只有當硬件ASID被分配完了(如被255個進程使用),發生回繞的時候才會清空所有cpu的tlb,大大提高了系統的性能(沒有ASID機制的情況下每次進程切換需要地址空間切換的時候都需要清空tlb)。
4. 普通用戶進程、普通用戶線程、內核線程切換的差別
內核地址空間切換的時候有一下原則:看的是進程描述符的mm_struct結構,即是成員mm:
1)如果mm為NULL,則表示即將切換的是內核線程,不需要切換地址空間(所有任務共享內核地址空間)。
2)內核線程會借用前一個用戶進程的mm,賦值到自己的active_mm(本身的mm為空),進程切換的時候就會比較前一個進程的active_mm和當前進程的mm。
3)如果前一個任務的和即將切換的任務,具有相同的mm成員,也就是共享地址空間的線程則也不需要切換地址空間。
->所有的進程線程之間進行切換都需要切換處理器狀態。
->對于普通的用戶進程之間進行切換需要切換地址空間。
->同一個線程組中的線程之間切換不需要切換地址空間,因為他們共享相同的地址空間。
-> 內核線程在上下文切換的時候不需要切換地址空間,僅僅是借用上一個進程mm_struct結構。
有一下場景:
約定:我們將進程/線程統稱為任務,其中U表示用戶任務(進程/線程),K表示內核線程,帶有數字表示同一個線程組中的線程。
有以下任務:Ua1 Ua2 Ub Uc Ka Kb (eg:Ua1為用戶進程, Ua2為和Ua1在同一線程組的用戶進程,Ub普通的用戶進程,Ka普通的內核線程 )。
如果調度順序如下:
Uc -> Ua1 -> Ua2 -> Ub -> Ka -> Kb -> Ub
從Uc -> Ua1 由于是不同的進程,需要切換地址空間。
從 Ua1 -> Ua2 由于是相同線程組中的不同線程,共享地址空間,在切換到Ua1的時候已經切換了地址空間,所有不需要切換地址空間。
從 Ua2 -> Ub 由于是不同的進程,需要切換地址空間。
從 Ub -> Ka 由于切換到內核線程,所以不需要切換地址空間。
從Ka -> Kb 倆內核線程之前切換,不需要切換地址空間。
從Kb -> Ub 從內核線程切換到用戶進程,由于Ka和Kb都是借用Ub的active_mm,而Ub的active_mm 等于Ub的mm,所以這個時候 Kb的active_mm和 Ub的mm相同,所有也不會切換地址空間。
如下為多任務地址空間切換示例圖:
5. 進程切換全景視圖
我們以下場景為例:
A,B兩個進程都是普通的用戶進程,從進程A切換到進程B,簡單起見我們在這里不考慮其他的搶占時機,我們假設A,B進程只是循環進行一些基本的運算操作,從來不調用任何系統調用,只考慮被時鐘中斷,返回用戶空間之前被搶占的情況。
下面給出進程切換的全景視圖:
視圖中已經講解很清楚,需要強調3個關鍵點:
1.發生中斷時的保存現場,將發生中斷時的所有通用寄存器保存到進程的內核棧,使用struct pt_regs結構。
2.地址空間切換將進程自己的頁全局目錄的基地址pgd保存在ttbr0_le1中,用于mmu的頁表遍歷的起始點。
3.硬件上下文切換的時候,將此時的調用保存寄存器和pc, sp保存到struct cpu_context結構中。做好了這幾個保存工作,當進程再次被調度回來的時候,通過cpu_context中保存的pc回到了cpu_switch_to的下一條指令繼續執行,而由于cpu_context中保存的sp導致當前進程回到自己的內核棧,經過一系列的內核棧的出棧處理,最后將原來保存在pt_regs中的通用寄存器的值恢復到了通用寄存器,這樣進程回到用戶空間就可以繼續沿著被中斷打斷的下一條指令開始執行,用戶棧也回到了被打斷之前的位置,而進程訪問的指令數據做地址轉化(VA到PA)也都是從自己的pgd開始進行,一切對用戶來說就好像沒有發生一樣,簡直天衣無縫。
感謝各位的閱讀,以上就是“Linux內核進程上下文切換怎么理解”的內容了,經過本文的學習后,相信大家對Linux內核進程上下文切換怎么理解這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。