轉自:https://durant35.github.io/2017/10/29/VM_Stacks/
Linux 中有幾種棧?各種棧的內存位置?
關於棧
- 函數調用棧的典型內存布局
- 棧幀 (Stack Frame) 的邊界由棧幀基地址指針
EBP
和 棧指針ESP
界定,EBP
指向當前棧幀底部 (高地址),在當前棧幀內位置固定;ESP
指向當前棧幀頂部 (低地址); - 當程序執行時,
ESP
會隨着數據的入棧和出棧而移動,因此函數中對大部分數據的訪問都基於EBP
進行。
- 棧幀 (Stack Frame) 的邊界由棧幀基地址指針
- 棧幀存放着參數,局部變量及恢復前一棧幀所需要的數據等。
進程棧
進程虛擬地址空間中的棧區,正指的是我們所說的進程棧。進程棧是屬於用戶態棧,和進程虛擬地址空間 (Virtual Address Space) 密切相關。

圖: 32 位系統下進程地址空間默認布局(左)和進程地址空間經典布局(右)
進程棧的初始化大小是由編譯器和鏈接器計算出來的,但是棧的實時大小並不是固定的,Linux 內核會根據入棧情況對棧區進行動態增長(其實也就是添加新的頁表)。但是並不是說棧區可以無限增長,它也有最大限制
RLIMIT_STACK
(一般為 8M),我們可以通過
ulimit 來查看或更改
RLIMIT_STACK
的值 (
stack size):
-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17gary@xxx:~$ ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 30980
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 30980
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
- 進程棧的動態增長實現
- 進程在運行的過程中,通過不斷向棧區壓入數據,當超出棧區容量時,就會耗盡棧所對應的內存區域,這將觸發一個缺頁異常 (page fault);
- 通過異常陷入內核態后,異常會被內核的 expand_stack() 函數處理,進而調用 acct_stack_growth() 來檢查是否還有合適的地方用於棧的增長:
- 如果棧的大小低於
RLIMIT_STACK
,那么一般情況下棧會被加長,程序繼續執行,感覺不到發生了什么事情,這是一種將棧擴展到所需大小的常規機制; - 如果達到了最大棧空間的大小,就會發生棧溢出 (stack overflow),進程將會收到內核發出的段錯誤(segmentation fault) 信號。
- 如果棧的大小低於
- 動態棧增長是唯一一種訪問未映射內存區域而被允許的情形,其他任何對未映射內存區域的訪問都會觸發頁錯誤,從而導致段錯誤。一些被映射的區域是只讀的,因此企圖寫這些區域也會導致段錯誤。
線程棧
從 Linux 內核的角度來說,其實它並沒有線程的概念,Linux 把所有線程都當做進程來實現,它將線程和進程不加區分的統一到了 task_struct
中;線程僅僅被視為一個與其他進程共享某些資源的進程,而是否共享地址空間幾乎是進程和 Linux 中所謂線程的唯一區別。線程創建的時候,加上了 CLONE_VM
標記,這樣線程的內存描述符將直接指向父進程的內存描述符。
- 雖然線程的地址空間和進程一樣,但是在對待其地址空間中的 stack 上還是有些區別的。
- 對於 Linux 進程或者說主線程,其 stack 是在 fork() 的時候生成的,實際上就是復制了父親的 stack 空間地址,然后寫時拷貝 (cow) 以及動態增長。
- 對於主線程生成的子線程而言,其 stack 將不再是這樣的了,而是事先固定下來的,使用 mmap()系統調用從進程的地址空間中 mmap 出來的一塊內存區域,它不帶有
VM_STACK_FLAGS
標記。- 由於線程的 mm->start_stack 棧地址和所屬進程相同,所以線程棧的起始地址並沒有存放在
task_struct
中,應該是使用pthread_attr_t
中的stackaddr
來初始化 task_struct->thread->sp(sp
指向struct pt_regs
對象,該結構體用於保存用戶進程或者線程的寄存器現場) - 重要的是,線程棧不能動態增長,一旦用盡就沒了,這是和生成進程的 fork() 不同的地方。
- 由於線程的 mm->start_stack 棧地址和所屬進程相同,所以線程棧的起始地址並沒有存放在
- 線程棧是從進程的地址空間中 mmap 出來的一塊內存區域,原則上是線程私有的;但是同一個進程的所有線程在生成的時候會淺拷貝線程生成者
task_struct
的很多字段,其中包括所有的vma
,因此如果願意,其它線程也還是可以訪問到的,於是一定要注意! - 為什么需要單獨的線程棧?不能共享同一個進程棧嗎?
- Linux 調度程序中並沒有區分線程和進程 (線程是調度的基本單位),當調度程序需要喚醒“進程”的時候,必然需要恢復進程的上下文環境,也就是進程棧;線程和父進程完全共享一份地址空間,如果棧也用同一個,那就會遇到以下問題:
- 假如進程的棧指針初始值為 0x7ffc80000000:父進程 A 先執行,調用了一些函數后棧指針
esp
為 0x7ffc8000FF00,此時父進程主動休眠了; - 接着調度器喚醒子線程 A1:
- 如果此時 A1 的棧指針
esp
為初始值 0x7ffc80000000,則線程 A1 一但出現函數調用,必然會破壞父進程 A 已入棧的數據; - 如果此時線程 A1 的棧指針和父進程最后更新的值一致,
esp
為 0x7ffc8000FF00,那線程 A1 進行一些函數調用后,棧指針esp
增加到 0x7ffc8000FFFF,然后線程 A1 休眠;調度器再次換成父進程 A 執行,那這個時候父進程的棧指針是應該為 0x7ffc8000FF00 還是 0x7ffc8000FFFF 呢? - 無論棧指針被設置到哪個值,都會有問題不是嗎?
- 如果此時 A1 的棧指針
- 假如進程的棧指針初始值為 0x7ffc80000000:父進程 A 先執行,調用了一些函數后棧指針
- Linux 調度程序中並沒有區分線程和進程 (線程是調度的基本單位),當調度程序需要喚醒“進程”的時候,必然需要恢復進程的上下文環境,也就是進程棧;線程和父進程完全共享一份地址空間,如果棧也用同一個,那就會遇到以下問題:
進程內核棧
在每一個進程的生命周期中,必然會通過到系統調用陷入內核。在執行系統調用陷入內核之后,這些內核代碼所使用的棧並不是原先進程用戶空間中的棧,而是一個單獨內核空間的棧,這個稱作進程內核棧。
進程內核棧在進程創建的時候,通過 slab 分配器 從
thread_info_cache
緩存池中分配出來,大小為THREAD_SIZE
,一般來說是一個頁大小 4K;
- 為什么需要單獨的進程內核棧?
所有進程運行的時候,都可能通過系統調用陷入內核態繼續執行:假設第一個進程 A 陷入內核態執行的時候,需要等待讀取網卡的數據,主動調用 schedule() 讓出 CPU;此時調度器喚醒了另一個進程 B,碰巧進程 B 也需要系統調用進入內核態;那問題就來了,如果內核棧只有一個,那進程 B 進入內核態的時候產生的壓棧操作,必然會破壞掉進程 A 已有的內核棧數據,一但進程 A 的內核棧數據被破壞,很可能導致進程 A 的內核態無法正確返回到對應的用戶態了。
- 進程和子線程是否共享一個進程內核棧?
線程和進程創建的時候都調用 dup_task_struct() 來創建 task 相關結構體,而內核棧也是在此函數中 alloc_thread_info_node() 出來的,因此雖然線程和進程共享一個地址空間
mm_struct
,但是並不共享一個內核棧(本來線程就是 CPU 調度的基本單位)。 - 進程內核棧與
current
當前進程 -
1
2
3
4union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};- 有了上述關聯結構后,內核可以先獲取到棧頂指針
esp
,然后通過esp
來獲取thread_info
; - 成功獲取到
thread_info
后,直接取出它的 task 成員就成功得到了task_struct
,也就是如下current
宏的實現方法:
- 有了上述關聯結構后,內核可以先獲取到棧頂指針
- current 宏的實現方法
1
2
3
4
5
6
7
8
9
10
11register 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));
}
#- 由於
thread_union
結構體是從thread_info_cache
的 Slab 緩存池中申請出來的,而thread_info_cache
在kmem_cache_create
創建的時候,保證了地址是THREAD_SIZE
對齊的; - 因此只需要對棧指針進行
THREAD_SIZE
對齊,即可獲得thread_union
的地址,也就獲得了thread_info
的地址:直接將esp
的地址與上~(THREAD_SIZE - 1)
- 由於
中斷棧
進程陷入內核態的時候,需要內核棧來支持內核函數調用;中斷也是如此,當系統收到中斷事件后,進行中斷處理的時候,也需要中斷棧來支持函數調用。
- 由於系統中斷的時候,系統當然是處於內核態的,所以中斷棧是可以和內核棧共享的(但是具體是否共享,這和具體處理架構密切相關)。
- 中斷棧獨立於內核棧
x86 上中斷棧就是獨立於內核棧的,獨立的中斷棧所在內存空間的分配發生在
arch/x86/kernel/irq_32.c
的 irq_ctx_init() 函數中(如果是多處理器系統,那么每個處理器都會有一個獨立的中斷棧)。- 函數使用 __alloc_pages() 在低端內存區分配 2 個物理頁面,也就是 8KB 大小的空間。
- 這個函數還會為 softirq 分配一個同樣大小的獨立堆棧,如此說來,softirq 將不會在 hardirq 的中斷棧上執行,而是在自己的上下文中執行。
- ARM 上中斷棧和內核棧則是共享的;中斷棧和內核棧共享有一個負面因素,如果中斷發生嵌套,可能會造成棧溢出,從而可能會破壞到內核棧的一些重要數據,所以棧空間有時候難免會捉襟見肘。
- 中斷棧獨立於內核棧
- 為什么需要單獨中斷棧?
這個問題其實不對,ARM 架構就沒有獨立的中斷棧。
References
本文標題:虛擬內存[02] Linux 中的各種棧:進程棧 線程棧 內核棧 中斷棧
文章作者:Gary
發布時間:2017-10-29, 22:01:00
最后更新:2020-03-28, 12:37:22