Linux 內(nèi)存管理
1 頁的概念
linux 內(nèi)核中把物理頁作為內(nèi)存分配的最小單位,32位CPU 頁的大小通常為4K,64位的CPU通常支持8K的也。內(nèi)存管理單元MMU 同樣以頁為大小分配內(nèi)存。
2 內(nèi)核虛擬地址分區(qū)和物理內(nèi)存分區(qū)
在32位內(nèi)核中,內(nèi)核虛擬地址空間為0-4G,其中用戶態(tài)為1-3G空間,內(nèi)核態(tài)為3G-4G,內(nèi)核空間根據(jù)物理地址的特性大概可以分為三個(gè)區(qū):
| 區(qū) | 描述 | 32位系統(tǒng)物理內(nèi)存大小 |
|---|---|---|
| ZONE_DMA | 和硬件操作相關(guān)的內(nèi)存區(qū)域 | < 16M |
| ZONE_NORMAL | 內(nèi)核正常映射的物理頁 | 16 - 896M |
| ZONE_HIGH | 高端內(nèi)存,由于內(nèi)核空間大小的原理部分頁不能永久的映射到內(nèi)核,需要?jiǎng)討B(tài)映射的 | > 896M |
下面的圖描述了內(nèi)核地址空間和物理內(nèi)存的映射關(guān)系:
32位 內(nèi)核內(nèi)存分區(qū)
Linux 內(nèi)核啟動(dòng)后的mm 的初始化過程:
/*
* Set up kernel memory allocators
*/
static void __init mm_init(void)
{
/*
* page_ext requires contiguous pages,
* bigger than MAX_ORDER unless SPARSEMEM.
*/
page_ext_init_flatmem();
mem_init();
kmem_cache_init();
percpu_init_late();
pgtable_init();
vmalloc_init();
ioremap_huge_init();
}
3伙伴系統(tǒng)算法
3.1 簡(jiǎn)介
在實(shí)際應(yīng)用中,經(jīng)常需要分配一組連續(xù)的頁,而頻繁地申請(qǐng)和釋放不同大小的連續(xù)頁,必然導(dǎo)致在已分配頁框的內(nèi)存塊中分散了許多小塊的空閑頁框。這樣,即使這些頁框是空閑的,其他需要分配連續(xù)頁框的應(yīng)用也很難得到滿足。為了避免出現(xiàn)這種情況,Linux內(nèi)核中引入了伙伴系統(tǒng)算法(buddy system)。把所有的空閑頁框分組為11個(gè)塊鏈表,每個(gè)塊鏈表分別包含大小為1,2,4,8,16,32,64,128,256,512和1024個(gè)連續(xù)頁框的頁框塊。最大可以申請(qǐng)1024個(gè)連續(xù)頁框,對(duì)應(yīng)4MB大小的連續(xù)內(nèi)存。每個(gè)頁框塊的第一個(gè)頁框的物理地址是該塊大小的整數(shù)倍。
假設(shè)要申請(qǐng)一個(gè)256個(gè)頁框的塊,先從256個(gè)頁框的鏈表中查找空閑塊,如果沒有,就去512個(gè)頁框的鏈表中找,找到了則將頁框塊分為2個(gè)256個(gè)頁框的塊,一個(gè)分配給應(yīng)用,另外一個(gè)移到256個(gè)頁框的鏈表中。如果512個(gè)頁框的鏈表中仍沒有空閑塊,繼續(xù)向1024個(gè)頁框的鏈表查找,如果仍然沒有,則返回錯(cuò)誤。頁框塊在釋放時(shí),會(huì)主動(dòng)將兩個(gè)連續(xù)的頁框塊合并為一個(gè)較大的頁框塊。
mem_init() 函數(shù)中會(huì)把內(nèi)核啟動(dòng)后的空閑內(nèi)存用buddy 系統(tǒng)管理。
3.2 伙伴系統(tǒng)算法分配函數(shù)
mem_init 初始化完伙伴系統(tǒng)后通過 alloc_page(s) 函數(shù)分配伙伴系統(tǒng)內(nèi)存池的內(nèi)存。
| 函數(shù) | 描述 |
|---|---|
| struct page * alloc_page(unsigned int gfp_mask) | 分配一頁物理內(nèi)存并返回該頁物理內(nèi)存的page結(jié)構(gòu)指針 |
| struct page * alloc_pages(unsigned int gfp_mask, unsigned int order) | 分配2的order次方連續(xù)的物理頁并返回分配的第一個(gè)物理頁的page結(jié)構(gòu)指針 |
| unsigned long get_free_page(unsigned int gfp_mask) | 只分配一頁,返回頁的邏輯地址 |
| unsigned long __get_free_pages(unsigned int gfp_mask, unsigned int order) | 分配 2的order頁,返回也是邏輯地址 |
3.3 get_free_page(s)與alloc_page(s)的差異
alloc_page alloc_pages 分配后還不能直接使用, 需要得到該頁對(duì)應(yīng)的虛擬地址
- void *page_address(struct page *page);
- 低端內(nèi)存的映射方式:__va((unsigned long)(page - mem_map) << 12)
- 高端內(nèi)存到映射方式:struct page_address_map分配一個(gè)動(dòng)態(tài)結(jié)構(gòu)來管理高端內(nèi)存。(內(nèi)核是訪問不到vma的3G以下的虛擬地址的) 具體映射由kmap / kmap_atomic執(zhí)行。
get_free_page(s)與alloc_page(s)系列最大的區(qū)別是無法申請(qǐng)高端內(nèi)存,因?yàn)樗祷氐绞且粋€(gè)邏輯地址,而高端內(nèi)存是需要額外映射才可以
Android x86 的buffyinfo.
以Normal區(qū)域進(jìn)行分析,第二列值為459,表示當(dāng)前系統(tǒng)中normal區(qū)域,可用的連續(xù)兩頁的內(nèi)存大小為459*2^1*PAGE_SIZE;第三列值為52,表示當(dāng)前系統(tǒng)中normal區(qū)域,可用的連續(xù)四頁的內(nèi)存大小為52*2^2*PAGE_SIZE
generic_x86:/ # cat /proc/buddyinfo
Node 0, zone DMA 4 1 2 2 3 2 3 1 2 0 1
Node 0, zone Normal 1186 459 220 142 25 13 2 0 1 2 138
Node 0, zone HighMem 87 74 12 9 0 1 1 0 0 0 0
4 Slab 內(nèi)存分配算法
Slab 內(nèi)存分配算法 和Java中的對(duì)象池是一個(gè)概念。采用buddy算法,解決了外碎片問題,這種方法適合大塊內(nèi)存請(qǐng)求,不適合小內(nèi)存區(qū)請(qǐng)求
4.1 Slab 內(nèi)存分配算法
slab分配器源于 Solaris 2.4 的分配算法,工作于物理內(nèi)存頁框分配器之上,管理特定大小對(duì)象的緩存,進(jìn)行快速而高效的內(nèi)存分配。slab分配器為每種使用的內(nèi)核對(duì)象建立單獨(dú)的緩沖區(qū)。Linux 內(nèi)核已經(jīng)采用了伙伴系統(tǒng)管理物理內(nèi)存頁框,因此 slab分配器直接工作于伙伴系統(tǒng)之上。每種緩沖區(qū)由多個(gè) slab 組成,每個(gè) slab就是一組連續(xù)的物理內(nèi)存頁框,被劃分成了固定數(shù)目的對(duì)象。根據(jù)對(duì)象大小的不同,缺省情況下一個(gè) slab 最多可以由 1024個(gè)頁框構(gòu)成。出于對(duì)齊等其它方面的要求,slab 中分配給對(duì)象的內(nèi)存可能大于用戶要求的對(duì)象實(shí)際大小,這會(huì)造成一定的內(nèi)存浪費(fèi)。
Linux 所使用的 slab 分配器的基礎(chǔ)是 Jeff Bonwick 為SunOS 操作系統(tǒng)首次引入的一種算法。Jeff的分配器是圍繞對(duì)象緩存進(jìn)行的。在內(nèi)核中,會(huì)為有限的對(duì)象集(例如文件描述符和其他常見結(jié)構(gòu))分配大量?jī)?nèi)存。Jeff發(fā)現(xiàn)對(duì)內(nèi)核中普通對(duì)象進(jìn)行初始化所需的時(shí)間超過了對(duì)其進(jìn)行分配和釋放所需的時(shí)間。因此他的結(jié)論是不應(yīng)該將內(nèi)存釋放回一個(gè)全局的內(nèi)存池,而是將內(nèi)存保持為針對(duì)特定目而初始化的狀態(tài)。例如,如果內(nèi)存被分配給了一個(gè)互斥鎖,那么只需在為互斥鎖首次分配內(nèi)存時(shí)執(zhí)行一次互斥鎖初始化函數(shù)(mutex_init)即可。后續(xù)的內(nèi)存分配不需要執(zhí)行這個(gè)初始化函數(shù),因?yàn)閺纳洗吾尫藕驼{(diào)用析構(gòu)之后,它已經(jīng)處于所需的狀態(tài)中了。
4.2 Slab 內(nèi)存結(jié)構(gòu)
kmem_cache_alloc 分配的所有的內(nèi)存塊在內(nèi)核中以鏈表的形式組織。
kmem_cache_alloc 從buddy系統(tǒng)分配到內(nèi)存后,在內(nèi)部被分為 slab 單元,這是一段連續(xù)的內(nèi)存塊(通常都是頁面)。所有的對(duì)象都分配在這些slab 單元上,這些slab 單元被組織為三個(gè)鏈表:
- slabs_full 完全分配的 slab
- slabs_partial 部分分配的 slab
- slabs_free 可以回收的 slab
4.3 slab 著色區(qū)和slab 結(jié)構(gòu)
每個(gè)Slab的首部都有一個(gè)小小的區(qū)域是不用的,稱為“著色區(qū)(coloring area)”。著色區(qū)的大小使Slab中的每個(gè)對(duì)象的起始地址都按高速緩存中的”緩存行(cache line)”大小進(jìn)行對(duì)齊(80386的一級(jí)高速緩存行大小為16字節(jié),Pentium為32字節(jié))。因?yàn)镾lab是由1個(gè)頁面或多個(gè)頁面(最多為32)組成,因此,每個(gè)Slab都是從一個(gè)頁面邊界開始的,它自然按高速緩存的緩沖行對(duì)齊。但是,Slab中的對(duì)象大小不確定,設(shè)置著色區(qū)的目的就是將Slab中第一個(gè)對(duì)象的起始地址往后推到與緩沖行對(duì)齊的位置。每個(gè)Slab上最后一個(gè)對(duì)象以后也有個(gè)小小的區(qū)是不用的,這是對(duì)著色區(qū)大小的補(bǔ)償,其大小取決于著色區(qū)的大小,以及Slab與其每個(gè)對(duì)象的相對(duì)大小。

4.4 Slab 內(nèi)存函數(shù)
mm_init --> kmem_cache_init(); kernel 初始化
- kmem_cache_t* xx_cache; // 鏈表頭
- 創(chuàng)建: xx_cache = kmem_cache_create("name", sizeof(struct xx), SLAB_HWCACHE_ALIGN, NULL, NULL);
- 分配: kmem_cache_alloc(xx_cache, GFP_KERNEL);
- 釋放: kmem_cache_free(xx_cache, addr);
slab 內(nèi)存用結(jié)構(gòu)體 kmem_cache_t 表示:
4.5 slabinfo對(duì)象
從 /proc/slabinfo 中看一看出,內(nèi)核為大結(jié)構(gòu)體使用了slab 緩存。如ext4_inode_cache vm_area_struct task_struct等。
generic_x86:/ # cat /proc/slabinfo
slabinfo - version: 2.1
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
...
ext4_inode_cache 2025 2025 632 25 4 : tunables 0 0 0 : slabdata 81 81 0
ext4_allocation_context 156 156 104 39 1 : tunables 0 0 0 : slabdata 4 4 0
ext4_prealloc_space 224 224 72 56 1 : tunables 0 0 0 : slabdata 4 4 0
ext4_io_end 408 408 40 102 1 : tunables 0 0 0 : slabdata 4 4 0
ext4_extent_status 2048 2048 32 128 1 : tunables 0 0 0 : slabdata 16 16 0
...
vm_area_struct 20791 22402 88 46 1 : tunables 0 0 0 : slabdata 487 487 0
mm_struct 85 85 480 17 2 : tunables 0 0 0 : slabdata 5 5 0
...
task_struct 621 621 1184 27 8 : tunables 0 0 0 : slabdata 23 23 0
...
kmalloc-8192 28 28 8192 4 8 : tunables 0 0 0 : slabdata 7 7 0
kmalloc-4096 96 104 4096 8 8 : tunables 0 0 0 : slabdata 13 13 0
kmalloc-2048 128 128 2048 16 8 : tunables 0 0 0 : slabdata 8 8 0
kmalloc-1024 336 336 1024 16 4 : tunables 0 0 0 : slabdata 21 21 0
kmalloc-512 752 752 512 16 2 : tunables 0 0 0 : slabdata 47 47 0
kmalloc-256 698 752 256 16 1 : tunables 0 0 0 : slabdata 47 47 0
kmalloc-192 903 903 192 21 1 : tunables 0 0 0 : slabdata 43 43 0
kmalloc-128 1760 1760 128 32 1 : tunables 0 0 0 : slabdata 55 55 0
kmalloc-96 2100 2100 96 42 1 : tunables 0 0 0 : slabdata 50 50 0
kmalloc-64 14272 14272 64 64 1 : tunables 0 0 0 : slabdata 223 223 0
kmalloc-32 26182 28416 32 128 1 : tunables 0 0 0 : slabdata 222 222 0
kmalloc-16 15360 15360 16 256 1 : tunables 0 0 0 : slabdata 60 60 0
kmalloc-8 6656 6656 8 512 1 : tunables 0 0 0 : slabdata 13 13 0
kmem_cache_node 128 128 32 128 1 : tunables 0 0 0 : slabdata 1 1 0
kmem_cache 128 128 128 32 1 : tunables 0 0 0 : slabdata 4 4 0
5 kmalloc 和 vmalloc
5.1 kmalloc
從 4.5 節(jié) /proc/slabinfo 對(duì)象也可以看出,kmalloc 的分配建立在 slab 內(nèi)存對(duì)象池上。
在mm/slab_common.c 中 kmalloc 的分配定義如下:
// mm/slab_common.c
static struct {
const char *name;
unsigned long size;
} const kmalloc_info[] __initconst = {
{NULL, 0}, {"kmalloc-96", 96},
{"kmalloc-192", 192}, {"kmalloc-8", 8},
{"kmalloc-16", 16}, {"kmalloc-32", 32},
{"kmalloc-64", 64}, {"kmalloc-128", 128},
{"kmalloc-256", 256}, {"kmalloc-512", 512},
{"kmalloc-1024", 1024}, {"kmalloc-2048", 2048},
{"kmalloc-4096", 4096}, {"kmalloc-8192", 8192},
{"kmalloc-16384", 16384}, {"kmalloc-32768", 32768},
{"kmalloc-65536", 65536}, {"kmalloc-131072", 131072},
{"kmalloc-262144", 262144}, {"kmalloc-524288", 524288},
{"kmalloc-1048576", 1048576}, {"kmalloc-2097152", 2097152},
{"kmalloc-4194304", 4194304}, {"kmalloc-8388608", 8388608},
{"kmalloc-16777216", 16777216}, {"kmalloc-33554432", 33554432},
{"kmalloc-67108864", 67108864}
};
kmalloc 獲取的是以字節(jié)為單位的連續(xù)物理內(nèi)存空間
5.2 gfp_t 結(jié)構(gòu)體
// include/linux/slab.h
void *kmalloc(size_t size, gfp_t flags)
在 alloc_page(s) get_free_page(s) kmalloc 函數(shù)的定義中 第二個(gè)參數(shù)類型為 gfp_t 類型;
gfp_t 標(biāo)志有3類:(所有的 GFP 標(biāo)志都在 <linux/gfp.h> 中定義)
- 行為標(biāo)志 :控制分配內(nèi)存時(shí),分配器的一些行為
- 區(qū)標(biāo)志 :控制內(nèi)存分配在那個(gè)區(qū)(ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM 之類)
- 類型標(biāo)志 :由上面2種標(biāo)志組合而成的一些常用的場(chǎng)景
區(qū)標(biāo)志主要以下3種:
| 區(qū)域 | 描述 |
|---|---|
| __GFP_DMA | 從 ZONE_DMA 分配 |
| __GFP_DMA32 | 只在 ZONE_DMA32 分配 |
| __GFP_HIGHMEM | 從 ZONE_HIGHMEM 或者 ZONE_NORMAL 分配 |
__GFP_HIGHMEM 優(yōu)先從 ZONE_HIGHMEM 分配,如果 ZONE_HIGHMEM 沒有多余的頁則從 ZONE_NORMAL 分配
5.3 vmalloc
vmalloc 分配的內(nèi)存和kmalloc 不同,vmalloc 在邏輯地址上是連續(xù)的,但是在物理地質(zhì)上不一定連續(xù)。
/**
* vmalloc - allocate virtually contiguous memory
* @size: allocation size
* Allocate enough pages to cover @size from the page level
* allocator and map them into contiguous kernel virtual space.
*
* For tight control over page level allocator and protection flags
* use __vmalloc() instead.
*/
void *vmalloc(unsigned long size)
{
return __vmalloc_node_flags(size, NUMA_NO_NODE,
GFP_KERNEL | __GFP_HIGHMEM);
}
static inline void *__vmalloc_node_flags(unsigned long size,
int node, gfp_t flags)
{
return __vmalloc_node(size, 1, flags, PAGE_KERNEL,
node, __builtin_return_address(0));
}
void *__vmalloc_node_range(unsigned long size, unsigned long align,
unsigned long start, unsigned long end, gfp_t gfp_mask,
pgprot_t prot, unsigned long vm_flags, int node,
const void *caller)
{
struct vm_struct *area;
void *addr;
unsigned long real_size = size;
size = PAGE_ALIGN(size);
if (!size || (size >> PAGE_SHIFT) > totalram_pages)
goto fail;
area = __get_vm_area_node(size, align, VM_ALLOC | VM_UNINITIALIZED |
vm_flags, start, end, node, gfp_mask, caller);
if (!area)
goto fail;
addr = __vmalloc_area_node(area, gfp_mask, prot, node);
if (!addr)
return NULL;
......
}
static void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask,
pgprot_t prot, int node)
{
struct page **pages;
unsigned int nr_pages, array_size, i;
const gfp_t nested_gfp = (gfp_mask & GFP_RECLAIM_MASK) | __GFP_ZERO;
const gfp_t alloc_mask = gfp_mask | __GFP_NOWARN;
nr_pages = get_vm_area_size(area) >> PAGE_SHIFT;
array_size = (nr_pages * sizeof(struct page *));
area->nr_pages = nr_pages;
/* Please note that the recursion is strictly bounded. */
if (array_size > PAGE_SIZE) {
pages = __vmalloc_node(array_size, 1, nested_gfp|__GFP_HIGHMEM,
PAGE_KERNEL, node, area->caller);
} else {
pages = kmalloc_node(array_size, nested_gfp, node);
}
area->pages = pages;
if (!area->pages) {
remove_vm_area(area->addr);
kfree(area);
return NULL;
}
for (i = 0; i < area->nr_pages; i++) {
struct page *page;
if (node == NUMA_NO_NODE)
page = alloc_page(alloc_mask);
else
page = alloc_pages_node(node, alloc_mask, 0);
if (unlikely(!page)) {
/* Successfully allocated i pages, free them in __vunmap() */
area->nr_pages = i;
goto fail;
}
area->pages[i] = page;
if (gfpflags_allow_blocking(gfp_mask))
cond_resched();
}
......
從vmalloc 函數(shù)的實(shí)現(xiàn)看 最終調(diào)用了alloc_page 系列函數(shù)實(shí)現(xiàn) 從伙伴分配系統(tǒng)中分配內(nèi)存。所以所vmalloc 適用了大塊非物理連續(xù)的內(nèi)存分配。 __vmalloc_node_flags(size, NUMA_NO_NODE, GFP_KERNEL | __GFP_HIGHMEM) 函數(shù)中vmalloc 指定了從高端內(nèi)存分配。

6 malloc
6.1 程序在內(nèi)存中的地址
二進(jìn)制程序通常分為text, Data, Bss, 區(qū), 堆和棧。加載到內(nèi)存后的內(nèi)存鏡像如圖所示:

==圖片來源于網(wǎng)絡(luò)==
6.2 sbrk 系統(tǒng)調(diào)用 和 “program break" (程序間斷點(diǎn))
程序間斷點(diǎn)在最開始指向堆區(qū)的起始位置,同時(shí)也是數(shù)據(jù)段的結(jié)尾。 malloc 分配內(nèi)存后,指向分配的內(nèi)存開始的位置。
linux 系統(tǒng)上malloc 的實(shí)現(xiàn)基于sbrk 系統(tǒng)調(diào)用。
p1 = sbrk(0); //sbrk(0)返回當(dāng)前的程序間斷點(diǎn)
p = sbrk(1) //將堆區(qū)的大小加1,但是返回的是p1的位置
參考
如何實(shí)現(xiàn)一個(gè)malloc
內(nèi)存的頁映射
malloc 調(diào)用后,只是分配了內(nèi)存的邏輯地址,在內(nèi)核的mm_struct 鏈表中插入vm_area_struct結(jié)構(gòu)體,沒有分配實(shí)際的內(nèi)存。當(dāng)分配的區(qū)域?qū)懭霐?shù)據(jù)是,引發(fā)頁中斷,建立物理頁和邏輯地址的映射。下圖表示了這個(gè)過程。

在Android 上通過procrank 查看 Vss 和 Rss, Rss 總是小于Vss 就是這個(gè)原因。
generic_x86_64:/ # procrank
PID Vss Rss Pss Uss cmdline
1509 1077592K 117132K 66232K 57296K system_server
1237 901952K 66596K 56300K 52884K zygote
1623 1061168K 98892K 50847K 44164K com.android.systemui
1236 916248K 78992K 29529K 20532K zygote64
1780 1020240K 63484K 20138K 15684K com.android.phone
2004 1014992K 66748K 20112K 14748K com.android.launcher3
| 字段 | 含義 |
|---|---|
| VSS | Virtual Set Size 虛擬耗用內(nèi)存(包含共享庫占用的內(nèi)存) |
| RSS | Resident Set Size 實(shí)際使用物理內(nèi)存(包含共享庫占用的內(nèi)存) |
| PSS | Proportional Set Size 實(shí)際使用的物理內(nèi)存(比例分配共享庫占用的內(nèi)存) |
| USS | Unique Set Size 進(jìn)程獨(dú)自占用的物理內(nèi)存(不包含共享庫占用的內(nèi)存) |
一般來說內(nèi)存占用大小有如下規(guī)律:VSS >= RSS >= PSS >= USS
參考:
How the Kernel Manages Your Memory
部分內(nèi)容來源于網(wǎng)絡(luò),沒有一一注明
