栈空间_Linux中的进程栈和线程栈
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),那么一般情況下棧會被加長,程序繼續執行,感覺不到發生了什么事情,這是一種將棧擴展到所需大小的常規機制。然而,如果達到了最大??臻g的大小,就會發生 棧溢出(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兩個函數:
這兩個函數當第一個參數傳入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。具體代碼還是比較復雜的,這里簡化為一個偽代碼:
可以看出,線程默認棧大小和進程棧大小的關系:
所以我們如果使用ulimit設置進程棧大小是無限大其實棧大小反而相對比較小,這是為什么呢?前面我們已經講過線程棧和進程棧的位置不同,線程棧其實是在進程的堆上分配的,并且不會動態增加,所以不可能設置一個無限大小的線程棧。
最后,我們再對進程棧和線程棧做一下總結和說明:
以上有不足的地方歡迎指出討論,覺得不錯的朋友希望能得到您的轉發支持,同時可以持續關注我,每天分享Linux C/C++后臺開發干貨內容!
最后,如果覺得學習資料難找的話,可以添加小編的 LinuxC/C++交流群 ,期待你的加入~
總結
以上是生活随笔為你收集整理的栈空间_Linux中的进程栈和线程栈的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 怎么用手机进入路由器设置如何手机进入路由
- 下一篇: iPhone14可以MagSafe无线充