前言
雖然我也很想講X86_64體系助隧,無奈這樣的資料的確不多,因此本文還是本著學習的態(tài)度滑沧,探究早已經過時的X86體系并村。
本文參考自此文巍实,該博主對棧的數據結構、棧的作用等進行了闡述哩牍,其中涉及了函數棧幀的相關知識功茴,這部分內容我沒有轉載夸浅,我僅僅轉載了我感興趣的進程棧,線程棧等部分,并在其中融入了自己的理解和補充吕漂。
原文作者將Linux的棧劃分為:
- 進程棧
- 線程棧
- 內核棧
- 中斷棧
顯然是缺少了信號棧母怜,有關信號棧掷倔,在我翻譯的信號相關的linux手冊中啊奄,曾經多次的提及:
linux手冊翻譯——signal(7)
linux手冊翻譯——sigaction(2)
linux手冊翻譯——sigreturn(2)
linux手冊翻譯——sigaltstack(2)
從實現上說,信號棧其實類似于線程棧处窥,都是可以用用戶自己的指定的嘱吗。
此外,需要讀者有對進程的虛擬地址空間有一定的了解滔驾,相關參考文獻谒麦,可以說非常多了:
http://www.reibang.com/p/174c1da40c03
https://www.cnblogs.com/clover-toeic/p/3754433.html
稍微總結一下:
在X86體系中,虛擬內存空間大小為4GB哆致,可以說是非常小了绕德,Linux將其分為了兩部分,即高1G字節(jié)的內核空間和低3G字節(jié)的用戶空間摊阀,其中內核空間是由所有的進程共享的耻蛇,在實現上就是所有進程的內核空間的頁表是共享使用的。
用戶空間胞此,或者說進程的地址空間有自己的標準布局臣咖,由各個內存段組成,每個內存段就是一個VMA結構漱牵,本質上全部都是用過mmap是實現的夺蛇,布局如圖:
包括了:
- 程序段 (Text Segment):可執(zhí)行文件代碼的內存映射
- 數據段 (Data Segment):可執(zhí)行文件的已初始化全局變量的內存映射
- BSS段 (BSS Segment):未初始化的全局變量或者靜態(tài)變量(用零頁初始化)
- 堆區(qū) (Heap) : 存儲動態(tài)內存分配,匿名的內存映射
- 棧區(qū) (Stack) : 進程用戶空間棧酣胀,由編譯器自動分配釋放刁赦,存放函數的參數值、局部變量的值等
- 映射段(Memory Mapping Segment):任何內存映射文件
一闻镶、進程棧
進程棧甚脉,就是進程地址空間當中的棧區(qū)!
進程棧的初始化大小是由編譯器和鏈接器計算出來的铆农,Linux內核會根據入棧情況對棧區(qū)進行動態(tài)增長(其實也就是添加新的頁表)牺氨。進程棧是有最大值的,此值由rlimit的值進行限制,可通過shell命令ulimit -s
查看波闹,默認是8Mb酝豪。關于rlimit,可以查看其linux手冊精堕!
以上結論在X86_64上依然是適用的,如下測試程序:
/* file name: stacksize.c */
void *orig_stack_pointer;
void blow_stack() {
blow_stack();
}
int main() {
__asm__("movq %rsp, orig_stack_pointer");
blow_stack();
return 0;
}
編譯運行結果如下:
【擴展閱讀】:進程棧的動態(tài)增長實現
進程在運行的過程中蒲障,通過不斷向棧區(qū)壓入數據歹篓,當超出棧區(qū)容量時,就會耗盡棧所對應的內存區(qū)域揉阎,這將觸發(fā)一個 缺頁異常 (page fault)庄撮。通過異常陷入內核態(tài)后,異常會被內核的expand_stack()
函數處理毙籽,進而調用acct_stack_growth()
來檢查是否還有合適的地方用于棧的增長洞斯。如果棧的大小低于
RLIMIT_STACK
(通常為8MB),那么一般情況下棧會被加長坑赡,程序繼續(xù)執(zhí)行烙如,感覺不到發(fā)生了什么事情,這是一種將棧擴展到所需大小的常規(guī)機制毅否。然而亚铁,如果達到了最大棧空間的大小螟加,就會發(fā)生 棧溢出(stack overflow)徘溢,進程將會收到內核發(fā)出的 段錯誤(segmentation fault) 信號。動態(tài)棧增長是唯一一種訪問未映射內存區(qū)域而被允許的情形捆探,其他任何對未映射內存區(qū)域的訪問都會觸發(fā)頁錯誤然爆,從而導致段錯誤。一些被映射的區(qū)域是只讀的黍图,因此企圖寫這些區(qū)域也會導致段錯誤曾雕。
如今的內核是如何處理的需要閱讀源碼,但是原理應該大差不多
上面對進程的地址空間有個比較全局的介紹雌隅,那我們看下 Linux 內核中是怎么體現上面內存布局的翻默。內核使用內存描述符來表示進程的地址空間,該描述符表示著進程所有地址空間的信息恰起。內存描述符由 mm_struct 結構體表示修械,下面給出內存描述符結構中各個域的描述,請大家結合前面的 進程內存段布局 圖一起看:
struct mm_struct {
struct vm_area_struct *mmap; /* 內存區(qū)域鏈表 */
struct rb_root mm_rb; /* VMA 形成的紅黑樹 */
...
struct list_head mmlist; /* 所有 mm_struct 形成的鏈表 */
...
unsigned long total_vm; /* 全部頁面數目 */
unsigned long locked_vm; /* 上鎖的頁面數據 */
unsigned long pinned_vm; /* Refcount permanently increased */
unsigned long shared_vm; /* 共享頁面數目 Shared pages (files) */
unsigned long exec_vm; /* 可執(zhí)行頁面數目 VM_EXEC & ~VM_WRITE */
unsigned long stack_vm; /* 棧區(qū)頁面數目 VM_GROWSUP/DOWN */
unsigned long def_flags;
unsigned long start_code, end_code, start_data, end_data; /* 代碼段检盼、數據段 起始地址和結束地址 */
unsigned long start_brk, brk, start_stack; /* 棧區(qū) 的起始地址肯污,堆區(qū) 起始地址和結束地址 */
unsigned long arg_start, arg_end, env_start, env_end; /* 命令行參數 和 環(huán)境變量的 起始地址和結束地址 */
...
/* Architecture-specific MM context */
mm_context_t context; /* 體系結構特殊數據 */
/* Must use atomic bitops to access the bits */
unsigned long flags; /* 狀態(tài)標志位 */
...
/* Coredumping and NUMA and HugePage 相關結構體 */
};
二、線程棧
Linux可以說很好的貫徹了,線程是任務的調度單位蹦渣,進程是資源分配單位的理念哄芜,在實現上,其實是只有task的概念柬唯,并對應了task_struct
結構认臊,而進程其實是一個task組,也就是他是一組task的集合锄奢,所有的task共享內存資源失晴,即mm_struct,其中task組的第一個task拘央,我們稱之為task組的leader涂屁,即主線程!
因此在linux中線程對應單個task灰伟,每個線程擁有一個task_struct拆又,進程就是一組task,每個進程擁有一個mm_struct栏账,進程中的線程共享mm_struct結構帖族!
我們在創(chuàng)建線程時,會調用clone系統(tǒng)調用发笔,clone系統(tǒng)調用也是fork的底層實現盟萨,如果要創(chuàng)建線程,那就需要帶上CLONE_VM
標志了讨,此時捻激,新創(chuàng)建的task,將會直接共享父親的mm_struct:
if (clone_flags & CLONE_VM) {
/*
* current 是父進程而 tsk 在 fork() 執(zhí)行期間是共享子進程
*/
atomic_inc(¤t->mm->mm_users);
tsk->mm = current->mm;
}
從上面的描述可以看出前计,諸多線程之間是共享棧區(qū)的胞谭,但是顯然他們不可能一起使用棧區(qū),否則就混亂了男杈!因此進程棧其實是線程組leader的椪梢伲空間。
因此線程棧是需要我們在創(chuàng)建線程時指定的伶棒!翻閱pthread_create()的源碼旺垒,其調用了:
allocate_stack (iattr, &pd, &stackaddr, &stacksize);
函數來申請棧空間肤无,而allocate_stack最終又調用了:
mem = __mmap (NULL, size, (guardsize == 0) ? prot : PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
因此線程棧先蒋,其實是通過mmap在文件映射區(qū)申請的匿名頁。
此外需要注意的是宛渐,線程的棧是不能動態(tài)增長的竞漾,指定了多大眯搭,用完就完了!當然了业岁,默認情況下是和主線程保持一致鳞仙,都是8Mb
三、內核棧
在一個線程的執(zhí)行周期中,必然會通過到系統(tǒng)調用陷入內核笔时。在執(zhí)行系統(tǒng)調用陷入內核之后棍好,這些內核代碼所使用的棧并不是原先用戶空間中的棧,而是一個單獨內核空間的棧糊闽,這個稱作內核棧梳玫。每當我們創(chuàng)建新的線程的時候都會一同創(chuàng)建線程的內核棧,實現上是通過 slab 分配器從 thread_info_cache 緩存池中分配出來右犹,其大小為 THREAD_SIZE,一般來說是一個頁大小 4K姚垃;
眾所周知念链,我們每個線程除擁有一個task_struct結構之外,還有一個用于找到task_struct的thread_info結構积糯,而thread_info被寫入在了線程內核棧的最低地址處掂墓,其使用了thread_union的聯合體結構來實現:
union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
具體關系如圖:
這里有一個小技巧,直接將 esp 的地址與上 ~(THREAD_SIZE - 1) 后即可直接獲得 thread_info 的地址看成。由于 thread_union 結構體是從 thread_info_cache 的 Slab 緩存池中申請出來的君编,而 thread_info_cache 在 kmem_cache_create 創(chuàng)建的時候,保證了地址是 THREAD_SIZE 對齊的川慌。因此只需要對棧指針進行 THREAD_SIZE 對齊吃嘿,即可獲得 thread_union 的地址,也就獲得了 thread_union 的地址梦重。成功獲取到 thread_info 后兑燥,直接取出它的 task 成員就成功得到了 task_struct。其實上面這段描述琴拧,也就是 current 宏的實現方法:
register unsigned long current_stack_pointer asm ("sp");
static inline struct thread_info *current_thread_info(void)
{
return (struct thread_info *)
(current_stack_pointer & ~(THREAD_SIZE - 1));
}
#define get_current() (current_thread_info()->task)
#define current get_current()
根據棧溢出的檢測中指出這種方式存在一定的問題:
如果棧的內存使用達到了thread_info的區(qū)域降瞳,雖然此時"棧溢出"還沒有發(fā)生,但thread_info的數據結構會受到破壞蚓胸,可能造成這個線程之后無法正常運行挣饥。
從Linux 4.1開始,thread_info就在逐漸被簡化沛膳,直到4.9版本徹底不再通過thread_info獲取task_struct指針扔枫,而thread_info本身也被移入了task_struct結構體中,所以這個問題也就不復存在了于置。
四茧吊、中斷棧
進程陷入內核態(tài)的時候贞岭,需要內核棧來支持內核函數調用。中斷也是如此搓侄,當系統(tǒng)收到中斷事件后瞄桨,進行中斷處理的時候,也需要中斷棧來支持函數調用讶踪。由于系統(tǒng)中斷的時候芯侥,系統(tǒng)當然是處于內核態(tài)的,所以中斷棧是可以和內核棧共享的乳讥。但是具體是否共享柱查,這和具體處理架構密切相關。
X86 上中斷棧就是獨立于內核棧的云石;獨立的中斷棧所在內存空間的分配發(fā)生在 arch/x86/kernel/irq_32.c 的 irq_ctx_init() 函數中(如果是多處理器系統(tǒng)唉工,那么每個處理器都會有一個獨立的中斷棧),函數使用 __alloc_pages 在低端內存區(qū)分配 2個物理頁面汹忠,也就是8KB大小的空間淋硝。有趣的是,這個函數還會為 softirq 分配一個同樣大小的獨立堆棧宽菜。如此說來谣膳,softirq 將不會在 hardirq 的中斷棧上執(zhí)行,而是在自己的上下文中執(zhí)行铅乡。
而 ARM 上中斷棧和內核棧則是共享的继谚;中斷棧和內核棧共享有一個負面因素,如果中斷發(fā)生嵌套阵幸,可能會造成棧溢出花履,從而可能會破壞到內核棧的一些重要數據,所以椙揉郑空間有時候難免會捉襟見肘臭挽。