您好,登錄后才能下訂單哦!
本篇內容主要講解“進程棧、線程棧、內核棧分別是什么”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“進程棧、線程棧、內核棧分別是什么”吧!
進程描述符 task_struct
線程創建的時候,加上了 CLONE_VM 標記,這樣 線程的內存描述符 將直接指向 父進程的內存描述符。
內存描述符mm_struct
進程棧:stack
線程棧:使用mmap系統調用分配的空間,但是mmap分配的系統空間是什么呢?也就是上圖中的mmap區域或者說共享的內存映射區域是什么呢?它的方向是向上生長還是向下生長的?
mmap其實和堆一樣,實際上可以說他們都是動態內存分配,但是嚴格來說mmap區域并不屬于堆區,反而和堆區會爭用虛擬地址空間。
這里要提到一個很重要的概念,內存的延遲分配,只有在真正訪問一個地址的時候才建立這個地址的物理映射,這是Linux內存管理的基本思想。Linux內核在用戶申請內存的時候,只是給它分配了一個線性區(也就是虛擬內存),并沒有分配實際物理內存;只有當用戶使用這塊內存的時候,內核才會分配具體的物理頁面給用戶,這時候才占用寶貴的物理內存。內核釋放物理頁面是通過釋放先行區,找到其對應的物理頁面,將其全部釋放的過程。
struct mm_struct { struct vm_area_struct *mmap; /* 內存區域鏈表 */ 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; /* 可執行頁面數目 VM_EXEC & ~VM_WRITE */ unsigned long stack_vm; /* 棧區頁面數目 VM_GROWSUP/DOWN */ unsigned long def_flags; unsigned long start_code, end_code, start_data, end_data; /* 代碼段、數據段 起始地址和結束地址 */ unsigned long start_brk, brk, start_stack; /* 棧區 的起始地址,堆區 起始地址和結束地址 */ unsigned long arg_start, arg_end, env_start, env_end; /* 命令行參數 和 環境變量的 起始地址和結束地址 */ ... /* Architecture-specific MM context */ mm_context_t context; /* 體系結構特殊數據 */ /* Must use atomic bitops to access the bits */ unsigned long flags; /* 狀態標志位 */ ... /* Coredumping and NUMA and HugePage 相關結構體 */ };
為什么需要區分這些棧,其實都是設計上的問題。這里就我看到過的一些觀點進行匯總,供大家討論:
為什么需要單獨的進程內核棧?
所有進程運行的時候,都可能通過系統調用陷入內核態繼續執行。假設第一個進程 A 陷入內核態執行的時候,需要等待讀取網卡的數據,主動調用 schedule()
讓出 CPU;此時調度器喚醒了另一個進程 B,碰巧進程 B 也需要系統調用進入內核態。那問題就來了,如果內核棧只有一個,那進程 B 進入內核態的時候產生的壓棧操作,必然會破壞掉進程 A 已有的內核棧數據;一但進程 A 的內核棧數據被破壞,很可能導致進程 A 的內核態無法正確返回到對應的用戶態了;
為什么需要單獨的線程棧?
此時 A1 的棧指針 esp 如果為初始值 0x7ffc80000000,則線程 A1 一但出現函數調用,必然會破壞父進程 A 已入棧的數據。
如果此時線程 A1 的棧指針和父進程最后更新的值一致,esp 為 0x7ffc8000FF00,那線程 A1 進行一些函數調用后,棧指針 esp 增加到 0x7ffc8000FFFF,然后線程 A1 休眠;調度器再次換成父進程 A 執行,那這個時候父進程的棧指針是應該為 0x7ffc8000FF00 還是 0x7ffc8000FFFF 呢?無論棧指針被設置到哪個值,都會有問題不是嗎?
Linux 調度程序中并沒有區分線程和進程,當調度程序需要喚醒”進程”的時候,必然需要恢復進程的上下文環境,也就是進程棧;但是線程和父進程完全共享一份地址空間,如果棧也用同一個那就會遇到以下問題。假如進程的棧指針初始值為 0x7ffc80000000;父進程 A 先執行,調用了一些函數后棧指針 esp 為 0x7ffc8000FF00,此時父進程主動休眠了;接著調度器喚醒子線程 A1:
進程和線程是否共享一個內核棧?
No,線程和進程創建的時候都調用 dup_task_struct
來創建 task 相關結構體,而內核棧也是在此函數中 alloc_thread_info_node
出來的。因此雖然線程和進程共享一個地址空間 mm_struct
,但是并不共享一個內核棧。
為什么需要單獨中斷棧?
這個問題其實不對,ARM 架構就沒有獨立的中斷棧。
進程空間中堆和棧的區別:
空間大小:棧系統指定大小限制在8M(M 級別),棧是連續空間;堆沒有限定,是不連續存儲空間,靠鏈表鏈接。
分配方式:堆都是程序員代碼中動態分配和回收的,沒有回收會產生內存泄露;棧有2種分配方式:靜態分配和動態分配。靜態分配是編譯器完成的,比如局部變量的分配。動態分配由alloca函數進行分配,但是棧的動態分配和堆是不同的,他的動態分配是由編譯器進行釋放,無需我們手工實現。
分配效率:棧分配專門的寄存器存放棧的地址,壓棧出棧都有專門的指令執行;堆是通過調用庫函數
棧:在函數調用時,第一個進棧的是主函數中后的下一條指令(函數調用語句的下一條可執行語句)的地址,然后是函數的各個參數,在大多數的 C 編譯器中,參數是由右往左入棧的,然后是函數中的局部變量。注意靜態變量是不入棧的。
當本次函數調用結束后,局部變量先出棧,然后是參數,最后棧頂指針指向最開始存的地址,也就是主函數中的下一條指令,程序由該點繼續運行。
堆:一般是在堆的頭部用一個字節存放堆的大小。堆中的具體內容有程序員安排。
總之,棧比堆效率高,但沒有堆靈活,優先使用棧,大內存使用堆。
char a[] = "hello"; //字符數組a的容量是6個字符,其內容為hello。a的內容可以改變,如a[0]= ‘X’
char *p = "world";//指針p指向常量字符串“world”(位于靜態存儲區,內容為world),常量字符串的內容是不可以被修改的。
/** *內核空間 *棧:grow down,大小系統設置~8M,連續空間,編譯器自動分配 *Memory Mapping Seg:堆棧共享,線程棧 *堆:grow up,大小硬件定,不連續空間,程序員malloc *BSS:Block Started by Symbol,未初始化的全局變量和靜態變量(靜態data區) *數據段:存放已初始化的全局變量、靜態變量(全局和局部)、const常量數據(常量data區) *代碼段:存放CPU執行的機器指令,代碼區是可共享,并且是只讀的。這部分區域的大小在程序運行前就已經確定 **/ #include <string> int a=0; //數據段:全局初始化變量 char *p1; //BSS:全局未初始化變量 void main() { int b;//棧 char s[] = "abc"; //棧 char *p2; //棧 char *p3="123456"; //123456\0在常量區(代碼段??),p3在棧上。 static int c=0; //數據段:全局(靜態)初始化區 *p1 = (char*)malloc(10); //分配得來的10字節區域在堆上 *p2 = (char*)malloc(20); //分配得來的20字節區域在堆上。 strcpy(p1,"123456"); //123456\0放在常量區,編譯器可能會將它與p3所向"123456\0"優化成一個地方。 } //const 常量 或右值常量如"123456"放在數據段還是代碼段??
malloc與free是C++/C語言的標準庫函數,new/delete是C++的運算符。它們都可用于申請動態內存和釋放內存。但是new/delete會調用對象的構造和析構函數。
到此,相信大家對“進程棧、線程棧、內核棧分別是什么”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。