目錄
一. 預備知識
1. 虛擬記憶體位址和物理記憶體位址
2. 頁與位址構成
3. 記憶體頁與磁碟頁
二. linux程序級記憶體管理
1. 記憶體排布
2. heap 記憶體模型
3. 堆記憶體管理實現原理
為了簡單,現代作業系統在處理記憶體位址時,普遍採用虛擬記憶體位址技術。即在匯程式設計序(或機器語言)層面,當涉及記憶體位址時,都是使用虛擬記憶體位址。採用這種技術時,每個程序彷彿自己獨享一片2n位元組的記憶體,其中n是機器位數。例如在64位cpu和64位作業系統下,每個程序的虛擬位址空間為264byte。
這種虛擬位址空間的作用主要是簡化程式的編寫及方便作業系統對程序間記憶體的隔離管理,真實中的程序不太可能(也用不到)如此大的記憶體空間,實際能用到的記憶體取決於物理記憶體大小。
由於在機器語言層面都是採用虛擬位址,當實際的機器碼程式涉及到記憶體操作時,需要根據當前程序執行的實際上下文將虛擬位址轉換為物理記憶體位址,才能實現對真實記憶體資料的操作。這個轉換一般由乙個叫mmu(memory management unit)的硬體完成。
在現代作業系統中,不論是虛擬記憶體還是物理記憶體,都不是以位元組為單位進行管理的,而是以頁(page)為單位。乙個記憶體頁是一段固定大小的連續記憶體位址的總稱,具體到linux中,典型的記憶體頁大小為4096byte(4k),所以記憶體位址可以分為頁號和頁內偏移量。
上面是虛擬記憶體位址,下面是物理記憶體位址。由於頁大小都是4k,所以頁內便宜都是用低12位表示,而剩下的高位址表示頁號。
mmu對映單位並不是位元組,而是頁,這個對映通過查乙個常駐記憶體的資料結構頁表來實現。現在計算機具體的記憶體位址對映比較複雜,為了加快速度會引入一系列快取和優化,例如tlb等機制。下面給出乙個經過簡化的記憶體位址翻譯示意圖,雖然經過了簡化,但是基本原理與現代計算機真實的情況的一致的
一般將記憶體看做磁碟的的快取,有時mmu在工作時,會發現頁表表明某個記憶體頁不在物理記憶體中,此時會觸發乙個缺頁異常(page fault),此時系統會到磁碟中相應的地方將磁碟頁載入到記憶體中,然後重新執行由於缺頁而失敗的機器指令。關於這部分,因為可以看做對malloc實現是透明的,所以不再詳細講述,有興趣的可以參考《深入理解計算機系統》相關章節。
明白了虛擬記憶體和物理記憶體的關係及相關的對映機制,下面看一下具體在乙個程序內是如何排布記憶體的。
以linux 64位系統為例。理論上,64bit記憶體位址可用空間為0x0000000000000000 ~ 0xffffffffffffffff,這是個相當龐大的空間,linux實際上只用了其中一小部分(256t)。
根據linux核心相關文件描述,linux64位作業系統僅使用低47位,高17位做擴充套件(只能是全0或全1)。所以,實際用到的位址為空間為0x0000000000000000 ~ 0x00007fffffffffff和0xffff800000000000 ~ 0xffffffffffffffff,其中前面為使用者空間(user space),後者為核心空間(kernel space)。
對使用者來說,主要關注的空間是user space。將user space放大後,可以看到裡面主要分為如下幾段:
data:這裡存放的是初始化過的全域性變數
bss:這裡存放的是未初始化的全域性變數
一般來說,malloc所申請的記憶體主要從heap區域分配(本文不考慮通過mmap申請大塊記憶體的情況)。
由上文知道,程序所面對的虛擬記憶體位址空間,只有按頁對映到物理記憶體位址,才能真正使用。受物理儲存容量限制,整個堆虛擬記憶體空間不可能全部對映到實際的物理記憶體。linux對堆的管理示意如下:
linux維護乙個break指標,這個指標指向堆空間的某個位址。從堆起始位址到break之間的位址空間為對映好的,可以供程序訪問;而從break往上,是未對映的位址空間,如果訪問這段空間則程式會報錯。
linux通過brk和sbrk系統呼叫操作break指標。兩個系統呼叫的原型如下, 見man brk:
#include int brk(void *addr);
void *sbrk(intptr_t increment);
brk將break指標直接設定為某個位址,而sbrk將break從當前位置移動increment所指定的增量。brk在執行成功時返回0,否則返回-1並設定errno為enomem;sbrk成功時返回break移動之前所指向的位址,否則返回(void *)-1。
乙個小技巧是,如果將increment設定為0,則可以獲得當前break的位址。
另外需要注意的是,由於linux是按頁進行記憶體對映的,所以如果break被設定為沒有按頁大小對齊,則系統實際上會在最後對映乙個完整的頁,從而實際已對映的記憶體空間比break指向的地方要大一些。但是使用break之後的位址是很危險的(儘管也許break之後確實有一小塊可用記憶體位址)。
系統對每乙個程序所分配的資源不是無限的,包括可對映的記憶體空間,因此每個程序有乙個rlimit表示當前程序可用的資源上限。這個限制可以通過getrlimit系統呼叫得到, 見man getrlimit
#include #include int getrlimit(int resource, struct rlimit *rlim);
int setrlimit(int resource, const struct rlimit *rlim);
struct rlimit ;
每種資源有軟限制和硬限制,並且可以通過setrlimit對rlimit進行有條件設定。其中硬限制作為軟限制的上限,非特權程序只能設定軟限制,且不能超過硬限制。
break指標通過在heap's start 和 rlimit 之間來回擺動, 來標記記憶體最大占用位置. 因為malloc出來的是一段記憶體塊, 那麼實際使用中在heap's start 和 break 之間存在著記憶體碎片, 有什麼方法能更好的利用這些記憶體碎片呢?
堆記憶體與棧記憶體的理解
記憶體中的堆與棧的根本區別在於堆記憶體由使用者自己申請,需要自己去釋放,否則會造成記憶體洩露,最終記憶體空間不夠。而棧記憶體則是由系統區釋放,程式設計師不需要自己去釋放。棧記憶體用來存放臨時申明的變數,如乙個函式中的區域性變數等。拿乙個函式為例,函式有返回值,函式引數 入參,出參 區域性變數,返回值...
堆與記憶體管理
當程式對堆的操作比較頻繁的時候,使用系統呼叫的方式向核心索要空間的代價太大。比較好的做法是向作業系統申請一塊適當大小的額堆空間,然後又程式自己管理這塊空間,所以管理空間分配的往往時程式的執行庫。執行庫相當於零售商,從核心批發了較大的堆空間,然後零售給程式使用。linux下的程序堆管理提供了兩種堆空間...
堆記憶體管理
堆記憶體是 段當中的其中一段,特點就是大,但不能與識別符號建立聯絡,只能與指標配合使用 c語言沒有提供管理堆記憶體的語句,而是標準庫提供了一套管理記憶體的函式 功能 從堆記憶體中分配記憶體 引數 size 所申請的位元組數,一般使用 sizeof 計算 返回值 所申請的記憶體的首位址 注意 1 如果...