linux 進程用戶棧和內核棧

進程的用戶棧和內核棧

進程是程序的一次執行過程。用劇本和演出來類比,程序相當於劇本,而進程則相當於劇本的一次演出,舞臺、燈光則相當於進程的運行環境,貼切

進程的堆棧

每個進程都有自己的堆棧,內核在創建一個新的進程時,在創建進程控制塊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的頁表,使用的內核映射頁表。這樣我們就很好理解上面的問題了。


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