內存管理一直是操做系統的核心問題,它對於編程和系統管理都是異常重要。接下來會有一系列博文從實際角度給你們介紹內存管理的一系列內容,儘管這一律念比較寬泛,可是博文中列舉的示例都是來自於Linux或Windows這些32位的x86系統。做爲這系列的第一篇論文,首先簡單描述一下程序如何在內存中佈局。html
每一個多任務系統中的進程都運行在它本身的內存「沙盒」裏,這個「沙盒」就是虛擬內存空間,這在32位模式下每每指4GB內存地址塊。這些虛擬地址經過頁表映射到物理內存,這些頁表是由操做系統內核來維護而且被處理器(CPU)來查詢。每一個進程都有它本身的頁表,一旦虛擬地址啓動,這些葉表就必須適用於機器上的全部軟件程序,包括系統內核自己。所以,虛擬地址的一部分必須保存在內核中:linux
這不是意味着內核使用頁表來匹配物理內存,只是內核須要使用那部分虛擬空間用來映射任意的物理空間。內核空間的頁面在頁表上被標記爲特權級(環2或更低),所以,若是一個用戶模式下的程序試圖去訪問一頁特權級內存頁,經常會致使一個頁異常(page fault----編程中常見報錯總結)。在Linux下,內核空間在全部用戶進程中的物理內存空間映射都是一致的。任什麼時候候,內存代碼和數據,能夠被中斷處理程序和系統調用所尋址和使用。相比之下,映射爲用戶模式那部分的地址空間,隨着一個任務切換的發生,也會映射到不一樣的物理地址空間:web
上圖中,藍色區域表示映射到物理地址空間的虛擬地址空間,而白色則是還未映射的虛擬地址。地址空間中不一樣的段對於不一樣的內存段----堆、棧等等。記住,這些段僅僅表示一段內存地址範圍,和實際的硬件架構中的段不一樣。總之,下圖就是Linux下一個進程的標準段佈局:算法
當計算和程序上一切運行順利,那麼在機器上每一個進程的不一樣段的佈局基本一致,都如上圖所示,這樣有利於避免不少安全注入問題。一次注入攻擊經常須要訪問絕對內存位置:例如堆棧上的地址、函數庫的地址等等。遠程攻擊者每每是盲目選擇內存位置,主要是但願每一個進程的地址空間都是一致的,若是是這樣的話,那麼我的的隱私就受到威脅;因此,現在線程地址空間的隨機映射大行其道。Linux下,系統經過讓基地址隨機加上一個偏移地址,從而獲得棧地址、堆地址以及其餘的內存映射段地址。然而,現在的32位地址空間顯得有點緊湊,這樣就致使隨機的空間很小,進而可能影響其安全性。編程
在進程中最重要的段----堆棧段,不少編程語言下,它是用來存放局部變量和函數參數的。調用一個函數或者方法,就會在棧上壓入一個新的棧幀,而且隨着函數返回,所對應的棧幀也就被釋放掉。這種簡單的設計,主要的思想來源多是數據聽從「先進先出」的規則,這也意味着不須要複雜的數據結構來追蹤棧內容,一個簡單的棧頂指針就能夠實現內容查詢----push和pop操做是很是迅速且穩定。同時,將那些被屢次重複使用的棧內容存放到「CPU緩存中(Cache)」,能夠帶來更快的訪問速度。ubuntu
若是在內存空間中,放入過多的數據內容,會致使棧空間的耗盡。Linux下,這樣所引發的頁異常,能夠經過expand_stack()
來解決,這就會致使調用acct_stack_growth()
來檢查是否能夠增加堆棧空間。若是棧空間尺度小於RLIMIT_STACK(通常是8MB),那麼棧空間增加能夠沒有問題的執行下去。可是,若是棧空間已經達到上限,那麼咱們進行上述操做,最終會收到一個段異常(segmentation fault)----也就是「棧溢出」。若是棧增加操做順序運行,娜美操做完成後不回收縮棧大小。c#
動態堆棧增加是訪問未映射內存區域的惟一正確方式。任何其餘訪問未映射內存區域,都會觸發一個頁異常,進而致使段異常。某些映射內存區域是隻讀內存區域,所以,試圖寫這些區域一樣會致使異常。windows
在棧下面,就是內存映射段區域。這裏,內存直接將文件內容映射到內存。任何應用能夠經過調用Linux下的mmap()
系統調用或者在Windows下的CreateFileMapping()
/MapViewOfFile()
來實現。內存映射對於文件I/O操做來講是很是方便且高效的,因此它常常被用來加載動態庫。也能夠建立一個匿名內存映射,不對應任何文件,而專門用作程序數據。Linux下,若是程序經過malloc()
需求一片大的內存塊,C庫將會建立一片匿名映射空間,這裏的「大」是指大於MMAP_THRESHOLD字節,該字段的默認值是128KB,能夠經過mallopt()
動態修改。api
接下來,就是堆的介紹。堆----經常使用來提供運行時內存分配,這裏點和棧比較相似;可是,堆也能夠分配存放棧範圍以外的數據變量,這一點區別於棧。大部分語言都提供堆管理程序。所以,知足內存請求是語言運行時和內核的共同事務。在C語言中,調用堆分配空間的接口是malloc()
,以及在具備垃圾回收機制的C#語言中,對應的接口的new關鍵字。緩存
若是在堆上有足夠的內存空間知足內存調用,那麼僅僅經過語言運行時就能夠知足相關操做,而不須要調用系統內核操做。不然的話,就須要經過調用brk()
系統調用來分配更多的空間來知足需求。在咱們實際程序複雜的內存分配的模式下,堆的管理是很複雜的,須要精妙的算法來平衡速度和內存使用效率,於是,用於分配堆空間的時間消耗可能差別很大;實時系統主要經過special-purpose allocators來處理這類問題。堆在內存中示意圖以下所示:
最後,咱們來分析最下面的一系列內存段----BSS、數據段和程序代碼段。在C語言中,BSS、數據段都用來存儲靜態(static)變量。不一樣之處是,BSS中存儲的內容是未初始化的靜態變量,這些變量的值是經過在程序代碼中進行設置的。BSS內存區域是匿名的:它不會映射到任何文件。若是,你輸入static int cntActiveUsers;
語句,那麼這個變量就存放在BSS段。
數據段,存放源碼中已經被初始化的靜態變量,因此,這段內存區域不是匿名的,它被映射到文件的二進制鏡像的某個部分,而且包含這些靜態變量在源碼中被初始化的值。因此,當你輸入static int cntWorkBees = 10;
語句,則變量的內容存放於數據段且值爲10。儘管數據段映射成一個文件,它還是私有內存映射,這也意味着對內存數據的更新不會同步到所映射的文件中。不然的話,對於靜態變量的賦值,會致使磁盤上文件內容的變化。
下圖中,數據段的示例比較複雜,由於使用了指針。下例中,指針gonzo內容----也就是一個4字節內存地址----存放在數據段。可是,它指向的實際字符串並不存放在數據段,而是在代碼段,也就是一個只讀段,也就是存放全部代碼和全部字符串的區域。代碼段一樣映射內存中的二進制文件,可是要寫代碼段會致使一個段異常。這有助於阻止不少指針錯誤,下圖是這些段的示意圖:
你能夠經過閱讀Linux源碼文件中的/proc/pid_of_process/maps來進一步瞭解內存區域,請記住,一個段可能包含不少不一樣的內存區域。例如,每一個內存映射文件一般在mmap段中都有本身的區域,而動態庫具備相似於BSS和數據的額外區域。你能夠藉助Linux下的工具nm和objdump來查閱一個目標文件的符號、地址、段等等。最終的虛擬地址空間的佈局在Linux中是一種靈活的方式,Linux下的經典內存佈局以下: