1.進程棧
進程棧是屬于用戶態棧,和進程虛擬地址空間(Virtual Address Space)密切相關。那我們先了解下什么是虛擬地址空間:在32位機器下,虛擬地址空間大小為4G。這些虛擬地址通過頁表(Page Table)映射到物理內存,頁表由操作系統維護,并被處理器的內存管理單元(MMU)硬件引用。每個進程都擁有一套屬于它自己的頁表,因此對于每個進程而言都好像獨享了整個虛擬地址空間。
Linux內核將這4G字節的空間分為兩部分,將最高的1G字節(0xC0000000-0xFFFFFFFF)供內核使用,稱為內核空間。而將較低的3G字節(0x00000000-0xBFFFFFFF)供各個進程使用,稱為用戶空間。每個進程可以通過系統調用陷入內核態,因此內核空間是由所有進程共享的。雖然說內核和用戶態進程占用了這么大地址空間,但是并不意味它們使用了這么多物理內存,僅表示它可以支配這么大的地址空間。它們是根據需要,將物理內存映射到虛擬地址空間中使用。
Linux對進程地址空間有個標準布局,地址空間中由各個不同的內存段組成(Memory Segment),主要的內存段如下:
-程序段(Text Segment):可執行文件代碼的內存映射
-數據段(Data Segment):可執行文件的已初始化全局變量的內存映射
- BSS段(BSS Segment):未初始化的全局變量或者靜態變量(用零頁初始化)
-堆區(Heap) :存儲動態內存分配,匿名的內存映射
-棧區(Stack) :進程用戶空間棧,由編譯器自動分配釋放,存放函數的參數值、局部變量的值等
-映射段(Memory Mapping Segment):任何內存映射文件
而上面進程虛擬地址空間中的棧區,正指的是我們所說的進程棧。進程棧的初始化大小是由編譯器和鏈接器計算出來的,但是棧的實時大小并不是固定的,Linux內核會根據入棧情況對棧區進行動態增長(其實也就是添加新的頁表)。但是并不是說棧區可以無限增長,它也有最大限制RLIMIT_STACK (一般為8M),我們可以通過ulimit來查看或更改RLIMIT_STACK的值。
【擴展閱讀】:進程棧的動態增長實現
進程在運行的過程中,通過不斷向棧區壓入數據,當超出棧區容量時,就會耗盡棧所對應的內存區域,這將觸發一個缺頁異常(page fault)。通過異常陷入內核態后,異常會被內核的expand_stack()函數處理,進而調用acct_stack_growth()來檢查是否還有合適的地方用于棧的增長。
如果棧的大小低于RLIMIT_STACK(通常為8MB),那么一般情況下棧會被加長,程序繼續執行,感覺不到發生了什么事情,這是一種將棧擴展到所需大小的常規機制。然而,如果達到了最大棧空間的大小,就會發生 棧溢出(stack overflow),進程將會收到內核發出的 段錯誤(segmentation fault) 信號。
動態棧增長是唯一一種訪問未映射內存區域而被允許的情形,其他任何對未映射內存區域的訪問都會觸發頁錯誤,從而導致段錯誤。一些被映射的區域是只讀的,因此企圖寫這些區域也會導致段錯誤。
2.線程棧
從Linux內核的角度來說,其實它并沒有線程的概念。Linux把所有線程都當做進程來實現,它將線程和進程不加區分的統一到了task_struct中。線程僅僅被視為一個與其他進程共享某些資源的進程,而是否共享地址空間幾乎是進程和Linux中所謂線程的唯一區別。線程創建的時候,加上了CLONE_VM標記,這樣線程的內存描述符將直接指向父進程的內存描述符。
點擊(此處)折疊或打開
if(clone_flags&CLONE_VM){
/*
*current 是父進程而 tsk 在 fork()執行期間是共享子進程
*/
atomic_inc(¤t->mm->mm_users);
tsk->mm=current->mm;
}
雖然線程的地址空間和進程一樣,但是對待其地址空間的stack還是有些區別的。對于Linux進程或者說主線程,其stack是在fork的時候生成的,實際上就是復制了父親的stack空間地址,然后寫時拷貝(cow)以及動態增長。然而對于主線程生成的子線程而言,其stack將不再是這樣的了,而是事先固定下來的,使用mmap系統調用(實際上是進程的堆的一部分),它不帶有VM_STACK_FLAGS標記。這個可以從glibc的nptl/allocatestack.c中的allocate_stack()函數中看到:
點擊(此處)折疊或打開
mem=mmap(NULL,size,prot,MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK,-1,0);
由于線程的mm->start_stack棧地址和所屬進程相同,所以線程棧的起始地址并沒有存放在task_struct中,應該是使用pthread_attr_t中的stackaddr來初始化task_struct->thread->sp(sp指向struct pt_regs對象,該結構體用于保存用戶進程或者線程的寄存器現場)。這些都不重要,重要的是,線程棧不能動態增長,一旦用盡就沒了,這是和生成進程的fork不同的地方。由于線程棧是從進程的地址空間中map出來的一塊內存區域,原則上是線程私有的。但是同一個進程的所有線程生成的時候淺拷貝生成者的task_struct的很多字段,其中包括所有的vma,如果愿意,其它線程也還是可以訪問到的,于是一定要注意。
3.進程棧和線程棧大小的調整
進程和線程的棧分別是多大呢?首先從我們熟悉的ulimit -s說起,熟悉linux的人都應該知道通過ulimit -s可以修改棧的大小,除此之外還有getrlimit/setrlimit兩個函數:
點擊(此處)折疊或打開
intgetrlimit(intresource,struct rlimit*rlim);
intsetrlimit(intresource,conststruct rlimit*rlim);
這兩個函數當第一個參數傳入RLIMIT_STACK時,可以設置和獲取棧的大小,其作用和ulimit -s是一樣的,只是單位不同,ulimit -s的單位是kB,而這兩個函數的單位是B(字節),詳細使用方法請參考man手冊。
最后還有線程的pthread_attr_setstacksize/pthread_attr_getstacksize。
使用setrlimit和使用ulimit -s設置棧大小效果相同,這兩種方式都是針對進程棧大小設置,只不過前者只真對當前進程,后者針對當前shell;
而線程棧大小的關系就相對比較復雜點,前文說過線程大小是靜態的,是在創建時就確定了的,當然如果使用pthread_attr_setstacksize可以在創建線程時指定線程棧大小,但如果不指定線程棧的話其默認大小是什么情況呢?想要了解線程棧的大小就要看glibc的線程創建函數,具體就是pthread_create->__pthread_create_2_1->allocate_stack。具體代碼還是比較復雜的,這里簡化為一個偽代碼:
點擊(此處)折疊或打開
limit=getlimit(RLIMIT_STACK)
if(limit==RLIMIT_INFINITY)
thread.rlimit=ARCH_STACK_DEFAULT_SIZE//2M
elseifthread.rlimit
thread.rlimit=PTHREAD_STACK_MIN
可以看出,線程默認棧大小和進程棧大小的關系:
1)如果ulimit(setrlimit)設置大小大于16k,則線程棧默認大小由ulimit(setrlimit)決定;
2)如果ulimit(setrlimit)設置大小小于16k,則線程棧默認大小為16;
3)如果ulimit(setrlimit)設置大小為無限制,則線程棧默認大小為2M;
所以我們如果使用ulimit設置進程棧大小是無限大其實棧大小反而相對比較小,這是為什么呢?前面我們已經講過線程棧和進程棧的位置不同,線程棧其實是在進程的堆上分配的,并且不會動態增加,所以不可能設置一個無限大小的線程棧。
最后,我們再對進程棧和線程棧做一下總結和說明:
(1)ulimit -s決定進程棧的大小,但不是嚴格相等(實際測試稍大于ulimit -s設置);
(2)創建線程時如果通過pthread_attr_setstacksize設置了線程棧大小,則使用該屬性創建的線程棧大小就為其設置的值,但不影響線程默認屬性的棧大小值,也不影響ulimit -s的值。
(3)線程一旦創建就無法在修改其棧大小了,即使使用setrlimit。
(4)pthread_attr_setstacksize/pthread_attr_getstacksize的作用是獲取和設置線程屬性中的棧大小的,而不獲取設置線程棧大小的。可以再創建前設置好線程屬性,這樣使用該屬性創建線程就能影響線程的棧大小了。但通過pthread_attr_init,pthread_attr_getstacksize是無法獲取當前線程棧大小的,只能獲取默認屬性的線程棧大小,其值未必就是當前線程棧大小。
-
Linux
+關注
關注
87文章
11320瀏覽量
209845 -
線程
+關注
關注
0文章
505瀏覽量
19705 -
內存映射
+關注
關注
0文章
14瀏覽量
7438 -
進程
+關注
關注
0文章
203瀏覽量
13965
發布評論請先 登錄
相關推薦
評論