一、什麼是 User space 與 Kernel space?算法
二、Linux 下一個進程裏典型的內存佈局是怎樣的?編程
三、什麼是棧區?數組
四、什麼是堆區?安全
五、malloc 算法是如何實現的?編輯器
六、Linux 系統下,有幾種堆空間分配方式?函數
上面幾個問題,你內心有答案嗎?若是沒有,跟我一塊兒來探究一下吧佈局
現代的應用程序都運行在一個內存空間裏,在 32 位系統中,這個內存空間擁有 4GB (2 的 32 次方)的尋址能力。性能
儘管如今的內存空間都號稱是平坦的,但實際上內存仍然在不一樣的地址區間有着不一樣的地位,例如,大多數操做系統都會將 4GB 的內存空間一部分挪給內核使用,應用程序沒法直接訪問這一段內存,這一部份內存地址被稱爲 內核空間。spa
Windows 在默認的狀況下會將高地址的 2GB 空間分配給內核(也能夠配置 1GB)。
Linux 默認狀況下將高地址的 1GB 空間分配給內核。
用戶使用的剩下的 2GB 或 3GB 的內存空間稱爲用戶空間。操作系統
爲何要區份內核空間和用戶空間?
大體有三點因素:
第一點:操做系統的數據都是存放於系統空間的,用戶進程的數據是存放於用戶空間的;
第二點:分開來存放,就讓系統的數據和用戶的數據互不干擾,保證系統的穩定性,而且管理上很方便;
第三點:也是重要的一點,將用戶的數據和系統的數據隔離開,就能夠對兩部分的數據的訪問進行控制。這樣就能夠確保用戶程序不能隨便操做系統的數據,這樣防止用戶程序誤操做或者是惡意破壞系統。
下面這一張圖,比較形象的解釋了 User space 與 Kernel space 的區別
簡單說,Kernel space 是 Linux 內核的運行空間,User space 是用戶程序的運行空間。爲了安全,它們是隔離的,即便用戶的程序崩潰了,內核也不受影響。
Kernel space 能夠執行任意命令,調用系統的一切資源;
相對來講,User space 執行的是較爲簡單的運算,執行的運算不影響其餘程序的執行,而且不能直接調用系統資源,必須經過系統接口(又稱 system call),才能向內核發出指令。
這裏補充下知乎網友@風雲評論:
其實,在用戶空間,幾乎全部內核資源在用戶空間都是能夠訪問的(必須有相應的權限),即便是操做系統內核的大腦(調度程序)。
在用戶空間裏,也有許多地址區間有特權的地位,通常來說,應用程序使用的內存空間裏有以下「默認」的區域。
棧: 棧用於維護函數調用的上下文,離開了棧,函數調用就沒法實現,棧一般在用戶空間的最高地址處分配,一般有數兆字節的大小。
堆: 堆是用來容納應用程序動態分配的內存區域,當程序使用 malloc 或者 new 分配內存的時候,獲得的內存會來自堆裏。堆一般存在棧的下方(低地址方向),在某些時候,堆也可能沒有固定統一的存儲區域。堆通常比棧大不少,能夠有幾十至數百兆字節的容量。
可執行文件映像: 存儲着可執行文件在內存裏的映像,由裝載器在裝載時將可執行文件的內存讀取或映射到這裏。
保留區: 保留區並非一個單一的內存區域,而是對內存中受到保護而禁止訪問的內存區域的總稱:例如大多數操做系統中,極小的地址一般都是不容許訪問的,如 NULL,C 語言將無效指針賦值爲 0 也是這個考慮。
動態連接庫映射區: 這個區域用於映射裝載的動態連接庫。在 Linux 下,若是可執行文件依賴其它共享庫,那麼系統就會爲它在從 0x40000000 開始的地址分配相應的空間,並將共享庫載入該空間。
剩下的還有如下幾部份組成:
(1)代碼段
(2)初始化數據段(數據段)
(3)未初始化數據段(BSS 段)
下圖是 Linux 下一個進程裏典型的內存佈局
圖中的箭頭,標明瞭幾個大小可變的尺寸增加的方向,在這裏,能夠清晰地看出
棧是由高地址向低地址增加。
堆是由低地址向高地址增加。
當棧或堆現有的大小不夠用的時候,它將按照圖中的增加方向擴大自身的尺寸,直到預留的空間被用完爲止。
在講堆和棧以前,咱們先來看一下代碼段,初始化數據段和未初始化數據段。
代碼段中存放可執行的指令,在內存中,爲了保證不會由於堆棧溢出被覆蓋,將其放在了堆棧段下面(從上圖能夠看出)。一般來說代碼段是共享的,這樣屢次反覆執行的指令只須要在內存中駐留一個副本便可,好比 C 編譯器,文本編輯器等。代碼段通常是隻讀的,程序執行時不能隨意更改指令,也是爲了進行隔離保護。
初始化數據段有時就稱之爲數據段。數據段是一個程序虛擬地址空間的一部分,包括一全局變量和靜態變量,這些變量在編程時就已經被初始化。數據段是能夠修改的,否則程序運行時變量就沒法改變了,這一點和代碼段不一樣。
數據段能夠細分爲初始化只讀區和初始化讀寫區。這一點和編程中的一些特殊變量吻合。好比全局變量 int global n = 1就被放在了初始化讀寫區,由於 global 是能夠修改的。而 const int m = 2 就會被放在只讀區,很明顯,m 是不能修改的。
未初始化數據段有時稱之爲 BSS 段,BSS 是英文 Block Started by Symbol 的簡稱,BSS 段屬於靜態內存分配。存放在這裏的數據都由內核初始化爲 0。未初始化數據段從數據段的末尾開始,存放有所有的全局變量和靜態變量並被,默認初始化爲 0,或者代碼中沒有顯式初始化。好比 static int i; 或者全局 int j; 都會被放到BSS段。
棧 (stack) 是現代計算機程序裏最爲重要的概念之一,幾乎每個程序都使用了棧,沒有棧就沒有函數,沒有局部變量,也就沒有咱們現在可以看見的全部的計算機語言。在解釋爲何棧會如此重要以前,讓咱們來先了解一下傳統的棧的定義:
在經典的計算機科學中,棧被定義爲一個特殊的容器,用戶能夠將數據壓入棧中(入棧,push,也能夠將已經壓入棧中的數據彈出(出棧, pop),但棧這個容器必須遵照一條規則:先入棧的數據後出棧(First In Last Out, FIFO),多多少少像疊成一疊的書:先疊上去的書在最下面:所以要最後才能取出。
在計算機系統中,棧則是一個具備以上屬性的動態內存區域。程序能夠將數據壓入棧中,也能夠將數據從棧頂彈出。壓棧操做使得棧增大,而彈出操做使棧減少。
在經典的操做系統裏,棧老是向下增加的。
在i386下,棧頂由稱爲 esp 的寄存器進行定位。壓棧的操做使棧頂的地址減少,彈出的操做使棧頂地址增大。
這裏棧底的地址是 0xbffff,而 esp 寄存器標明瞭棧頂,地址爲 0xbifff4。
在棧上壓入數據會致使 esp 減少,彈出數據使得 esp 增大。
棧在程序運行中具備舉足輕重的地位。最重要的,棧保存了一個函數調用所須要的維護信息,這經常被稱爲堆棧幀(Stack Frame)或活動記錄(Activate Record),堆棧幀通常包括以下幾方面內容:
一、函數的返回地址和參數。
二、臨時變量:包括函數的非靜態局部變量以及編譯器自動生成的其餘臨時變量。
三、保存的上下文:包括在函數調用先後須要保持不變的寄存器。
相對於棧,堆這片內存面臨着一個稍微複雜的行爲模式:在任意時刻,程序可能發出請求,要麼申請一段內存,要麼釋放一段已經申請過的內存,並且申請的大小從幾個字節到數 GB 都是有可能的,咱們不能假設程序會一次申請多少堆空間,所以,堆的管理顯得較爲複雜。
爲何須要堆?
光有棧,對於面向過程的程序設計還遠遠不夠,由於棧上的數據在函數返回的時候就會被釋放掉,因此沒法將數據傳遞至函數外部。而全局變量沒有辦法動態地產生,只能在編譯的時候定義,有不少狀況下缺少表現力,在這種狀況下,堆(Heap)是一種惟一的選擇。
堆是一款巨大的內存空間,經常佔據整個虛擬空間的絕大部分,在這片空間裏,程序能夠請求一塊連續的內存,並自由地使用,這塊內存在程序主動放棄以前都活一直保持有效,下面是一個申請堆空間最簡單的例子:
int main() {
char* p = (char*) malloc(233); free(p); return 0;
}
在第 3 行用 malloc 申請了 233 個字節的空間以後,程序能夠自由地使用這 233個字節,直到程序用free函數釋放它。
那麼 malloc 究竟是怎麼實現的呢?
有一種作法是,把進程的內存管理交給操做系統內核去作,既然內核管理着進程的地址空間,那麼若是它提供一個系統調用,可讓程序使用這個系統調用申請內存,不就能夠了嗎?
固然這是一種理論上可行的作法,但實際上這樣作的性能比較差,緣由在於每次程序申請或者釋放堆空間都須要進行系統調用。
咱們知道系統調用的性能開銷是很大的,當程序對堆的操做比較頻繁時,這樣作的結果是會嚴重影響程序的性能的。
比較好的作法就是:程序向操做系統申請一塊適當大小的堆空間,而後由程序本身管理這塊空間,而具體來說,管理着堆空間分配的每每是程序的運行庫。
運行庫至關因而向操做系統 「批發」 了一塊較大的堆空間,而後 「零售」 給程序用。
當所有「售完」或程序有大量的內存需求時,再根據實際需求向操做系統「進貨」。
固然運行庫在向程序零售堆空間時,必須管理它批發來的堆空間,不能把同一塊地址出售兩次,致使地址的衝突。
由第一節可知,進程的地址空間中,除了可執行文件,共享庫和棧以外,剩餘的未分配的空間均可以用來做爲堆空間。
Linux 系統下,提供兩種堆空間分配方式,兩個系統調用:brk() 系統調用 和 mmap() 系統調用
這兩種方式分配的都是虛擬內存,沒有分配物理內存。在第一次訪問已分配的虛擬地址空間的時候,發生缺頁中斷,操做系統負責分配物理內存,而後創建虛擬內存和物理內存之間的映射關係。
在標準 C 庫中,提供了malloc/free函數分配釋放內存,這兩個函數底層是由 brk,mmap,munmap 這些系統調用實現的。
C 語言形式聲明:int brk() {void* end_data_segment;}
brk() 的做用實際上就是設置進程數據段的結束地址,即它能夠擴大或者縮小數據段(Linux 下數據段和 BBS 合併在一塊兒統稱數據段)。
若是咱們將數據段的結束地址向高地址移動,那麼擴大的那部分空間就能夠被咱們使用,把這塊空間拿過來使用做爲堆空間是最多見的作法。
和 Windows 系統下的 VirtualAlloc 很類似,它的做用就是向操做系統申請一段虛擬地址空間,(堆和棧中間,稱爲文件映射區域的地方)這塊虛擬地址空間能夠映射到某個文件。
glibc 的 malloc 函數是這樣處理用戶的空間請求的:對於小於 128KB 的請求來講,它會在現有的堆空間裏面,按照堆分配算法爲它分配一塊空間並返回;對於大於128KB 的請求來講,它會使用 mmap() 函數爲它分配一塊匿名空間,而後在這個匿名空間中爲用戶分配空間。
聲明以下:
void* mmap{
void* start; size_t length; int prot; int flags; int fd; off_t offset;
}
mmap 前兩個參數分別用於指定須要申請的空間的起始地址和長度,若是起始地址設置 0,那麼 Linux 系統會自動挑選合適的起始地址。
prot/flags 參數:用於設置申請的空間的權限(可讀,可寫,可執行)以及映射類型(文件映射,匿名空間等)。
最後兩個參數用於文件映射時指定的文件描述符和文件偏移的。
瞭解了 Linux 系統對於堆的管理以後,能夠再來詳細這麼一個問題,那就是 malloc 到底一次可以申請的最大空間是多少?
爲了回答這個問題,就不得再也不回頭仔細研究一下以前的圖一。咱們能夠看到在有共享庫的狀況下,留給堆能夠用的空間還有兩處。第一處就是從 BSS 段結束到 0x40 000 000 即大約 1GB 不到的空間;
第二處是從共享庫到棧的這塊空間,大約是 2GB 不到。這兩塊空間大小都取決於棧、共享庫的大小和數量。
因而能夠估算到 malloc 最大的申請空間大約是 2GB 不到。(Linux 內核 2.4 版本)。
還有其它諸多因素會影響 malloc 的最大空間大小,好比系統的資源限制(ulimit),物理內存和交換空間的總和等。mmap 申請匿名空間時,系統會爲它在內存或交換空間中預留地址,可是申請的空間大小不能超過空閒內存+空閒交換空間的總和。
一、空閒鏈表法(即調用 malloc 分配):
就是把堆中各個空閒的塊按照鏈表的方式鏈接起來,當用戶請求一塊空間的時候,能夠遍歷整個列表,直到找到合適大小的塊而且將它拆分;當用戶釋放空間的時候將它合併到空閒鏈表中。
空閒鏈表是這樣一種結構,在堆裏的每個空閒空間的開頭(或結尾)有一個頭 (header),頭結構裏記錄了上一個 (prev) 和下一個 (next) 空閒塊的地址,也就是說,全部的空閒塊造成了一個鏈表。如圖所示。
具體實現方案:
1)malloc 函數的實質是它有一個將可用的內存塊鏈接爲一個長長的列表的所謂空閒鏈表。
2)調用 malloc()函數時,它沿着鏈接表尋找一個大到足以知足用戶請求所須要的內存塊。而後,將該內存塊一分爲二(一塊的大小與用戶申請的大小相等,另外一塊的大小就是剩下來的字節)。接下來,將分配給用戶的那塊內存存儲區域傳給用戶,並將剩下的那塊(若是有的話)返回到鏈接表上。
3)調用 free 函數時,它將用戶釋放的內存塊鏈接到空閒鏈表上。
4)到最後,空閒鏈會被切成不少的小內存片斷,若是這時用戶申請一個大的內存片斷, 那麼空閒鏈表上可能沒有能夠知足用戶要求的片斷了。因而,malloc() 函數請求延時,並開始在空閒鏈表上檢查各內存片斷,對它們進行內存整理,將相鄰的小空閒塊合併成較大的內存塊。
二、位圖法
針對空閒鏈表的弊端,另外一種分配方式顯得更加穩健。這種方式稱爲位圍(Bitmap),其核心思想是將整個堆劃分爲大量的塊(block),每一個塊的大小相同。
當用戶請求內存的時候,老是分配整數個塊的空間給用戶,第一個塊咱們稱爲已分配區域的頭(Head),其他的稱爲己分配區域的主體(Body),而咱們可使用一個整數數組來記錄塊的使用狀況,因爲每一個塊只有頭/主體/空閒三種狀態,所以僅僅須要兩位便可表示一個塊,所以稱爲位圖。
三、對象池
還有一種方法是對象池,也是把堆空間分紅了大小相等的一些塊,它是認爲某些場合每次分配的空間都相等,因此每次就直接返回一個塊的大小,它的管理方法能夠是鏈表也能夠是位圖。由於不用每次查找合適的大小的內存返回,因此效率很高。
實際上不少現實應用中,堆的分配算法每每是採起多種算法複合而成的。
好比對於 glibc 來講,它對於小於 64 字節的空間申請是採用相似於對象池的方法;
而對於大於 512 字節的空間申請採用的是最佳適配算法;對於大於 64 字節而小於 512 字節的,它會根據狀況採起上述方法中的最佳折中策略;對於大於 128KB 的申請,它會使用mmap 機制直接向操做系統申請空間。