栈空间_Linux中的进程栈和线程栈
1. 進(jìn)程棧
進(jìn)程棧是屬于用戶態(tài)棧,和進(jìn)程虛擬地址空間 (Virtual Address Space) 密切相關(guān)。那我們先了解下什么是虛擬地址空間:在 32 位機器下,虛擬地址空間大小為 4G。這些虛擬地址通過頁表 (Page Table) 映射到物理內(nèi)存,頁表由操作系統(tǒng)維護(hù),并被處理器的內(nèi)存管理單元 (MMU) 硬件引用。每個進(jìn)程都擁有一套屬于它自己的頁表,因此對于每個進(jìn)程而言都好像獨享了整個虛擬地址空間。
Linux 內(nèi)核將這 4G 字節(jié)的空間分為兩部分,將最高的 1G 字節(jié)(0xC0000000-0xFFFFFFFF)供內(nèi)核使用,稱為 內(nèi)核空間。而將較低的3G字節(jié)(0x00000000-0xBFFFFFFF)供各個進(jìn)程使用,稱為 用戶空間。每個進(jìn)程可以通過系統(tǒng)調(diào)用陷入內(nèi)核態(tài),因此內(nèi)核空間是由所有進(jìn)程共享的。雖然說內(nèi)核和用戶態(tài)進(jìn)程占用了這么大地址空間,但是并不意味它們使用了這么多物理內(nèi)存,僅表示它可以支配這么大的地址空間。它們是根據(jù)需要,將物理內(nèi)存映射到虛擬地址空間中使用。
Linux 對進(jìn)程地址空間有個標(biāo)準(zhǔn)布局,地址空間中由各個不同的內(nèi)存段組成 (Memory Segment),主要的內(nèi)存段如下:
- 程序段 (Text Segment):可執(zhí)行文件代碼的內(nèi)存映射
- 數(shù)據(jù)段 (Data Segment):可執(zhí)行文件的已初始化全局變量的內(nèi)存映射
- BSS段 (BSS Segment):未初始化的全局變量或者靜態(tài)變量(用零頁初始化)
- 堆區(qū) (Heap) : 存儲動態(tài)內(nèi)存分配,匿名的內(nèi)存映射
- 棧區(qū) (Stack) : 進(jìn)程用戶空間棧,由編譯器自動分配釋放,存放函數(shù)的參數(shù)值、局部變量的值等
- 映射段(Memory Mapping Segment):任何內(nèi)存映射文件
而上面進(jìn)程虛擬地址空間中的棧區(qū),正指的是我們所說的進(jìn)程棧。進(jìn)程棧的初始化大小是由編譯器和鏈接器計算出來的,但是棧的實時大小并不是固定的,Linux 內(nèi)核會根據(jù)入棧情況對棧區(qū)進(jìn)行動態(tài)增長(其實也就是添加新的頁表)。但是并不是說棧區(qū)可以無限增長,它也有最大限制 RLIMIT_STACK (一般為 8M),我們可以通過 ulimit 來查看或更改 RLIMIT_STACK 的值。
進(jìn)程棧的動態(tài)增長實現(xiàn)
進(jìn)程在運行的過程中,通過不斷向棧區(qū)壓入數(shù)據(jù),當(dāng)超出棧區(qū)容量時,就會耗盡棧所對應(yīng)的內(nèi)存區(qū)域,這將觸發(fā)一個 缺頁異常 (page fault)。通過異常陷入內(nèi)核態(tài)后,異常會被內(nèi)核的 expand_stack() 函數(shù)處理,進(jìn)而調(diào)用 acct_stack_growth() 來檢查是否還有合適的地方用于棧的增長。
如果棧的大小低于 RLIMIT_STACK(通常為8MB),那么一般情況下棧會被加長,程序繼續(xù)執(zhí)行,感覺不到發(fā)生了什么事情,這是一種將棧擴展到所需大小的常規(guī)機制。然而,如果達(dá)到了最大棧空間的大小,就會發(fā)生 棧溢出(stack overflow),進(jìn)程將會收到內(nèi)核發(fā)出的 段錯誤(segmentation fault) 信號。
動態(tài)棧增長是唯一一種訪問未映射內(nèi)存區(qū)域而被允許的情形,其他任何對未映射內(nèi)存區(qū)域的訪問都會觸發(fā)頁錯誤,從而導(dǎo)致段錯誤。一些被映射的區(qū)域是只讀的,因此企圖寫這些區(qū)域也會導(dǎo)致段錯誤。
2. 線程棧
從 Linux 內(nèi)核的角度來說,其實它并沒有線程的概念。Linux 把所有線程都當(dāng)做進(jìn)程來實現(xiàn),它將線程和進(jìn)程不加區(qū)分的統(tǒng)一到了 task_struct 中。線程僅僅被視為一個與其他進(jìn)程共享某些資源的進(jìn)程,而是否共享地址空間幾乎是進(jìn)程和 Linux 中所謂線程的唯一區(qū)別。線程創(chuàng)建的時候,加上了 CLONE_VM 標(biāo)記,這樣 線程的內(nèi)存描述符 將直接指向 父進(jìn)程的內(nèi)存描述符。
if (clone_flags & CLONE_VM) {/** current 是父進(jìn)程而 tsk 在 fork() 執(zhí)行期間是共享子進(jìn)程*/atomic_inc(¤t->mm->mm_users);tsk->mm = current->mm;}雖然線程的地址空間和進(jìn)程一樣,但是對待其地址空間的 stack 還是有些區(qū)別的。對于 Linux 進(jìn)程或者說主線程,其 stack 是在 fork 的時候生成的,實際上就是復(fù)制了父親的 stack 空間地址,然后寫時拷貝 (cow) 以及動態(tài)增長。然而對于主線程生成的子線程而言,其 stack 將不再是這樣的了,而是事先固定下來的,使用 mmap 系統(tǒng)調(diào)用(實際上是進(jìn)程的堆的一部分),它不帶有 VM_STACK_FLAGS 標(biāo)記。這個可以從 glibc 的nptl/allocatestack.c 中的 allocate_stack() 函數(shù)中看到:點擊(此處)折疊或打開
mem = mmap (NULL, size, prot, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
由于線程的 mm->start_stack 棧地址和所屬進(jìn)程相同,所以線程棧的起始地址并沒有存放在 task_struct 中,應(yīng)該是使用 pthread_attr_t 中的 stackaddr 來初始化 task_struct->thread->sp(sp 指向 struct pt_regs 對象,該結(jié)構(gòu)體用于保存用戶進(jìn)程或者線程的寄存器現(xiàn)場)。這些都不重要,重要的是,線程棧不能動態(tài)增長,一旦用盡就沒了,這是和生成進(jìn)程的 fork 不同的地方。由于線程棧是從進(jìn)程的地址空間中 map 出來的一塊內(nèi)存區(qū)域,原則上是線程私有的。但是同一個進(jìn)程的所有線程生成的時候淺拷貝生成者的 task_struct 的很多字段,其中包括所有的 vma,如果愿意,其它線程也還是可以訪問到的,于是一定要注意。
3. 進(jìn)程棧和線程棧大小的調(diào)整
進(jìn)程和線程的棧分別是多大呢?首先從我們熟悉的ulimit -s說起,熟悉linux的人都應(yīng)該知道通過ulimit -s可以修改棧的大小,除此之外還有g(shù)etrlimit/setrlimit兩個函數(shù):
這兩個函數(shù)當(dāng)?shù)谝粋€參數(shù)傳入RLIMIT_STACK時,可以設(shè)置和獲取棧的大小,其作用和ulimit -s是一樣的,只是單位不同,ulimit -s的單位是kB,而這兩個函數(shù)的單位是B(字節(jié)),詳細(xì)使用方法請參考man手冊。
最后還有線程的pthread_attr_setstacksize/pthread_attr_getstacksize。使用setrlimit和使用ulimit -s設(shè)置棧大小效果相同,這兩種方式都是針對進(jìn)程棧大小設(shè)置,只不過前者只真對當(dāng)前進(jìn)程,后者針對當(dāng)前shell;
而線程棧大小的關(guān)系就相對比較復(fù)雜點,前文說過線程大小是靜態(tài)的,是在創(chuàng)建時就確定了的,當(dāng)然如果使用pthread_attr_setstacksize可以在創(chuàng)建線程時指定線程棧大小,但如果不指定線程棧的話其默認(rèn)大小是什么情況呢?想要了解線程棧的大小就要看glibc的線程創(chuàng)建函數(shù),具體就是pthread_create->__pthread_create_2_1->allocate_stack。具體代碼還是比較復(fù)雜的,這里簡化為一個偽代碼:
可以看出,線程默認(rèn)棧大小和進(jìn)程棧大小的關(guān)系:
所以我們?nèi)绻褂胾limit設(shè)置進(jìn)程棧大小是無限大其實棧大小反而相對比較小,這是為什么呢?前面我們已經(jīng)講過線程棧和進(jìn)程棧的位置不同,線程棧其實是在進(jìn)程的堆上分配的,并且不會動態(tài)增加,所以不可能設(shè)置一個無限大小的線程棧。
最后,我們再對進(jìn)程棧和線程棧做一下總結(jié)和說明:
以上有不足的地方歡迎指出討論,覺得不錯的朋友希望能得到您的轉(zhuǎn)發(fā)支持,同時可以持續(xù)關(guān)注我,每天分享Linux C/C++后臺開發(fā)干貨內(nèi)容!
最后,如果覺得學(xué)習(xí)資料難找的話,可以添加小編的 LinuxC/C++交流群 ,期待你的加入~
總結(jié)
以上是生活随笔為你收集整理的栈空间_Linux中的进程栈和线程栈的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 怎么用手机进入路由器设置如何手机进入路由
- 下一篇: linux 命令解码空格,Shell 编