本文基於Linux 3.5.4原始碼。
對每個程式,Linux核心都把兩個不同的資料結構緊湊的存放在一個單獨為程式分配的記憶體區域中:一個是核心態的程式堆疊,另一個是緊挨著程式描述符的小資料結構thread_info,叫做執行緒描述符。在較新的核心程式碼中,這個儲存區域的大小通常為8192個位元組(兩個頁框)。在linux/arch/x86/include/asm/page_32_types.h中,
1 2 |
#define THREAD_SIZE_ORDER 1 #define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER) |
出於效率考慮,核心讓這8K空間佔據連續的兩個頁框並讓第一個頁框的起始地址是213的倍數。
核心態的程式訪問處於核心資料段的棧,這個棧不同於使用者態的程式所用的棧。使用者態程式所用的棧,是在程式執行緒地址空間中;而核心棧是當程式從使用者空間進入核心空間時,特權級發生變化,需要切換堆疊,那麼核心空間中使用的就是這個核心棧。因為核心控制路徑使用很少的棧空間,所以只需要幾千個位元組的核心態堆疊。需要注意的是,核心態堆疊僅用於核心例程,Linux核心另外提供了單獨的硬中斷棧和軟中斷棧。
下圖中顯示了在實體記憶體中存放兩種資料結構的方式。執行緒描述符駐留與這個記憶體區的開始,而棧頂末端向下增長。 下圖摘自ULK3,但是較新的核心程式碼中,程式描述符task_struct結構中沒有直接指向thread_info結構的指標,而是用一個void指標型別的成員表示,然後通過型別轉換來訪問thread_info結構。相關程式碼在include/linux/sched.h中:
1 |
#define task_thread_info(task) ((struct thread_info *)(task)->stack) |
在這個圖中,esp暫存器是CPU棧指標,用來存放棧頂單元的地址。在80×86系統中,棧起始於頂端,並朝著這個記憶體區開始的方向增長。從使用者態剛切換到核心態以後,程式的核心棧總是空的。因此,esp暫存器指向這個棧的頂端。
一旦資料寫入堆疊,esp的值就遞減。在Linux3.5.4核心中,thread_info結構是72個位元組長(ULK3時代的核心中,這個結構的大小是52個位元組),因此核心棧能擴充套件到8120個位元組。thread_info結構的定義如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
struct thread_info { struct task_struct *task; /* main task structure */ struct exec_domain *exec_domain; /* execution domain */ __u32 flags; /* low level flags */ __u32 status; /* thread synchronous flags */ __u32 cpu; /* current CPU */ int preempt_count; /* 0 => preemptable, <0 => BUG */ mm_segment_t addr_limit; struct restart_block restart_block; void __user *sysenter_return; #ifdef CONFIG_X86_32 unsigned long previous_esp; /* ESP of the previous stack in case of nested (IRQ) stacks */ __u8 supervisor_stack[0]; #endif unsigned int sig_on_uaccess_error:1; unsigned int uaccess_err:1; /* uaccess failed */ }; |
Linux核心中使用一個聯合體來表示一個程式的執行緒描述符和核心棧:
1 2 3 4 |
union thread_union { struct thread_info thread_info; unsigned long stack[THREAD_SIZE/sizeof(long)]; }; |
下面來說說如何通過esp棧指標來獲取當前在CPU上正在執行程式的thread_info結構。實際上,上面提到,thread_info結構和核心態堆疊是緊密結合在一起的,佔據兩個頁框的實體記憶體空間。而且,這兩個頁框的起始起始地址是213對齊的。所以,核心通過簡單的遮蔽掉esp的低13位有效位就可以獲得thread_info結構的基地址了。在檔案linux/arch/x86/include/asm/thread_info.h中,有如下程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#ifndef __ASSEMBLY__ /* how to get the current stack pointer from C */ register unsigned long current_stack_pointer asm("esp") __used; /* how to get the thread information struct from C */ static inline struct thread_info *current_thread_info(void) { return (struct thread_info *) (current_stack_pointer & ~(THREAD_SIZE - 1)); } #else /* !__ASSEMBLY__ */ /* how to get the thread information struct from ASM */ #define GET_THREAD_INFO(reg) \ movl $-THREAD_SIZE, reg; \ andl %esp, reg /* use this one if reg already contains %esp */ #define GET_THREAD_INFO_WITH_ESP(reg) \ andl $-THREAD_SIZE, reg #endif |
在上面的程式碼中,當前的棧指標current_stack_pointer就是esp,
THREAD_SIZE為8K,二進位制的表示為0000 0000 0000 0000 0010 0000 0000 0000。
~(THREAD_SIZE-1)的結果剛好為1111 1111 1111 1111 1110 0000 0000 0000,第十三位是全為零,也就是剛好遮蔽了esp的低十三位,最終得到的是thread_info的地址。
程式最常用的是程式描述符結構task_struct而不是thread_info結構的地址。為了獲取當前CPU上執行程式的task_struct結構,核心提供了current巨集,該巨集本質上等價於current_thread_info()->task,在include/asm-generic/current.h中定義:
1 2 |
#define get_current() (current_thread_info()->task) #define current get_current() |