Unix/Linux內存管理
一、底層結構
采用三層結構,實際使用中可以方便映射到兩層或者三層結構,以適用不同的硬件結構。最下層的申請內存函數get_free_page。之上有三種類型的內存分配函數:
1.kmalloc類型。內核進程使用,基于切片(slab)技術,用于管理小于內存頁的內存申請。思想出發點和應用層的內存緩沖池同出一轍。但它針對內核結構,特別處理應用場景固定,不考慮釋放。
2.vmalloc類型。內核進程使用。用于申請不連續內存。
3.brk/mmap類型。用戶進程使用。malloc/free實現的基礎。
二、內存管理的相關函數圖
STL -> 內存自動分配和自動回收(C++)
|
C++ -> new分配內存,delete回收內存
|
C -> malloc分配內存,free回收內存
|
Unix 系統函數 -> sbrk/brk 分配和回收內存
|
Unix底層系統函數 -> mmap/munmap分配回收
(用戶層)
----------------------------------------------------------------------------
(內核層)
Unix內核函數 kmalloc/vmalloc/get_free_page
三、進程與內存
a.所有進程(執行的程序)都必須占用一定數量的內存
b.對任何一個普通進程來講,它都會涉及到5種不同的數據段,其內存空間劃分為:
1.代碼區 —— 存放代碼/函數,也就是說它是可執行程序的內存中的鏡像。(只讀)
2.全局區 —— 保存全局變量,static局部變量。
3.BSS段 —— 未初始化的全局變量,BSS段在main函數執行之前會自動清零
4.棧區 —— 局部變量,包括函數的形參,棧區內存自動分配和自動回收。
5.堆區 —— 程序員自己管理的區域,malloc/free操作的都是堆區。
6.只讀常量區 —— 存放字符串常量和const修飾的全局變量
注:只讀常量區和代碼區非常接近,有些書把只讀常量區和代碼區合并為代碼區。
c.進程如何組織這些區域?
從小到大次序:代碼區、只讀常量區、全局區、BSS段、堆區、棧區
堆區在離前面四個區不遠的地址空間開始,從小到大分配,棧區從3G開始,從大到小分配。主要為了避免堆區和棧區重疊。
d.查看內存分配
Linux把一切都看做成文件,內存也可以在文件中查看。每個進程都在/proc目錄下有一個對應的子目錄,以進程ID作為子目錄名。進程ID是系統對進程的標識。可以用ps-aux命令查看進程。
cat /proc/進程ID/maps 可以查看當前進程的內存情況。
四、虛擬內存管理技術
Linux使用了虛擬內存地址。每個Linux中的進程都有 0~4G的虛擬內存地址,就是0~4G的數字。虛擬內存地址在開始時只是一個數字,不對應任何的內存。虛擬內存地址必須先映射一段物理內存或硬盤上的文件才能被使用。所謂的分配內存其實就是讓虛擬內存地址映射一段物理內存。如果使用沒有映射的虛擬內存地址就會引發段錯誤。
程序員所操作的內存地址都是虛擬內存地址,看不到物理內存地址。
0~4G的虛擬內存地址中,0~3G是用戶使用,叫做“用戶空間”,3G~4G是內核使用的,叫做“內核空間”。用戶空間不能直接使用內核空間,但可以通過內核空間提供的一些函數(系統調用)訪問內核空間。
注:內存管理的基本單位是4096 byte (4K),叫內存頁。內存的映射和回收都是以內存頁作為基本單位。
五、進程內存管理
進程內存管理的對象是進程線性地址空間上的內存鏡像,這些內存鏡像其實就是進程使用的虛擬內存區域(memory region)。進程虛擬空間是個32或64位的“平坦”(獨立的連續區間)地址空間(空間的具體大小取決于體系結構)。要統一管理這么大的平坦空間可絕非易事,為了方便管理,虛擬空間被劃分為許多大小可變的(但必須是4096byte的整數倍數)內存區域,這些區域在進程線性地址中像停車位一樣有序排列。這些區域的劃分原則是“將訪問屬性一致的地址空間存放在一起”,所謂訪問屬性一致無非是指“可讀、可寫、可執行等”。
六、物理內存管理(頁管理)
Linux內核管理物理內存是通過分頁機制實現的,它將整個內存劃分成無數4K(在i386體系結構中)大小頁,從而分配和回收內存的基本單位便是內存頁了。利用分頁管利用助于靈活分配內存地址,因為分配時不必要求必須有大塊的連續內存,系統可以東一頁、西一頁的湊出所需要的內存供進程使用。雖然如此,但是實際上系統使用內存還是傾向于分配連續的內存塊,因為分配連續內存時,頁表不需要修改,因此能降低刷新率(頻繁刷新會很大增加訪問速度)。
七、brk/sbrk的虛擬內存管理
void *sbrk(int size);
size = 0 返回sbrk/brk上次的末尾地址,代表取當前的位置,
size > 0 分配內存空間,返回上次的末尾地址,代表分配size字節的內存,
size < 0 釋放空間,代表回收size字節內存。
int brk(void* ptr);
直接修改訪問的有效范圍的末尾地址,釋放空間形成一個完整的page,則該頁映射被解除
返回:0 分配成功
-1 分配失敗
經驗:sbrk在分配內存上簡單,brk在釋放內存上簡單。因此,開發大多數使用sbrk分配內存,使用brk釋放內存。
八、系統底層的內存映射(mmap/munmap)
1 |
#include <sys/mman.h> |
2 |
void *mmap( void *start, size_t length, int prot, int flags, int fd, off_t offset); |
3 |
int munmap( void *start, size_t length); |
參數公共部分:
start:指向欲映射的內存起始地址,通常設為 NULL,代表讓系統自動選定地址,映射成功后返回該地址。
length:代表將文件中多大的部分映射到內存。 映射空間大小。建議4k倍數,不是4K倍數,自動對齊。
mmap獨有部分:
prot:映射區域的保護方式。可以為以下幾種方式的組合:
1.PROT_EXEC 映射區域可被執行
2.PROT_READ 映射區域可被讀取
3.PROT_WRITE 映射區域可被寫入
4.PROT_NONE 映射區域不能存取
flags:影響映射區域的各種特性。在調用mmap()時必須要指定MAP_SHARED 或MAP_PRIVATE。
1.MAP_FIXED 如果參數start所指的地址無法成功建立映射時,則放棄映射,不對地址做修正。通常不鼓勵用此標志。
2.MAP_SHARED對映射區域的寫入數據會復制回文件內,而且允許其他映射該文件的進程共享。
3.MAP_PRIVATE 對映射區域的寫入操作會產生一個映射文件的復制,即私人的“寫入時復制”(copy on write)對此區域作的任何修改都不會寫回原來的文件內容。
4.MAP_ANONYMOUS建立匿名映射。此時會忽略參數fd,不涉及文件,而且映射區域無法和其他進程共享。
5.MAP_DENYWRITE只允許對映射區域的寫入操作,其他對文件直接寫入的操作將會被拒絕。
6.MAP_LOCKED 將映射區域鎖定住,這表示該區域不會被置換(swap)。
fd:要映射到內存中的文件描述符。如果使用匿名內存映射時,即flags中設置了MAP_ANONYMOUS,fd設為-1。有些系統不支持匿名內存映射,則可以使用fopen打開/dev/zero文件,然后對該文件進行映射,可以同樣達到匿名內存映射的效果。
offset:文件映射的偏移量,通常設置為0,代表從文件最前方開始對應,offset必須是分頁大小的整數倍。
返回值:
若映射成功則返回映射區的內存起始地址,否則返回MAP_FAILED(-1),錯誤原因存于errno 中。
九、errno錯誤代碼
1.EBADF 參數fd 不是有效的文件描述詞
2.EACCES 存取權限有誤。如果是MAP_PRIVATE 情況下文件必須可讀,使用MAP_SHARED則要有PROT_WRITE以及該文件要能寫入。
3.EINVAL 參數start、length 或offset有一個不合法。
4.EAGAIN 文件被鎖住,或是有太多內存被鎖住。
5.ENOMEM 內存不足。