在多任務操做系統中,每一個進程都運行在屬於本身的內存沙盤中。這個沙盤就是虛擬地址空間(Virtual Address Space),在32位模式下它是一個4GB的內存地址塊。在Linux系統中, 內核進程和用戶進程所佔的虛擬內存比例是1:3,而Windows系統爲2:2(經過設置Large-Address-Aware Executables標誌也可爲1:3)。這並不意味着內核使用那麼多物理內存,僅表示它可支配這部分地址空間,根據須要將其映射到物理內存。html
虛擬地址經過頁表(Page Table)映射到物理內存,頁表由操做系統維護並被處理器引用。內核空間在頁表中擁有較高特權級,所以用戶態程序試圖訪問這些頁時會致使一個頁錯誤(page fault)。在Linux中,內核空間是持續存在的,而且在全部進程中都映射到一樣的物理內存。內核代碼和數據老是可尋址,隨時準備處理中斷和系統調用。與此相反,用戶模式地址空間的映射隨進程切換的發生而不斷變化。linux
Linux進程在虛擬內存中的標準內存段佈局以下圖所示:程序員
其中,用戶地址空間中的藍色條帶對應於映射到物理內存的不一樣內存段,灰白區域表示未映射的部分。這些段只是簡單的內存地址範圍,與Intel處理器的段沒有關係。算法
上圖中Random stack offset和Random mmap offset等隨機值意在防止惡意程序。Linux經過對棧、內存映射段、堆的起始地址加上隨機偏移量來打亂佈局,以避免惡意程序經過計算訪問棧、庫函數等地址。execve(2)負責爲進程代碼段和數據段創建映射,真正將代碼段和數據段的內容讀入內存是由系統的缺頁異常處理程序按需完成的。另外,execve(2)還會將BSS段清零。數組
用戶進程部分分段存儲內容以下表所示(按地址遞減順序):緩存
名稱數據結構 |
存儲內容架構 |
棧app |
局部變量、函數參數、返回地址等dom |
堆 |
動態分配的內存 |
BSS段 |
未初始化或初值爲0的全局變量和靜態局部變量 |
數據段 |
已初始化且初值非0的全局變量和靜態局部變量 |
代碼段 |
可執行代碼、字符串字面值、只讀變量 |
在將應用程序加載到內存空間執行時,操做系統負責代碼段、數據段和BSS段的加載,並在內存中爲這些段分配空間。棧也由操做系統分配和管理;堆由程序員本身管理,即顯式地申請和釋放空間。
BSS段、數據段和代碼段是可執行程序編譯時的分段,運行時還須要棧和堆。
如下詳細介紹各個分段的含義。
內核老是駐留在內存中,是操做系統的一部分。內核空間爲內核保留,不容許應用程序讀寫該區域的內容或直接調用內核代碼定義的函數。
棧又稱堆棧,由編譯器自動分配釋放,行爲相似數據結構中的棧(先進後出)。堆棧主要有三個用途:
持續地重用棧空間有助於使活躍的棧內存保持在CPU緩存中,從而加速訪問。進程中的每一個線程都有屬於本身的棧。向棧中不斷壓入數據時,若超出其容量就會耗盡棧對應的內存區域,從而觸發一個頁錯誤。此時若棧的大小低於堆棧最大值RLIMIT_STACK(一般是8M),則棧會動態增加,程序繼續運行。映射的棧區擴展到所需大小後,再也不收縮。
Linux中ulimit -s命令可查看和設置堆棧最大值,當程序使用的堆棧超過該值時, 發生棧溢出(Stack Overflow),程序收到一個段錯誤(Segmentation Fault)。注意,調高堆棧容量可能會增長內存開銷和啓動時間。
堆棧既可向下增加(向內存低地址)也可向上增加, 這依賴於具體的實現。本文所述堆棧向下增加。
堆棧的大小在運行時由內核動態調整。
此處,內核將硬盤文件的內容直接映射到內存, 任何應用程序均可經過Linux的mmap()系統調用或Windows的CreateFileMapping()/MapViewOfFile()請求這種映射。內存映射是一種方便高效的文件I/O方式, 於是被用於裝載動態共享庫。用戶也可建立匿名內存映射,該映射沒有對應的文件, 可用於存放程序數據。在 Linux中,若經過malloc()請求一大塊內存,C運行庫將建立一個匿名內存映射,而不使用堆內存。」大塊」 意味着比閾值 MMAP_THRESHOLD還大,缺省爲128KB,可經過mallopt()調整。
該區域用於映射可執行文件用到的動態連接庫。在Linux 2.4版本中,若可執行文件依賴共享庫,則系統會爲這些動態庫在從0x40000000開始的地址分配相應空間,並在程序裝載時將其載入到該空間。在Linux 2.6內核中,共享庫的起始地址被往上移動至更靠近棧區的位置。
從進程地址空間的佈局能夠看到,在有共享庫的狀況下,留給堆的可用空間還有兩處:一處是從.bss段到0x40000000,約不到1GB的空間;另外一處是從共享庫到棧之間的空間,約不到2GB。這兩塊空間大小取決於棧、共享庫的大小和數量。這樣來看,是否應用程序可申請的最大堆空間只有2GB?事實上,這與Linux內核版本有關。在上面給出的進程地址空間經典佈局圖中,共享庫的裝載地址爲0x40000000,這其實是Linux kernel 2.6版本以前的狀況了,在2.6版本里,共享庫的裝載地址已經被挪到靠近棧的位置,即位於0xBFxxxxxx附近,所以,此時的堆範圍就不會被共享庫分割成2個「碎片」,故kernel 2.6的32位Linux系統中,malloc申請的最大內存理論值在2.9GB左右。
堆用於存放進程運行時動態分配的內存段,可動態擴張或縮減。堆中內容是匿名的,不能按名字直接訪問,只能經過指針間接訪問。當進程調用malloc(C)/new(C++)等函數分配內存時,新分配的內存動態添加到堆上(擴張);當調用free(C)/delete(C++)等函數釋放內存時,被釋放的內存從堆中剔除(縮減) 。
分配的堆內存是通過字節對齊的空間,以適合原子操做。堆管理器經過鏈表管理每一個申請的內存,因爲堆申請和釋放是無序的,最終會產生內存碎片。堆內存通常由應用程序分配釋放,回收的內存可供從新使用。若程序員不釋放,程序結束時操做系統可能會自動回收。
堆的末端由break指針標識,當堆管理器須要更多內存時,可經過系統調用brk()和sbrk()來移動break指針以擴張堆,通常由系統自動調用。
使用堆時常常出現兩種問題:1) 釋放或改寫仍在使用的內存(「內存破壞」);2)未釋放再也不使用的內存(「內存泄漏」)。當釋放次數少於申請次數時,可能已形成內存泄漏。泄漏的內存每每比忘記釋放的數據結構更大,由於所分配的內存一般會圓整爲下個大於申請數量的2的冪次(如申請212B,會圓整爲256B)。
注意,堆不一樣於數據結構中的」堆」,其行爲相似鏈表。
【擴展閱讀】棧和堆的區別 ①管理方式:棧由編譯器自動管理;堆由程序員控制,使用方便,但易產生內存泄露。 ②生長方向:棧向低地址擴展(即」向下生長」),是連續的內存區域;堆向高地址擴展(即」向上生長」),是不連續的內存區域。這是因爲系統用鏈表來存儲空閒內存地址,天然不連續,而鏈表從低地址向高地址遍歷。 ③空間大小:棧頂地址和棧的最大容量由系統預先規定(一般默認2M或10M);堆的大小則受限於計算機系統中有效的虛擬內存,32位Linux系統中堆內存可達2.9G空間。 ④存儲內容:棧在函數調用時,首先壓入主調函數中下條指令(函數調用語句的下條可執行語句)的地址,而後是函數實參,而後是被調函數的局部變量。本次調用結束後,局部變量先出棧,而後是參數,最後棧頂指針指向最開始存的指令地址,程序由該點繼續運行下條可執行語句。堆一般在頭部用一個字節存放其大小,堆用於存儲生存期與函數調用無關的數據,具體內容由程序員安排。 ⑤分配方式:棧可靜態分配或動態分配。靜態分配由編譯器完成,如局部變量的分配。動態分配由alloca函數在棧上申請空間,用完後自動釋放。堆只能動態分配且手工釋放。 ⑥分配效率:棧由計算機底層提供支持:分配專門的寄存器存放棧地址,壓棧出棧由專門的指令執行,所以效率較高。堆由函數庫提供,機制複雜,效率比棧低得多。Windows系統中VirtualAlloc可直接在進程地址空間中分配一塊內存,快速且靈活。 ⑦分配後系統響應:只要棧剩餘空間大於所申請空間,系統將爲程序提供內存,不然報告異常提示棧溢出。 操做系統爲堆維護一個記錄空閒內存地址的鏈表。當系統收到程序的內存分配申請時,會遍歷該鏈表尋找第一個空間大於所申請空間的堆結點,而後將該結點從空閒結點鏈表中刪除,並將該結點空間分配給程序。若無足夠大小的空間(可能因爲內存碎片太多),有可能調用系統功能去增長程序數據段的內存空間,以便有機會分到足夠大小的內存,而後進行返回。,大多數系統會在該內存空間首地址處記錄本次分配的內存大小,供後續的釋放函數(如free/delete)正確釋放本內存空間。 此外,因爲找到的堆結點大小不必定正好等於申請的大小,系統會自動將多餘的部分從新放入空閒鏈表中。 ⑧碎片問題:棧不會存在碎片問題,由於棧是先進後出的隊列,內存塊彈出棧以前,在其上面的後進的棧內容已彈出。而頻繁申請釋放操做會形成堆內存空間的不連續,從而形成大量碎片,使程序效率下降。 可見,堆容易形成內存碎片;因爲沒有專門的系統支持,效率很低;因爲可能引起用戶態和內核態切換,內存申請的代價更爲昂貴。因此棧在程序中應用最普遍,函數調用也利用棧來完成,調用過程當中的參數、返回地址、棧基指針和局部變量等都採用棧的方式存放。因此,建議儘可能使用棧,僅在分配大量或大塊內存空間時使用堆。 使用棧和堆時應避免越界發生,不然可能程序崩潰或破壞程序堆、棧結構,產生意想不到的後果。 |
BSS(Block Started by Symbol)段中一般存放程序中如下符號:
C語言中,未顯式初始化的靜態分配變量被初始化爲0(算術類型)或空指針(指針類型)。因爲程序加載時,BSS會被操做系統清零,因此未賦初值或初值爲0的全局變量都在BSS中。BSS段僅爲未初始化的靜態分配變量預留位置,在目標文件中並不佔據空間,這樣可減小目標文件體積。但程序運行時需爲變量分配內存空間,故目標文件必須記錄全部未初始化的靜態分配變量大小總和(經過start_bss和end_bss地址寫入機器代碼)。當加載器(loader)加載程序時,將爲BSS段分配的內存初始化爲0。在嵌入式軟件中,進入main()函數以前BSS段被C運行時系統映射到初始化爲全零的內存(效率較高)。
注意,儘管均放置於BSS段,但初值爲0的全局變量是強符號,而未初始化的全局變量是弱符號。若其餘地方已定義同名的強符號(初值可能非0),則弱符號與之連接時不會引發重定義錯誤,但運行時的初值可能並不是指望值(會被強符號覆蓋)。所以,定義全局變量時,若只有本文件使用,則儘可能使用static關鍵字修飾;不然須要爲全局變量定義賦初值(哪怕0值),保證該變量爲強符號,以便連接時發現變量名衝突,而不是被未知值覆蓋。
某些編譯器將未初始化的全局變量保存在common段,連接時再將其放入BSS段。在編譯階段可經過-fno-common選項來禁止將未初始化的全局變量放入common段。
此外,因爲目標文件不含BSS段,故程序燒入存儲器(Flash)後BSS段地址空間內容未知。U-Boot啓動過程當中,將U-Boot的Stage2代碼(一般位於lib_xxxx/board.c文件)搬遷(拷貝)到SDRAM空間後必須人爲添加清零BSS段的代碼,而不可依賴於Stage2代碼中變量定義時賦0值。
【擴展閱讀】BSS歷史 BSS(Block Started by Symbol,以符號開始的塊)一詞最初是UA-SAP彙編器(United Aircraft Symbolic Assembly Program)中的僞指令,用於爲符號預留一塊內存空間。該彙編器由美國聯合航空公司於20世紀50年代中期爲IBM 704大型機所開發。 後來該詞被做爲關鍵字引入到了IBM 709和7090/94機型上的標準彙編器FAP(Fortran Assembly Program),用於定義符號而且爲該符號預留指定字數的未初始化空間塊。 在採用段式內存管理的架構中(如Intel 80x86系統),BSS段一般指用來存放程序中未初始化全局變量的一塊內存區域,該段變量只有名稱和大小卻沒有值。程序開始時由系統初始化清零。 BSS段不包含數據,僅維護開始和結束地址,以便內存能在運行時被有效地清零。BSS所需的運行時空間由目標文件記錄,但BSS並不佔用目標文件內的實際空間,即BSS節段應用程序的二進制映象文件中並不存在。 |
數據段一般用於存放程序中已初始化且初值不爲0的全局變量和靜態局部變量。數據段屬於靜態內存分配(靜態存儲區),可讀可寫。
數據段保存在目標文件中(在嵌入式系統裏通常固化在鏡像文件中),其內容由程序初始化。例如,對於全局變量int gVar = 10,必須在目標文件數據段中保存10這個數據,而後在程序加載時複製到相應的內存。
數據段與BSS段的區別以下:
1) BSS段不佔用物理文件尺寸,但佔用內存空間;數據段佔用物理文件,也佔用內存空間。
對於大型數組如int ar0[10000] = {1, 2, 3, ...}和int ar1[10000],ar1放在BSS段,只記錄共有10000*4個字節須要初始化爲0,而不是像ar0那樣記錄每一個數據一、二、3...,此時BSS爲目標文件所節省的磁盤空間至關可觀。
2) 當程序讀取數據段的數據時,系統會出發缺頁故障,從而分配相應的物理內存;當程序讀取BSS段的數據時,內核會將其轉到一個全零頁面,不會發生缺頁故障,也不會爲其分配相應的物理內存。
運行時數據段和BSS段的整個區段一般稱爲數據區。某些資料中「數據段」指代數據段 + BSS段 + 堆。
代碼段也稱正文段或文本段,一般用於存放程序執行代碼(即CPU執行的機器指令)。通常C語言執行語句都編譯成機器代碼保存在代碼段。一般代碼段是可共享的,所以頻繁執行的程序只須要在內存中擁有一份拷貝便可。代碼段一般屬於只讀,以防止其餘程序意外地修改其指令(對該段的寫操做將致使段錯誤)。某些架構也容許代碼段爲可寫,即容許修改程序。
代碼段指令根據程序設計流程依次執行,對於順序指令,只會執行一次(每一個進程);如有反覆,則需使用跳轉指令;若進行遞歸,則須要藉助棧來實現。
代碼段指令中包括操做碼和操做對象(或對象地址引用)。若操做對象是當即數(具體數值),將直接包含在代碼中;如果局部數據,將在棧區分配空間,而後引用該數據地址;若位於BSS段和數據段,一樣引用該數據地址。
代碼段最容易受優化措施影響。
位於虛擬地址空間的最低部分,未賦予物理地址。任何對它的引用都是非法的,用於捕捉使用空指針和小整型值指針引用內存的異常狀況。
它並非一個單一的內存區域,而是對地址空間中受到操做系統保護而禁止用戶進程訪問的地址區域的總稱。大多數操做系統中,極小的地址一般都是不容許訪問的,如NULL。C語言將無效指針賦值爲0也是出於這種考慮,由於0地址上正常狀況下不會存放有效的可訪問數據。
在32位X86架構的Linux系統中,用戶進程可執行程序通常從虛擬地址空間0x08048000開始加載。該加載地址由ELF文件頭決定,可經過自定義連接器腳本覆蓋連接器默認配置,進而修改加載地址。0x08048000如下的地址空間一般由C動態連接庫、動態加載器ld.so和內核VDSO(內核提供的虛擬共享庫)等佔用。經過使用mmap系統調用,可訪問0x08048000如下的地址空間。
經過cat /proc/self/maps命令查看加載表以下:
【擴展閱讀】分段的好處 進程運行過程當中,代碼指令根據流程依次執行,只需訪問一次(固然跳轉和遞歸可能使代碼執行屢次);而數據(數據段和BSS段)一般須要訪問屢次,所以單獨開闢空間以方便訪問和節約空間。具體解釋以下: 當程序被裝載後,數據和指令分別映射到兩個虛存區域。數據區對於進程而言可讀寫,而指令區對於進程只讀。兩區的權限可分別設置爲可讀寫和只讀。以防止程序指令被有意或無心地改寫。 現代CPU具備極爲強大的緩存(Cache)體系,程序必須儘可能提升緩存命中率。指令區和數據區的分離有利於提升程序的局部性。現代CPU通常數據緩存和指令緩存分離,故程序的指令和數據分開存放有利於提升CPU緩存命中率。 當系統中運行多個該程序的副本時,其指令相同,故內存中只須保存一份該程序的指令部分。若系統中運行數百進程,經過共享指令將節省大量空間(尤爲對於有動態連接的系統)。其餘只讀數據如程序裏的圖標、圖片、文本等資源也可共享。而每一個副本進程的數據區域不一樣,它們是進程私有的。 此外,臨時數據及須要再次使用的代碼在運行時放入棧區中,生命週期短。全局數據和靜態數據可能在整個程序執行過程當中都須要訪問,所以單獨存儲管理。堆區由用戶自由分配,以便管理。 |
原文地址:http://www.cnblogs.com/clover-toeic/p/3754433.html