天天看點

轉載好文:《專題研究一 程序的深入了解與分析》 (早期2.4核心)

在學習Linux程序核心棧的時候,看到這篇好文,在這裡轉載下:

<b>(注意:資料結構針對的是早期的2.4核心,2.6以後的核心資料結構和處理方法稍有不同,但是基本原理相同)</b>

作者:

曹國輝 

南京淩嵌教育嵌入式Linux金牌講師

<b>專題研究一</b>

 程序的深入了解與分析

       程序是程式的一次執行過程。用劇本和演出來類比,程式相當于劇本,而程序則相當于劇本的一次演出,舞台、燈光則相當于程序的運作環境。

<b>程序的堆棧</b>

      每個程序都有自己的堆棧,核心在建立一個新的程序時,在建立程序控制塊task_struct的同時,也為程序建立自己堆棧。<b>一個程序有</b><b>2</b><b>個堆棧:使用者堆棧和系統堆棧</b>;使用者堆棧的空間指向使用者位址空間,核心堆棧的空間指向核心位址空間。當程序在使用者态運作時,CPU堆棧指針寄存器指向使用者堆棧位址,使用使用者堆棧;當程序運作在核心态時,CPU堆棧指針寄存器指向的是核心棧空間位址,使用的是核心棧。

<b>程序使用者棧和核心棧之間的切換</b>

       當程序由于中斷或系統調用從使用者态轉換到核心态時,程序所使用的棧也要從使用者棧切換到核心棧。系統調用實質就是通過指令産生中斷,稱為軟中斷。程序因為中斷(軟中斷或硬體産生中斷),使得CPU切換到特權工作模式,此時程序陷入核心态,<b>程序進入核心态後,首先把使用者态的堆棧位址儲存在核心堆棧中,然後設定堆棧指針寄存器的位址為核心棧位址,這樣就完成了使用者棧向核心棧的切換。</b>

      <b>當程序從核心态切換到使用者态時,最後把儲存在核心棧中的使用者棧位址恢複到</b><b>CPU棧指針寄存器即可,這樣就完成了核心棧向使用者棧的切換。</b>

       這裡要了解一下核心堆棧。前面我們講到,程序從使用者态進入核心态時,需要在核心棧中儲存使用者棧的位址。那麼進入核心态時,從哪裡獲得核心棧的棧指針呢?

      要解決這個問題,<b>先要了解從使用者态剛切換到核心态以後,程序的核心棧總是空的。</b>這點很好了解,當程序在使用者空間運作時,使用的是使用者棧;當程序在核心态運作時,核心棧中儲存程序在核心态運作的相關資訊,但是當程序完成了核心态的運作,重新回到使用者态時,此時核心棧中儲存的資訊全部恢複,也就是說,程序在核心态中的代碼執行完成回到使用者态時,核心棧是空的。

     了解了從使用者态剛切換到核心态以後,程序的核心棧總是空的,那剛才這個問題就很好了解了,<b>因為核心棧是空的,那當程序從使用者态切換到核心态後,把核心棧的棧頂位址設定給</b><b>CPU的棧指針寄存器就可以了。</b>

      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 

<b>程序上下文</b>

       程序切換現場稱為程序上下文(context),包含了一個程序所具有的全部資訊,一般包括:程序控制塊(Process

Control Block,PCB)、有關程式段和相應的資料集。

<b>      程序控制塊</b><b>PCB</b><b>(任務控制塊)</b>

        程序控制塊是程序在記憶體中的靜态存在方式,Linux核心中用task_struct表示一個程序(相當于程序的人事檔案)。程序的靜

态描述必須保證一個程序在獲得CPU并重新進入運作态時,能夠精确的接着上次運作的位置繼續進行,相關的程式段,資料以及CPU現場資訊必須儲存。處理機

現場資訊主要包括處理機内部寄存器和堆棧等基本資料。

程序控制塊一般可以分為程序描述資訊、程序控制資訊,程序相關的資源資訊和CPU現場保護機構。 

<b>程序的切換</b>

當一個程序的時間片到時,程序需要讓出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__ &gt;= 6

#ifdef CONFIG_CPU_32v6K

clrex

#else

strex r5, r4, [ip] @ Clear exclusive monitor

#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

mcr p15, 0, r6, c3, c0, 0 @ Set domain register

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-&gt;thread.save

中;

3. 從新程序的task-&gt;thread.save恢複SP為新程序的棧基位址;

4. 從新程序的棧中恢複新程序的CPU相關寄存器值,

5. 新程序開始運作,完成任務切換。 

這裡讀者可能會問,在進行任務切換的時候,到底是在運作程序1還是運作程序2呢?程序切換的時候,已經進行頁表切換,那頁表切換之後,切換程序使用的是程序1還是程序2的頁表呢? 

要回答這個問題,首先我們要明白由誰來完成程序切換? 

通過對作業系統的了解,毫無疑問,程序切換是由核心來完成的,也就是說,在進行程序切換時,CPU運作在核心模式,使用的是核心空間的核心代碼,它既不屬于程序1,也不屬于程序2,當程序的時間片到時,核心提供服務來完成程序的切換。既不使用程序1的頁表,也不使用程序2的頁表,使用的核心映射頁表。這樣我們就很好了解上面的問題了。 

希望這篇文的對大家深入了解程序有所幫助。

繼續閱讀