虛擬內存[02] Linux 中的各種棧:進程棧 線程棧 內核棧 中斷棧【轉】


轉自:https://durant35.github.io/2017/10/29/VM_Stacks/

 Linux 中有幾種棧?各種棧的內存位置?

關於棧

  • 函數調用棧的典型內存布局
    • 棧幀 (Stack Frame) 的邊界由棧幀基地址指針 EBP 和 棧指針 ESP 界定,EBP指向當前棧幀底部 (高地址),在當前棧幀內位置固定;ESP指向當前棧幀頂部 (低地址);
    • 當程序執行時,ESP會隨着數據的入棧和出棧而移動,因此函數中對大部分數據的訪問都基於EBP進行。
  • 棧幀存放着參數局部變量恢復前一棧幀所需要的數據等。

進程棧

 進程虛擬地址空間中的棧區,正指的是我們所說的進程棧進程棧是屬於用戶態棧,和進程虛擬地址空間 (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
    17
    gary@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->spsp 指向 struct pt_regs 對象,該結構體用於保存用戶進程或者線程的寄存器現場)
      • 重要的是,線程棧不能動態增長,一旦用盡就沒了,這是和生成進程的 fork() 不同的地方。
  • 線程棧是從進程的地址空間中 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 呢?
        • 無論棧指針被設置到哪個值,都會有問題不是嗎?

進程內核棧

 在每一個進程的生命周期中,必然會通過到系統調用陷入內核。在執行系統調用陷入內核之后,這些內核代碼所使用的棧並不是原先進程用戶空間中的棧,而是一個單獨內核空間的棧,這個稱作進程內核棧

 進程內核棧在進程創建的時候,通過 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當前進程
    • 內核執行的大多數操作還是和某個特定的進程相關,內核代碼可通過訪問current來獲得當前進程。

       內核開發者設計了一種能找到運行在相關 CPU 上的當前進程的機制;因為 current的引用會頻繁發生,因此這種機制必須是快速的。

    • 內核將進程內核棧的頭部一段空間用於存放thread_info結構體,該結構體中則記錄了對應進程的描述符task_struct
  • 1
    2
    3
    4
    union 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
    11
    register 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));
    }

    #define get_current() (current_thread_info()->task)

    #define current get_current()
    • 由於 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

·《Linux 中的各種棧:進程棧 線程棧 內核棧 中斷棧》
·《Linux虛擬地址空間布局以及進程棧和線程棧總結》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM