ARM-LINUX的進程切換

本文主要記錄S3C6410/ARM1176JZF-S架構下Linux(kernel 2.6.35)內核如何進行進程切換。

進程切換是操作系統進程調度的基礎,首先要能夠實現切換,接下來才談得上“多進程”、“多線程”以及調度算法等更高級的話題。(這裏在說“進程切換”的時候提到多線程,並不是把概念搞混淆了。在內核裏談切換的時候,Linux並不區分進程與線程,因爲這裏只有task,一個進程裏如果有多個線程,每一個都是一個task。內核實際上切換的就是task。所以,來自同一個進程的不同線程的task和來自不同進程的task對於內核來說並沒有區別。)

Linux進程切換的核心代碼是函數context_switch(),此函數的骨幹內容如下:

  static inline void
context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next)
{
switch_mm(oldmm, mm, next);

switch_to(prev, next, prev);
}

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

其中prev是當前進程/切出進程的task_struct指針,next是下一進程/切入進程的task_struct指針。context_switch()主要做兩件事情,一件是切換頁表,另一件是切換進程上下文。分別由一個函數來實現,下面分別講解。

switch_mm

switch_mm()的作用是切換切換進程的頁表,要做的最重要的事情就是把下一進程的二級頁表地址pgd(物理地址)設置到CPU的CP15控制器。進程的頁表pgd可以分爲兩部分來看,0~3G空間部分是用戶空間,採用二級映射,每個進程各不相同;3G~4G空間部分是內核空間,採用一級映射,每個進程都相同,其實每個進程的這一塊頁表內容都是從內核的頁表拷貝來的。切換頁表的主要目的是切換用戶空間的頁表,內核空間部分都一樣,不需要切換。所以,如果next是一個內核線程的話,並不會調用switch_mm()。

下面是經過簡化的switch_mm()彙編代碼:

  
  @ r0 = pgd_phys, * r1 = context_id
@
ENTRY(cpu_v6_switch_mm)
mov r2, #0
orr r0, r0, #TTB_FLAGS_UP
mcr p15, 0, r2, c7, c5, 6 @ flush BTAC/BTB
mcr p15, 0, r2, c7, c10, 4 @ drain write buffer
mcr p15, 0, r0, c2, c0, 0 @ write Translation Table Base Register 0
mcr p15, 0, r1, c13, c0, 1 @ set context ID
mov pc, lr
ENDPROC(cpu_v6_switch_mm)
  

其中第8行是最核心的一行,它把pgd的值設置給CP15的C2寄存器,C2即是”Translation Table Base Register 0“(地址轉換表基地址寄存器)。

switch_mm()調用完之後,用戶空間的內容已經是新的進程了,但這時內核空間還屬於老的進程,因爲CPU還在老進程的內核棧上面運行。下面要做的就是趕緊把內核空間空間也切換到新進程中去,這就是switch_to()所要做的。

switch_to

switch_to()的作用有兩個:一是要把當前所運行的進程(切出進程)的現場(包括各個通用寄存器、SP和PC)保存好;二是切換到新進程(切入進程),即取出此前已保存的新進程的現場,並從上次保存的地方繼續運行。注意,這裏所說的的現場是內核空間的現場,用戶空間的現場在中斷剛剛發生時已經保存過。

下面是經過簡化的switch_to()彙編代碼:

  
  @ r0 = previous task_struct, r1 = previous thread_info, r2 = next thread_info
@
ENTRY(__switch_to)
@ thread_info + TI_CPU_SAVE hold saved cpu context, registers value is stored
@ now ip hold the address of the context of previous process 
add ip, r1, #TI_CPU_SAVE
@ now r3 hold TP value of next process 
ldr r3, [r2, #TI_TP_VALUE]
@ store current regs to prev thread_info 
stmia ip!, {r4 - sl, fp, sp, lr} @ Store most regs on
@ store CPU_DOMAIN of next to r6 
ldr r6, [r2, #TI_CPU_DOMAIN]
@ set tp value and domain to cp15 
mcr p15, 0, r3, c13, c0, 3 @ yes, set TLS register
mcr p15, 0, r6, c3, c0, 0 @ Set domain register
@ now r4 hold the address of the next context 
add r4, r2, #TI_CPU_SAVE
@ put next context to registers 
ldmia r4, {r4 - sl, fp, sp, pc} @ Load all regs saved previously
ENDPROC(__switch_to)

我爲每一行都加了註釋,應該比較容易理解了。

其中第10行和第19行是比較核心的代碼,它們分別是保存當前cpu context以及恢復上一次保存的cpu context。這裏所說的”上一次“指的是當前進程在上一次處於內核態的時候,當時在離開內核態(切出)的時候,保存了現場。

這裏所說的cpu context是由結構體cpu_context所表示的,內容如下。

  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];
};

在switch_to()的第10行,當前正在運行的SVC模式下的各寄存器(包括r4-r9, sp, lr等等)都被保存起來。

在switch_to()的第19行,r4指向的是下一進程的cpu_context結構地址,這一行執行完後,cpu context中所保存的內容就被讀進各個寄存器,sp和pc都被更新,現在CPU已經不在剛剛的那個內核棧上了。

第10行和第19行的寄存器列表有一處區別:第10行的最後一個寄存器是lr,即調用__switch_to()的返回地址;而第19行的最後一個寄存器是pc。這就是說,在切換的時候,當前進程在切回來的時候會從__switch_to()的下一條指令開始執行,這正是內核所需要的。

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