深入理解 Linux 內核中的棧

前言

雖然我也很想講X86_64體系助隧,無奈這樣的資料的確不多,因此本文還是本著學習的態(tài)度滑沧,探究早已經過時的X86體系并村。

本文參考自此文巍实,該博主對棧的數據結構、棧的作用等進行了闡述哩牍,其中涉及了函數棧幀的相關知識功茴,這部分內容我沒有轉載夸浅,我僅僅轉載了我感興趣的進程棧,線程棧等部分,并在其中融入了自己的理解和補充吕漂。

原文作者將Linux的棧劃分為:

  • 進程棧
  • 線程棧
  • 內核棧
  • 中斷棧

顯然是缺少了信號棧母怜,有關信號棧掷倔,在我翻譯的信號相關的linux手冊中啊奄,曾經多次的提及:
linux手冊翻譯——signal(7)
linux手冊翻譯——sigaction(2)
linux手冊翻譯——sigreturn(2)
linux手冊翻譯——sigaltstack(2)

從實現上說,信號棧其實類似于線程棧处窥,都是可以用用戶自己的指定的嘱吗。

此外,需要讀者有對進程的虛擬地址空間有一定的了解滔驾,相關參考文獻谒麦,可以說非常多了:
http://www.reibang.com/p/174c1da40c03
https://www.cnblogs.com/clover-toeic/p/3754433.html

稍微總結一下:
在X86體系中,虛擬內存空間大小為4GB哆致,可以說是非常小了绕德,Linux將其分為了兩部分,即高1G字節(jié)的內核空間和低3G字節(jié)的用戶空間摊阀,其中內核空間是由所有的進程共享的耻蛇,在實現上就是所有進程的內核空間的頁表是共享使用的。

用戶空間胞此,或者說進程的地址空間有自己的標準布局臣咖,由各個內存段組成,每個內存段就是一個VMA結構漱牵,本質上全部都是用過mmap是實現的夺蛇,布局如圖:



包括了:

  • 程序段 (Text Segment):可執(zhí)行文件代碼的內存映射
  • 數據段 (Data Segment):可執(zhí)行文件的已初始化全局變量的內存映射
  • BSS段 (BSS Segment):未初始化的全局變量或者靜態(tài)變量(用零頁初始化)
  • 堆區(qū) (Heap) : 存儲動態(tài)內存分配,匿名的內存映射
  • 棧區(qū) (Stack) : 進程用戶空間棧酣胀,由編譯器自動分配釋放刁赦,存放函數的參數值、局部變量的值等
  • 映射段(Memory Mapping Segment):任何內存映射文件

一闻镶、進程棧

進程棧甚脉,就是進程地址空間當中的棧區(qū)!

進程棧的初始化大小是由編譯器和鏈接器計算出來的铆农,Linux內核會根據入棧情況對棧區(qū)進行動態(tài)增長(其實也就是添加新的頁表)牺氨。進程棧是有最大值的,此值由rlimit的值進行限制,可通過shell命令ulimit -s查看波闹,默認是8Mb酝豪。關于rlimit,可以查看其linux手冊精堕!

以上結論在X86_64上依然是適用的,如下測試程序:

/* file name: stacksize.c */

void *orig_stack_pointer;

void blow_stack() {
    blow_stack();
}

int main() {
    __asm__("movq %rsp, orig_stack_pointer");

    blow_stack();
    return 0;
}

編譯運行結果如下:


【擴展閱讀】:進程棧的動態(tài)增長實現
進程在運行的過程中蒲障,通過不斷向棧區(qū)壓入數據歹篓,當超出棧區(qū)容量時,就會耗盡棧所對應的內存區(qū)域揉阎,這將觸發(fā)一個 缺頁異常 (page fault)庄撮。通過異常陷入內核態(tài)后,異常會被內核的 expand_stack() 函數處理毙籽,進而調用 acct_stack_growth() 來檢查是否還有合適的地方用于棧的增長洞斯。

如果棧的大小低于 RLIMIT_STACK(通常為8MB),那么一般情況下棧會被加長坑赡,程序繼續(xù)執(zhí)行烙如,感覺不到發(fā)生了什么事情,這是一種將棧擴展到所需大小的常規(guī)機制毅否。然而亚铁,如果達到了最大棧空間的大小螟加,就會發(fā)生 棧溢出(stack overflow)徘溢,進程將會收到內核發(fā)出的 段錯誤(segmentation fault) 信號。

動態(tài)棧增長是唯一一種訪問未映射內存區(qū)域而被允許的情形捆探,其他任何對未映射內存區(qū)域的訪問都會觸發(fā)頁錯誤然爆,從而導致段錯誤。一些被映射的區(qū)域是只讀的黍图,因此企圖寫這些區(qū)域也會導致段錯誤曾雕。

如今的內核是如何處理的需要閱讀源碼,但是原理應該大差不多

上面對進程的地址空間有個比較全局的介紹雌隅,那我們看下 Linux 內核中是怎么體現上面內存布局的翻默。內核使用內存描述符來表示進程的地址空間,該描述符表示著進程所有地址空間的信息恰起。內存描述符由 mm_struct 結構體表示修械,下面給出內存描述符結構中各個域的描述,請大家結合前面的 進程內存段布局 圖一起看:

struct mm_struct {
    struct vm_area_struct *mmap;           /* 內存區(qū)域鏈表 */
    struct rb_root mm_rb;                  /* VMA 形成的紅黑樹 */
    ...
    struct list_head mmlist;               /* 所有 mm_struct 形成的鏈表 */
    ...
    unsigned long total_vm;                /* 全部頁面數目 */
    unsigned long locked_vm;               /* 上鎖的頁面數據 */
    unsigned long pinned_vm;               /* Refcount permanently increased */
    unsigned long shared_vm;               /* 共享頁面數目 Shared pages (files) */
    unsigned long exec_vm;                 /* 可執(zhí)行頁面數目 VM_EXEC & ~VM_WRITE */
    unsigned long stack_vm;                /* 棧區(qū)頁面數目 VM_GROWSUP/DOWN */
    unsigned long def_flags;
    unsigned long start_code, end_code, start_data, end_data;    /* 代碼段检盼、數據段 起始地址和結束地址 */
    unsigned long start_brk, brk, start_stack;                   /* 棧區(qū) 的起始地址肯污,堆區(qū) 起始地址和結束地址 */
    unsigned long arg_start, arg_end, env_start, env_end;        /* 命令行參數 和 環(huán)境變量的 起始地址和結束地址 */
    ...
    /* Architecture-specific MM context */
    mm_context_t context;                  /* 體系結構特殊數據 */

    /* Must use atomic bitops to access the bits */
    unsigned long flags;                   /* 狀態(tài)標志位 */
    ...
    /* Coredumping and NUMA and HugePage 相關結構體 */
};

二、線程棧

Linux可以說很好的貫徹了,線程是任務的調度單位蹦渣,進程是資源分配單位的理念哄芜,在實現上,其實是只有task的概念柬唯,并對應了task_struct結構认臊,而進程其實是一個task組,也就是他是一組task的集合锄奢,所有的task共享內存資源失晴,即mm_struct,其中task組的第一個task拘央,我們稱之為task組的leader涂屁,即主線程!

因此在linux中線程對應單個task灰伟,每個線程擁有一個task_struct拆又,進程就是一組task,每個進程擁有一個mm_struct栏账,進程中的線程共享mm_struct結構帖族!

我們在創(chuàng)建線程時,會調用clone系統(tǒng)調用发笔,clone系統(tǒng)調用也是fork的底層實現盟萨,如果要創(chuàng)建線程,那就需要帶上CLONE_VM標志了讨,此時捻激,新創(chuàng)建的task,將會直接共享父親的mm_struct:

  if (clone_flags & CLONE_VM) {
    /*
     * current 是父進程而 tsk 在 fork() 執(zhí)行期間是共享子進程
     */
    atomic_inc(&current->mm->mm_users);
    tsk->mm = current->mm;
  }

從上面的描述可以看出前计,諸多線程之間是共享棧區(qū)的胞谭,但是顯然他們不可能一起使用棧區(qū),否則就混亂了男杈!因此進程棧其實是線程組leader的椪梢伲空間。

因此線程棧是需要我們在創(chuàng)建線程時指定的伶棒!翻閱pthread_create()的源碼旺垒,其調用了:
allocate_stack (iattr, &pd, &stackaddr, &stacksize);
函數來申請棧空間肤无,而allocate_stack最終又調用了:
mem = __mmap (NULL, size, (guardsize == 0) ? prot : PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
因此線程棧先蒋,其實是通過mmap在文件映射區(qū)申請的匿名頁。

此外需要注意的是宛渐,線程的棧是不能動態(tài)增長的竞漾,指定了多大眯搭,用完就完了!當然了业岁,默認情況下是和主線程保持一致鳞仙,都是8Mb

三、內核棧

在一個線程的執(zhí)行周期中,必然會通過到系統(tǒng)調用陷入內核笔时。在執(zhí)行系統(tǒng)調用陷入內核之后棍好,這些內核代碼所使用的棧并不是原先用戶空間中的棧,而是一個單獨內核空間的棧糊闽,這個稱作內核棧梳玫。每當我們創(chuàng)建新的線程的時候都會一同創(chuàng)建線程的內核棧,實現上是通過 slab 分配器從 thread_info_cache 緩存池中分配出來右犹,其大小為 THREAD_SIZE,一般來說是一個頁大小 4K姚垃;

眾所周知念链,我們每個線程除擁有一個task_struct結構之外,還有一個用于找到task_struct的thread_info結構积糯,而thread_info被寫入在了線程內核棧的最低地址處掂墓,其使用了thread_union的聯合體結構來實現:

union thread_union {                                   
        struct thread_info thread_info;                
        unsigned long stack[THREAD_SIZE/sizeof(long)];
};  

具體關系如圖:



這里有一個小技巧,直接將 esp 的地址與上 ~(THREAD_SIZE - 1) 后即可直接獲得 thread_info 的地址看成。由于 thread_union 結構體是從 thread_info_cache 的 Slab 緩存池中申請出來的君编,而 thread_info_cache 在 kmem_cache_create 創(chuàng)建的時候,保證了地址是 THREAD_SIZE 對齊的川慌。因此只需要對棧指針進行 THREAD_SIZE 對齊吃嘿,即可獲得 thread_union 的地址,也就獲得了 thread_union 的地址梦重。成功獲取到 thread_info 后兑燥,直接取出它的 task 成員就成功得到了 task_struct。其實上面這段描述琴拧,也就是 current 宏的實現方法:

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_info的區(qū)域降瞳,雖然此時"棧溢出"還沒有發(fā)生,但thread_info的數據結構會受到破壞蚓胸,可能造成這個線程之后無法正常運行挣饥。

從Linux 4.1開始,thread_info就在逐漸被簡化沛膳,直到4.9版本徹底不再通過thread_info獲取task_struct指針扔枫,而thread_info本身也被移入了task_struct結構體中,所以這個問題也就不復存在了于置。

四茧吊、中斷棧

進程陷入內核態(tài)的時候贞岭,需要內核棧來支持內核函數調用。中斷也是如此搓侄,當系統(tǒng)收到中斷事件后瞄桨,進行中斷處理的時候,也需要中斷棧來支持函數調用讶踪。由于系統(tǒng)中斷的時候芯侥,系統(tǒng)當然是處于內核態(tài)的,所以中斷棧是可以和內核棧共享的乳讥。但是具體是否共享柱查,這和具體處理架構密切相關。

X86 上中斷棧就是獨立于內核棧的云石;獨立的中斷棧所在內存空間的分配發(fā)生在 arch/x86/kernel/irq_32.c 的 irq_ctx_init() 函數中(如果是多處理器系統(tǒng)唉工,那么每個處理器都會有一個獨立的中斷棧),函數使用 __alloc_pages 在低端內存區(qū)分配 2個物理頁面汹忠,也就是8KB大小的空間淋硝。有趣的是,這個函數還會為 softirq 分配一個同樣大小的獨立堆棧宽菜。如此說來谣膳,softirq 將不會在 hardirq 的中斷棧上執(zhí)行,而是在自己的上下文中執(zhí)行铅乡。

而 ARM 上中斷棧和內核棧則是共享的继谚;中斷棧和內核棧共享有一個負面因素,如果中斷發(fā)生嵌套阵幸,可能會造成棧溢出花履,從而可能會破壞到內核棧的一些重要數據,所以椙揉郑空間有時候難免會捉襟見肘臭挽。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市咬腕,隨后出現的幾起案子欢峰,更是在濱河造成了極大的恐慌,老刑警劉巖涨共,帶你破解...
    沈念sama閱讀 218,607評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件纽帖,死亡現場離奇詭異,居然都是意外死亡举反,警方通過查閱死者的電腦和手機懊直,發(fā)現死者居然都...
    沈念sama閱讀 93,239評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來火鼻,“玉大人室囊,你說我怎么就攤上這事雕崩。” “怎么了融撞?”我有些...
    開封第一講書人閱讀 164,960評論 0 355
  • 文/不壞的土叔 我叫張陵盼铁,是天一觀的道長。 經常有香客問我尝偎,道長饶火,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,750評論 1 294
  • 正文 為了忘掉前任致扯,我火速辦了婚禮肤寝,結果婚禮上,老公的妹妹穿的比我還像新娘抖僵。我一直安慰自己鲤看,他們只是感情好,可當我...
    茶點故事閱讀 67,764評論 6 392
  • 文/花漫 我一把揭開白布耍群。 她就那樣靜靜地躺著刨摩,像睡著了一般。 火紅的嫁衣襯著肌膚如雪世吨。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,604評論 1 305
  • 那天呻征,我揣著相機與錄音耘婚,去河邊找鬼。 笑死陆赋,一個胖子當著我的面吹牛沐祷,可吹牛的內容都是我干的。 我是一名探鬼主播攒岛,決...
    沈念sama閱讀 40,347評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼赖临,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了灾锯?” 一聲冷哼從身側響起兢榨,我...
    開封第一講書人閱讀 39,253評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎顺饮,沒想到半個月后吵聪,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,702評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡兼雄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,893評論 3 336
  • 正文 我和宋清朗相戀三年吟逝,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片赦肋。...
    茶點故事閱讀 40,015評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡块攒,死狀恐怖励稳,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情囱井,我是刑警寧澤驹尼,帶...
    沈念sama閱讀 35,734評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站琅绅,受9級特大地震影響扶欣,放射性物質發(fā)生泄漏。R本人自食惡果不足惜千扶,卻給世界環(huán)境...
    茶點故事閱讀 41,352評論 3 330
  • 文/蒙蒙 一料祠、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧澎羞,春花似錦髓绽、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,934評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至括饶,卻和暖如春株茶,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背图焰。 一陣腳步聲響...
    開封第一講書人閱讀 33,052評論 1 270
  • 我被黑心中介騙來泰國打工启盛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人技羔。 一個月前我還...
    沈念sama閱讀 48,216評論 3 371
  • 正文 我出身青樓僵闯,卻偏偏與公主長得像,于是被迫代替她去往敵國和親藤滥。 傳聞我的和親對象是個殘疾皇子鳖粟,可洞房花燭夜當晚...
    茶點故事閱讀 44,969評論 2 355

推薦閱讀更多精彩內容