深入Linux內核(進程篇)—進程切換之ARM體系架構【轉】

轉自:https://blog.csdn.net/liyuewuwunaile/article/details/106773630

進程切換
一、context_switch
二、switch_mm
2.1 刷新I-CACHE
2.2 ASID和TLB
2.3 頁錶轉換基址切換
三、switch_to
進程切換由兩部分組成:

切換頁全局目錄安裝一個新的地址空間;
切換內核態堆棧及硬件上下文。
一、context_switch
Linux內核中由context_switch實現了上述兩部分內容。

調用switch_mm完成用戶空間切換;
調用switch_to完成內核棧及寄存器切換。
具體實現流程:

通過進程描述符next->mm是否爲空判斷當前進程是否是內核線程,因爲內核線程的內存描述符mm_struct
*mm總是爲空,詳見《深入Linux內核(進程篇)—進程描述》內存描述一節。
如果是內核線程則借用prev進程的active_mm,對於用戶進程,active_mm == mm,對於內核線程,mm = NULL,active_mm = prev->active_mm。
如果prev->mm不爲空,則說明prev是用戶進程,調用mmgrab增加mm->mm_count引用計數。
對於內核線程,會啓動懶惰TLB模式。懶惰TLB模式是爲了減少無用的TLB刷新,關於TLB的內容詳見《深入Linux內核(內存篇)–頁表映射》TLB一節。enter_lazy_tlb與體系結構相關。
如果是用戶進程則調用switch_mm_irqs_off完成用戶地址空間切換,switch_mm_irqs_off(或switch_mm)與體系結構相關。
調用switch_to完成內核態堆棧及硬件上下文切換,switch_to與體系結構相關。
switch_to執行完成後,next進程獲得CPU使用權,prev進程進入睡眠狀態。
調用finish_task_switch,如果prev是內核線程,則調用mmdrop減少內存描述符引用計數。如果引用計數爲0,則釋放與頁表相關的所有描述符和虛擬內存。
/*
* context_switch - switch to the new MM and the new thread's register state.
*/
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next, struct rq_flags *rf)
{
/* 進程切換的準備工作 */
prepare_task_switch(rq, prev, next);

/*
* For paravirt, this is coupled with an exit in switch_to to
* combine the page table reload and the switch backend into
* one hypercall.
*/
arch_start_context_switch(prev);

/*
* kernel -> kernel lazy + transfer active
* user -> kernel lazy + mmgrab() active
*
* kernel -> user switch + mmdrop() active
* user -> user switch
*/
if (!next->mm) { // to kernel
enter_lazy_tlb(prev->active_mm, next);

next->active_mm = prev->active_mm;
if (prev->mm) // from user
mmgrab(prev->active_mm);
else
prev->active_mm = NULL;
} else { // to user
membarrier_switch_mm(rq, prev->active_mm, next->mm);
/*
* sys_membarrier() requires an smp_mb() between setting
* rq->curr / membarrier_switch_mm() and returning to userspace.
*
* The below provides this either through switch_mm(), or in
* case 'prev->active_mm == next->mm' through
* finish_task_switch()'s mmdrop().
*/
/* 調用switch_mm_irqs_off完成用戶地址空間切換 */
switch_mm_irqs_off(prev->active_mm, next->mm, next);

if (!prev->mm) { // from kernel
/* will mmdrop() in finish_task_switch(). */
rq->prev_mm = prev->active_mm;
prev->active_mm = NULL;
}
}

rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);

prepare_lock_switch(rq, next, rf);

/* Here we just switch the register state and the stack. */
/* 調用switch_to完成內核態堆棧及硬件上下文切換 */
switch_to(prev, next, prev);
barrier();

return finish_task_switch(prev);
}

二、switch_mm
對於用戶進程需要完成用戶空間的切換,switch_mm函數完成了這個任務。switch_mm是與體系架構相關的函數。下面以ARM體系架構說明用戶空間的切換過程。
Linux5.6.4內核調用switch_mm_irqs_off切換用戶進程空間,對於沒有定義該函數的架構,則調用的是switch_mm。X86體系架構定義了switch_mm_irqs_off函數,ARM體系架構沒有定義。

#ifndef switch_mm_irqs_off
# define switch_mm_irqs_off switch_mm
#endif

本文只關心ARM體系架構。ARM進程地址空間的切換實際是設置頁表基址寄存器TTBR0的過程,對於每個進程擁有系統全部的虛擬地址空間,但是其並沒有佔用所以的物理地址,物理地址的訪問需要頁錶轉換完成,頁錶轉換的基址存放在頁表基址寄存器TTBR0中,每個進程都有一套自己的映射頁表存放在物理內存(實際最初並不是所以的頁表都存放到內存裏,而是發生缺頁異常時纔將頁表寫入物理內存),TTBR0指示了進程PGD頁表基址,PGD指示了PTE頁表基址,PTE指示了物理地址PA。每個進程的PGD不同,因而不同進程虛擬內存對於的物理地址就隔離開了。進程切換switch_mm實質上就是完成TTBR0寄存器的改寫。

 

ARMv7體系架構switch_mm實現如下。由上圖分析可知,switch_mm函數實質是將新進程的頁表基址設置到也目錄表基地址寄存器中,對於ARMv7即協處理器cp15的TTBR0寄存器。

/*
* This is the actual mm switch as far as the scheduler
* is concerned. No registers are touched. We avoid
* calling the CPU specific function when the mm hasn't
* actually changed.
*/
static inline void
switch_mm(struct mm_struct *prev, struct mm_struct *next,
struct task_struct *tsk)
{
#ifdef CONFIG_MMU
unsigned int cpu = smp_processor_id();

/*
* __sync_icache_dcache doesn't broadcast the I-cache invalidation,
* so check for possible thread migration and invalidate the I-cache
* if we're new to this CPU.
*/
if (cache_ops_need_broadcast() &&
!cpumask_empty(mm_cpumask(next)) &&
!cpumask_test_cpu(cpu, mm_cpumask(next)))
__flush_icache_all(); /* 刷新CPU Core所有I-Cache */

/* 將當前CPU設置到next進程的cpumask位圖 */
if (!cpumask_test_and_set_cpu(cpu, mm_cpumask(next)) || prev != next) {
/* 處理TLB及切換進程頁表映射地址TTBR0 */
check_and_switch_context(next, tsk);
if (cache_is_vivt())
cpumask_clear_cpu(cpu, mm_cpumask(prev));
}
#endif
}

2.1 刷新I-CACHE
如果next進程發生遷移,在一個新的CPU上執行,則需要flush I-Cache(Instructions Cache)。如下圖所示,對於ARM SMP架構來說每個core都有獨立的I-Cache和D-Cache(哈佛結構L1 Cache),因而新進程第一次運行到某Core時需要將I-Cache內容全部刷新。

 


__flush_icache_all函數實現了I-Cache刷新,flush I-Cache是通過訪問協處理器cp15的c7寄存器實現的。

/* Invalidate I-cache inner shareable */
/* 將cp15協處理器c7寄存器ICIALLUIS */
#define __flush_icache_all_v7_smp() \
asm("mcr p15, 0, %0, c7, c1, 0" \
: : "r" (0));
static inline void __flush_icache_all(void)
{
__flush_icache_preferred();
dsb(ishst);
}

CP15協處理器保護c0-c15共16個寄存器,寄存器32位的組織形式如下:


C R n , o p c 1 , C R m , o p c 2 {CRn, opc1, CRm, opc2}
CRn,opc1,CRm,opc2

對於彙編語句“mcr p15, 0, %0, c7, c1, 0”指示四個操作數結果如下:

 


CRn:第一個協處理器寄存器c7;
opc1:協處理器操作碼0;
CRm:第二個協處理器寄存器c1;
opc2:協處理器操作碼0。
因而對應ICIALLUIS (Invalidate all instruction caches Inner Shareable to PoU)寄存器。

 


2.2 ASID和TLB
check_and_switch_context完成了進程地址空間的切換,這包括兩部分內容:

ASID和TLB的處理;
TTBR處理。
本節關注switch_mm中關於ASID和TLB的處理。
ASID即Address Space ID,TLB即Translation Lookaside Buffer。
MMU在做Table Walk時,需要訪問物理內存中的頁表映射,每一級頁表映射都需要訪問一次內存,而內存的訪問對性能影響很大,因而效率很低。TLB是用於緩存MMU地址轉換結果的cache,顯然訪問cache找到物理地址比訪問內存找物理地址快的多,因而TLB加快內存的訪問效率。
ARMv7架構TLB結構如下圖所示,TLB entry中緩存了VA(虛擬地址),PA(物理地址),Attr(cache策略,訪問權限等屬性)和ASID(地址空間ID)。

 

VA和PA很好理解,即物理地址和虛擬地址映射關係。Attr用來指示TLB entry屬性。ASID用來幹甚?
TLB緩存了地址映射關係,不同進程擁有不同的地址映射頁表,因而進程切換時,TLB緩存的前一個進程的地址映射關係不能用於新進程,一個簡單的辦法是將TLB entry全部刷新,這導致TLB使用效率大打折扣,A和B兩個進程相互切換時,每次切換後都將面對一個空白的TLB,TLB miss大大增加,顯然這種方法不夠完美。
ASID指示了每個TLB entry所屬的進程,這樣可以保證不同進程之間的TLB entry不會互相干擾,因而避免了切換進程時將TLB刷新的問題。所以ASID作用避免了進程切換時TLB的頻繁刷新。

實際上,ARM TLB包含了Global和process-specific表項。

Global類型TLB entry:用於內核空間地址轉換,內核空間爲所以進程所共有,因而進程切換時,內核映射關係無需變化,所以其TLB entry也不用變。內核的頁表基址寄存器是TTBR1,進程切換時頁表不變的。
process-specific類型TLB entry:用戶進程獨立地址空間映射關係。即ASID用於隔離不同進程的TLB entry。
區分Global和process-specific表項則是根據PTE entry的bit11(nG位)。nG位爲1時,則表示TLB entry屬於進程。

 

check_and_switch_context函數前面部分主要實現了ASID相關的內容。

將TTBR1的內容設置到TTBR0。pgd和ASID的更新不能原子的完成,因而避免錯誤的映射,先將TTBR0設置成TTBR1;
從mm->context.id原子的獲取ASID;
asid_generation記錄ASID溢出,mm->context.id低8位記錄ASID,高24位記錄了ASID溢出次數,如果沒有發生ASID溢出則直接調用cpu_switch_mm切換TTBR0。
如果發生ASID溢出則需要爲進程重新分配ASID,並刷新TLB。
void check_and_switch_context(struct mm_struct *mm, struct task_struct *tsk)
{
unsigned long flags;
unsigned int cpu = smp_processor_id();
u64 asid;

if (unlikely(mm->context.vmalloc_seq != init_mm.context.vmalloc_seq))
__check_vmalloc_seq(mm);

/*
* We cannot update the pgd and the ASID atomicly with classic
* MMU, so switch exclusively to global mappings to avoid
* speculative page table walking with the wrong TTBR.
*/
cpu_set_reserved_ttbr0();/* 將TTBR1的內容設置到TTBR0 */

asid = atomic64_read(&mm->context.id);/* 獲取進程ASID */
/* ASID沒有發生溢出,不用關係TLB,直接跳到cpu_switch_mm切換TTBR0即可 */
if (!((asid ^ atomic64_read(&asid_generation)) >> ASID_BITS)
&& atomic64_xchg(&per_cpu(active_asids, cpu), asid))
goto switch_mm_fastpath;

raw_spin_lock_irqsave(&cpu_asid_lock, flags);
/* Check that our ASID belongs to the current generation. */
/* ASID發生溢出,調用new_context爲進程重新分配ASID,並記錄到mm->context.id中 */
asid = atomic64_read(&mm->context.id);
if ((asid ^ atomic64_read(&asid_generation)) >> ASID_BITS) {
asid = new_context(mm, cpu);
atomic64_set(&mm->context.id, asid);
}
/* ASID發生溢出,刷新TLB */
if (cpumask_test_and_clear_cpu(cpu, &tlb_flush_pending)) {
local_flush_bp_all(); /* 指令cache刷新 */
local_flush_tlb_all(); /* TLB刷新 */
}

atomic64_set(&per_cpu(active_asids, cpu), asid);
cpumask_set_cpu(cpu, mm_cpumask(mm));
raw_spin_unlock_irqrestore(&cpu_asid_lock, flags);

switch_mm_fastpath:
cpu_switch_mm(mm->pgd, mm); /* 頁表基址寄存器TTBR0切換 */
}


ASID爲什麼只有8bit,這是由 CONTEXTIDR(Context ID Register)寄存器決定的。cpu_switch_mm除了設置TTBR0寄存器外,還會設置CONTEXTIDR寄存器,3.3章節也會講到該寄存器。
如下圖所示,未開啓LAPE功能時,CONTEXTIDR的[7:0]是ASID,因而ASID只有8bit,256個ASID分配完後,需要重新分配。

 

local_flush_tlb_all完成TLB刷新。

static inline void local_flush_tlb_all(void)
{
const int zero = 0;
const unsigned int __tlb_flag = __cpu_tlb_flags;

if (tlb_flag(TLB_WB))
dsb(nshst);

__local_flush_tlb_all();
tlb_op(TLB_V7_UIS_FULL, "c8, c7, 0", zero);

if (tlb_flag(TLB_BARRIER)) {
dsb(nsh);
isb();
}
}

tlb_op操作使用協處理器指令MCR操作CP15的寄存器。
“c8, c7, 0” 指示協處理器指令。根據3.1節中關於協處理器指令的描述,可以知道。

CRn:第一個協處理器寄存器c8;
opc1:協處理器操作碼0;
CRm:第二個協處理器寄存器c7;
opc2:協處理器操作碼1。
因而對應TLBIALL(invalidate unified TLB)寄存器,即將TLB entry全部刷新。

2.3 頁錶轉換基址切換
進程切換需要切換進程地址空間,每個進程都擁有全部的虛擬地址空間,而物理地址空間是隔離的,操作系統能夠實現這種內存策略,依靠的是芯片級的地址轉換功能,也就是MMU(Memory Management Unit)。MMU完成了虛擬地址到物理地址的轉換工作,使得操作系統可以通過虛擬地址訪問到物理地址空間的真是數據。
對於ARM體系架構下圖是其MMU及內存層次的基本框圖。

 

MMU包含Table Walk Unit和TLB(Translation Lookaside Buffer),其中Table Walk Unit即處理虛擬地址到物理地址的轉換單元,而TLB用於緩存地址轉換結果,TLB實質上是Cache,與Cache的區別在於它專門用來存儲地址轉換結果。
ARMv7採用二級頁表映射,下圖是虛擬地址轉換到物理地址的頁表映射過程,這個過程是由MMU完成的。
TTBRx(Translation Table Base Register x)即頁錶轉換基址寄存器,ARMv7提供了TTBR0和TTBR1兩個寄存器,Linux分別將其應用於內核態和用戶態。而進程地址空間切換實質就是將TTBR0寄存器中***Translation Table Base 0 Address修改爲當前進程的PGD(頁全局目錄)。
MMU通過TTBRx和虛擬地址中的PGD index找到 First-level descriptor,First-level descriptor記錄了二級頁表基址(即PTE),結合虛擬地址的PTE index即找到 * Second-level descriptor, Second-level descriptor記錄了物理地址[31:12],物理地址[31:12]結合虛擬地址的VA[11:0]即得到物理地址。

 

ARMv7地址空間切換由cpu_switch_mm完成。

void check_and_switch_context(struct mm_struct *mm, struct task_struct *tsk)
{
…………
switch_mm_fastpath:
cpu_switch_mm(mm->pgd, mm);
}


cpu_switch_mm調用cpu_do_switch_mm完成進程地址空間切換。

#define cpu_switch_mm(pgd,mm) cpu_do_switch_mm(virt_to_phys(pgd),mm)


cpu_do_switch_mm最終調用的彙編代碼cpu_v7_switch_mm。

ENTRY(cpu_v7_switch_mm)
#ifdef CONFIG_MMU
@R1寄存器即APCS定義的第二個入參,即next進程的內存描述符mm
mmid r1, r1 @ get mm->context.id
ALT_SMP(orr r0, r0, #TTB_FLAGS_SMP)
ALT_UP(orr r0, r0, #TTB_FLAGS_UP)
#ifdef CONFIG_PID_IN_CONTEXTIDR
mrc p15, 0, r2, c13, c0, 1 @ read current context ID
lsr r2, r2, #8 @ extract the PID
bfi r1, r2, #8, #24 @ insert into new context ID
#endif
#ifdef CONFIG_ARM_ERRATA_754322
dsb
#endif
mcr p15, 0, r1, c13, c0, 1 @ set context ID
isb
mcr p15, 0, r0, c2, c0, 0 @ set TTB 0
isb
#endif
bx lr
ENDPROC(cpu_v7_switch_mm)


“mmid r1, r1” 將mm->context.id存入R1寄存器中。
“mcr p15, 0, r1, c13, c0, 1” 使用協處理器指令MCR將R1寄存器寫入CP15協處理器C13寄存器中。
根據3.1節中關於協處理器指令的描述,可以知道。

CRn:第一個協處理器寄存器c13;
opc1:協處理器操作碼0;
CRm:第二個協處理器寄存器c0;
opc2:協處理器操作碼1。
因而對應CONTEXTIDR(Context ID Register)寄存器,即將mm->context.id寫入CONTEXTIDR寄存器。這一步處理用於指示當前進程ASID(Address Space Identifier)。ASID應用於TLB,ASID可以將不同的進程在TLB中緩存的頁表映射隔離,因而可以避免進程切換時將TLB表項刷新。

 

“mcr p15, 0, r0, c2, c0, 0” 使用協處理器指令MCR將R0寄存器寫入CP15協處理器C2寄存器中。R0寄存器即APCS定義的第一個入參,即PGD。
根據3.1節中關於協處理器指令的描述,可以知道。

CRn:第一個協處理器寄存器c2;
opc1:協處理器操作碼0;
CRm:第二個協處理器寄存器c0;
opc2:協處理器操作碼0。
因而對應TTBR0寄存器,即將PGD寫入TTBR0寄存器,完成進程地址空間切換。


三、switch_to
對於內核空間及寄存器的切換,switch_to函數完成了這個任務。switch_to是與體系架構相關的函數。下面以ARM體系架構說明用戶空間的切換過程。
switch_to調用到__switch_to。

#define switch_to(prev,next,last) \
do { \
__complete_pending_tlbi(); \
last = __switch_to(prev,task_thread_info(prev), task_thread_info(next)); \
} while (0)

__switch_to彙編實現如下。三個入參分別爲:

r0:移出進程prev的task_struct;
r1:移出進程prev的thread_info;
r2:移入進程next的thread_info.
ENTRY(__switch_to)
UNWIND(.fnstart )
UNWIND(.cantunwind )
add ip, r1, #TI_CPU_SAVE @ip = r1 + TI_CPU_SAVE
ARM( stmia ip!, {r4 - sl, fp, sp, lr} ) @ Store most regs on stack
THUMB( stmia ip!, {r4 - sl, fp} ) @ Store most regs on stack
THUMB( str sp, [ip], #4 )
THUMB( str lr, [ip], #4 )
ldr r4, [r2, #TI_TP_VALUE]
ldr r5, [r2, #TI_TP_VALUE + 4]
#ifdef CONFIG_CPU_USE_DOMAINS
mrc p15, 0, r6, c3, c0, 0 @ Get domain register
str r6, [r1, #TI_CPU_DOMAIN] @ Save old domain register
ldr r6, [r2, #TI_CPU_DOMAIN]
#endif
switch_tls r1, r4, r5, r3, r7
#if defined(CONFIG_STACKPROTECTOR) && !defined(CONFIG_SMP)
ldr r7, [r2, #TI_TASK]
ldr r8, =__stack_chk_guard
.if (TSK_STACK_CANARY > IMM12_MASK)
add r7, r7, #TSK_STACK_CANARY & ~IMM12_MASK
.endif
ldr r7, [r7, #TSK_STACK_CANARY & IMM12_MASK]
#endif
#ifdef CONFIG_CPU_USE_DOMAINS
mcr p15, 0, r6, c3, c0, 0 @ Set domain register
#endif
mov r5, r0
add r4, r2, #TI_CPU_SAVE
ldr r0, =thread_notify_head
mov r1, #THREAD_NOTIFY_SWITCH
bl atomic_notifier_call_chain
#if defined(CONFIG_STACKPROTECTOR) && !defined(CONFIG_SMP)
str r7, [r8]
#endif
THUMB( mov ip, r4 )
mov r0, r5
ARM( ldmia r4, {r4 - sl, fp, sp, pc} ) @ Load all regs saved previously
THUMB( ldmia ip!, {r4 - sl, fp} ) @ Load all regs saved previously
THUMB( ldr sp, [ip], #4 )
THUMB( ldr pc, [ip] )
UNWIND(.fnend )
ENDPROC(__switch_to)

“add ip, r1, #TI_CPU_SAVE” 將IP寄存器賦值爲r1+ TI_CPU_SAVE,r1即爲prev->thread_info,TI_CPU_SAVE是cpu_context成員在thread_info中的偏移。

DEFINE(TI_CPU_SAVE, offsetof(struct thread_info, cpu_context));
1
因此IP寄存器保存了prev->thread_info->cpu_context的地址。
ARM體系架構定義的cpu_context包含了r4-r9,sl,fp,sp和pc寄存器。

struct cpu_context_save {
__u32 r4;
__u32 r5;
__u32 r6;
__u32 r7;
__u32 r8;
__u32 r9;
__u32 sl;
__u32 fp;
__u32 sp;
__u32 pc;
__u32 extra[2]; /* Xscale 'acc' register, etc */
};

“ARM( stmia ip!, {r4 - sl, fp, sp, lr} )” 將r4 - sl, fp, sp, lr寄存器中的內容保存到IP寄存器所指向的內存地址,即prev->thread_info->cpu_context,這相當於保存了prev進程運行時的寄存器上下文。

stmia是多寄存器尋址內存操作指令。用於將多個寄存器的值存放到內存。
內存操作指令stm的ia後綴表示,數據傳輸完成後地址增加。
!表示數據傳輸完成後,將地址回寫到ip寄存器。
關於stmia的詳細內容請看《ARM體系架構—ARMv7-A指令集:內存操作指令》

如下操作依然是將寄存器保存到內存,內存地址不斷遞增,且回寫到IP寄存器。
*THUMB( stmia ip!, {r4 - sl, fp} ) @ Store most regs on stack
THUMB( str sp, [ip], #4 )
THUMB( str lr, [ip], #4 ) *

prev寄存器R4和R5以壓入prev進程內核棧中,因而可以被next進程使用,寄存器R4和R5分別用來保存next->thread_info->tp_value[0]和next->thread_info->tp_value[1]
ldr r4, [r2, #TI_TP_VALUE]
ldr r5, [r2, #TI_TP_VALUE + 4]

調用atomic_notifier_call_chain函數,入參爲thread_notify_head和THREAD_NOTIFY_SWITCH。
ldr r0, =thread_notify_head
mov r1, #THREAD_NOTIFY_SWITCH
bl atomic_notifier_call_chain

add r4, r2, #TI_CPU_SAVE 實現r4寄存器保存了next->thread_info->cpu_context的地址。

“ARM( ldmia r4, {r4 - sl, fp, sp, pc} )” 將next->thread_info->cpu_context的數據加載到r4 - sl, fp, sp, lr,pc寄存器中,next->thread_info->cpu_context->sp存入寄存器SP相當於內核棧切換完成,next->thread_info->cpu_context->pc存入寄存器PC相當於跳轉到next進程運行。即切換到next進程運行時的寄存器上下文。

這樣就完成了進程內核棧及寄存器切換。

關於ARM寄存器介紹請參看《ARM體系架構—ARMv7-A處理器模式及寄存器》
————————————————
版權聲明:本文爲CSDN博主「迷途小生」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/liyuewuwunaile/article/details/106773630

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章