1.進(jìn)程棧
進(jìn)程棧是屬于用戶態(tài)棧,和進(jìn)程虛擬地址空間(Virtual Address Space)密切相關(guān)。那我們先了解下什么是虛擬地址空間:在32位機(jī)器下,虛擬地址空間大小為4G。這些虛擬地址通過頁表(Page Table)映射到物理內(nèi)存,頁表由操作系統(tǒng)維護(hù),并被處理器的內(nèi)存管理單元(MMU)硬件引用。每個進(jìn)程都擁有一套屬于它自己的頁表,因此對于每個進(jìn)程而言都好像獨(dú)享了整個虛擬地址空間。
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)程棧的初始化大小是由編譯器和鏈接器計算出來的,但是棧的實(shí)時大小并不是固定的,Linux內(nèi)核會根據(jù)入棧情況對棧區(qū)進(jìn)行動態(tài)增長(其實(shí)也就是添加新的頁表)。但是并不是說棧區(qū)可以無限增長,它也有最大限制RLIMIT_STACK (一般為8M),我們可以通過ulimit來查看或更改RLIMIT_STACK的值。
【擴(kuò)展閱讀】:進(jìn)程棧的動態(tài)增長實(shí)現(xiàn)
進(jìn)程在運(yù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ā)生了什么事情,這是一種將棧擴(kuò)展到所需大小的常規(guī)機(jī)制。然而,如果達(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)核的角度來說,其實(shí)它并沒有線程的概念。Linux把所有線程都當(dāng)做進(jìn)程來實(shí)現(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)存描述符。
點(diǎn)擊(此處)折疊或打開
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的時候生成的,實(shí)際上就是復(fù)制了父親的stack空間地址,然后寫時拷貝(cow)以及動態(tài)增長。然而對于主線程生成的子線程而言,其stack將不再是這樣的了,而是事先固定下來的,使用mmap系統(tǒng)調(diào)用(實(shí)際上是進(jìn)程的堆的一部分),它不帶有VM_STACK_FLAGS標(biāo)記。這個可以從glibc的nptl/allocatestack.c中的allocate_stack()函數(shù)中看到:
點(diǎn)擊(此處)折疊或打開
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ù):
點(diǎn)擊(此處)折疊或打開
intgetrlimit(intresource,struct rlimit*rlim);
intsetrlimit(intresource,conststruct rlimit*rlim);
這兩個函數(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ù)雜點(diǎn),前文說過線程大小是靜態(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ù)雜的,這里簡化為一個偽代碼:
點(diǎn)擊(此處)折疊或打開
limit=getlimit(RLIMIT_STACK)
if(limit==RLIMIT_INFINITY)
thread.rlimit=ARCH_STACK_DEFAULT_SIZE//2M
elseifthread.rlimit
thread.rlimit=PTHREAD_STACK_MIN
可以看出,線程默認(rèn)棧大小和進(jìn)程棧大小的關(guān)系:
1)如果ulimit(setrlimit)設(shè)置大小大于16k,則線程棧默認(rèn)大小由ulimit(setrlimit)決定;
2)如果ulimit(setrlimit)設(shè)置大小小于16k,則線程棧默認(rèn)大小為16;
3)如果ulimit(setrlimit)設(shè)置大小為無限制,則線程棧默認(rèn)大小為2M;
所以我們?nèi)绻褂胾limit設(shè)置進(jìn)程棧大小是無限大其實(shí)棧大小反而相對比較小,這是為什么呢?前面我們已經(jīng)講過線程棧和進(jìn)程棧的位置不同,線程棧其實(shí)是在進(jìn)程的堆上分配的,并且不會動態(tài)增加,所以不可能設(shè)置一個無限大小的線程棧。
最后,我們再對進(jìn)程棧和線程棧做一下總結(jié)和說明:
(1)ulimit -s決定進(jìn)程棧的大小,但不是嚴(yán)格相等(實(shí)際測試稍大于ulimit -s設(shè)置);
(2)創(chuàng)建線程時如果通過pthread_attr_setstacksize設(shè)置了線程棧大小,則使用該屬性創(chuàng)建的線程棧大小就為其設(shè)置的值,但不影響線程默認(rèn)屬性的棧大小值,也不影響ulimit -s的值。
(3)線程一旦創(chuàng)建就無法在修改其棧大小了,即使使用setrlimit。
(4)pthread_attr_setstacksize/pthread_attr_getstacksize的作用是獲取和設(shè)置線程屬性中的棧大小的,而不獲取設(shè)置線程棧大小的??梢栽賱?chuàng)建前設(shè)置好線程屬性,這樣使用該屬性創(chuàng)建線程就能影響線程的棧大小了。但通過pthread_attr_init,pthread_attr_getstacksize是無法獲取當(dāng)前線程棧大小的,只能獲取默認(rèn)屬性的線程棧大小,其值未必就是當(dāng)前線程棧大小。
-
Linux
+關(guān)注
關(guān)注
87文章
11310瀏覽量
209620 -
線程
+關(guān)注
關(guān)注
0文章
505瀏覽量
19695 -
內(nèi)存映射
+關(guān)注
關(guān)注
0文章
14瀏覽量
7416 -
進(jìn)程
+關(guān)注
關(guān)注
0文章
203瀏覽量
13962
發(fā)布評論請先 登錄
相關(guān)推薦
評論