進程是程序的一次執行過程。用劇本和演出來類比,程序相當於劇本,而進程則相當於劇本的一次演出,舞臺、燈光則相當於進程的運行環境,貼切
進程的堆棧
每個進程都有自己的堆棧,內核在創建一個新的進程時,在創建進程控制塊task_struct的同時,也爲進程創建自己堆棧。一個進程 有2個堆棧,用戶堆棧和系統堆棧;用戶堆棧的空間指向用戶地址空間,內核堆棧的空間指向內核地址空間。當進程在用戶態運行時,CPU堆棧指針寄存器指向的 用戶堆棧地址,使用用戶堆棧,當進程運行在內核態時,CPU堆棧指針寄存器指向的是內核棧空間地址,使用的是內核棧;
進程用戶棧和內核棧之間的切換
當進程由於中斷或系統調用從用戶態轉換到內核態時,進程所使用的棧也要從用戶棧切換到內核棧。系統調用實質就是通過指令產生中斷,稱爲軟中斷。進程因爲中斷(軟中斷或硬件產生中斷),使得CPU切換到特權工作模式(0最高爲內核模式,3最低爲用戶模式),此時進程陷入內核態,進程進入內核態後,首先把用戶態的堆棧地址保存在內核堆棧中,然後設置堆棧指針寄存器的地址爲內核棧地址,這樣就完成了用戶棧向內核棧的切換。
當進程從內核態切換到用戶態時,最後把保存在內核棧中的用戶棧地址恢復到CPU棧指針寄存器即可,這樣就完成了內核棧向用戶棧的切換。
這裏要理解一下內核堆棧。前面我們講到,進程從用戶態進入內核態時,需要在內核棧中保存用戶棧的地址。那麼進入內核態時,從哪裏獲得內核棧的棧指針呢?
要解決這個問題,先要理解從用戶態剛切換到內核態以後,進程的內核棧總是空的。這點很好理解,當進程在用戶空間運行時,使用的是用戶 棧;當進程在內核態運行時,內核棧中保存進程在內核態運行的相關信息,但是當進程完成了內核態的運行,重新回到用戶態時,此時內核棧中保存的信息全部恢 復,也就是說,進程在內核態中的代碼執行完成回到用戶態時,內核棧是空的。
理解了從用戶態剛切換到內核態以後,進程的內核棧總是空的,那剛纔這個問題就很好理解了,因爲內核棧是空的,那當進程從用戶態切換到內核態後,把內核棧的棧頂地址設置給CPU的棧指針寄存器就可以了。
X86 Linux內核棧定義如下(可能現在的版本有所改變,但不妨礙我們對內核棧的理解):
在/include/linux/sched.h中定義瞭如下一個聯合結構:
union task_union {
struct task_struct task;
unsigned long stack[2408];
};
從這個結構可以看出,內核棧佔8kb的內存區。實際上,進程的task_struct結構所佔的內存是由內核動態分配的,更確切地說,內核根本不給task_struct分配內存,而僅僅給內核棧分配8K的內存,並把其中的一部分給task_struct使用。
這樣內核棧的起始地址就是union task_union變量的地址+8K 字節的長度。例如:我們動態分配一個union task_union類型的變量如下:
unsigned char *gtaskkernelstack
gtaskkernelstack = kmalloc(sizeof(union task_union));
那麼該進程每次進入內核態時,內核棧的起始地址均爲:(unsigned char *)gtaskkernelstack + 8096
進程上下文
進程切換現場稱爲進程上下文(context),包含了一個進程所具有的全部信息,一般包括:進程控制塊(Process Control Block,PCB)、有關程序段和相應的數據集。
進程控制塊PCB(任務控制塊)
進程控制塊是進程在內存中的靜態存在方式,Linux內核中用task_struct表示一個進程(相當於進程的人事檔案)。進程的靜 態描述必須保證一個進程在獲得CPU並重新進入運行態時,能夠精確的接着上次運行的位置繼續進行,相關的程序段,數據以及CPU現場信息必須保存。處理機 現場信息主要包括處理機內部寄存器和堆棧等基本數據。
進程控制塊一般可以分爲進程描述信息、進程控制信息,進程相關的資源信息和CPU現場保護機構。
進程的切換
當一個進程的時間片到時,進程需要讓出CPU給其他進程運行,內核需要進行進程切換。
Linux 的進程切換是通過調用函數進程切換函數schedule來實現的。進程切換主要分爲2個步驟:
1. 調用switch_mm()函數進行進程頁表的切換;
2. 調用 switch_to() 函數進行 CPU寄存器切換;
__switch_to定義在/arch/arm/kernel目錄下的entry-armv.S 文件中,源碼如下:
-----------------------------------------------------------------------------
ENTRY(__switch_to)
UNWIND(.fnstart )
UNWIND(.cantunwind )
add ip, r1, #TI_CPU_SAVE
ldr r3, [r2, #TI_TP_VALUE]
stmia ip!, {r4 - sl, fp, sp, lr} @ Store most regs on stack
#ifdef CONFIG_MMU
ldr r6, [r2, #TI_CPU_DOMAIN]
#endif
#if __LINUX_ARM_ARCH__ >= 6
#ifdef CONFIG_CPU_32v6K
clrex
#else
strex r5, r4, [ip] @ Clear exclusive monitor
#endif
#endif
#if defined(CONFIG_HAS_TLS_REG)
mcr p15, 0, r3, c13, c0, 3 @ set TLS register
#elif !defined(CONFIG_TLS_REG_EMUL)
mov r4, #0xffff0fff
str r3, [r4, #-15] @ TLS val at 0xffff0ff0
#endif
#ifdef CONFIG_MMU
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
mov r0, r5
ldmia r4, {r4 - sl, fp, sp, pc} @ Load all regs saved previously
UNWIND(.fnend )
ENDPROC(__switch_to)
----------------------------------------------------------
Switch_to的處理流程如下:
1. 保存本進程的CPU寄存器(PC、R0 ~ R13)到本進程的棧中;
2. 保存SP(本進程的棧基地址)到task->thread.save 中;
3. 從新進程的task->thread.save恢復SP爲新進程的棧基地址;
4. 從新進程的棧中恢復新進程的CPU相關寄存器值,
5. 新進程開始運行,完成任務切換。
這裏讀者可能會問,在進行任務切換的時候,到底是在運行進程1還是運行進程2呢?進程切換的時候,已經進行頁表切換,那頁表切換之後,切換進程使用的是進程1還是進程2的頁表呢?
要回答這個問題,首先我們要明白由誰來完成進程切換?
通過對操作系統的理解,毫無疑問,進程切換是由內核來完成的,也就是說,在進行進程切換時,CPU運行在內核模式,使用的是內核空間的內核代碼,它既不屬於進程1,也不屬於進程2,當進程的時間片到時,內核提供服務來完成進程的切換。既不使用進程1的頁表,也不使用進程2的頁表,使用的內核映射頁表。這樣我們就很好理解上面的問題了。