1 几个概念

针对物理内存定义方面,引入了内存节点,内存区域内存页的概念;

1.1 内存节点node

UMA系统只有一个内存节点,NUMA有多个内存节点;NUMA,cpu访问本地内存节点的速度快,访问其他节点的内存慢,节点越远,访问速度越慢;
struct pglist_data 描述单个内存节点;

1.2 内存区域ZONE

内存区域是单个节点内部的概念;
struct zone描述一个内存区域,ZONG_DMA,ZONE_NORMAL,ZONE_HIGHMEM等

1.3 内存页

页是内存管理的最小单位,系统为每个页创建一个struct page对象;
全局变量struct page *mem_map来存放所有物理页page对象指针。

系统初始化时,将内核虚拟地址空间的物理页面线性映射到ZONG_DMA和ZONG_NORMAL两个内存区域;针对ZONE_HIGHMEM区域,通过MMU管理虚拟地址到物理地址的转换。

1.4 页表结构

将虚拟地址分段,每段虚拟地址做为索引指向页表,页表项则逐级指向下一级页表,最终找到物理页面。
32bit 分页机制下虚拟地址是由32bit组成的,常规4KB分页,32位的虚拟地址被分成3个域。
Directory(目录) Table(页表) Offset(偏移)
最高10位 中间10位 最低12位
具体的地址转换过程,文字描述太累,看图直观一些:
内存管理——学习笔记 - 图1

依据以下步骤进行转换:
1)从cr3中取出进程的页目录地址(操作系统负责在调度进程的时候,把这个地址装入对应寄存器);
2)根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。(又引入了一个数组),页的地址被放到页表中去了。
3)根据线性地址的中间十位,在页表(也是数组)中找到页的起始地址;
4)将页的起始地址与线性地址中最后12位相加,得到最终我们想要的葫芦;

2 页面分配

页面分配的两个核心函数,alloc_pages和get_free_pages,get_free_pages不能从高端内存分配物理页;

2.1 gfp_mask,分配内存的标志

GFP_ATOMIC,是以原子方式分配内存,不会引起睡眠。允许使用紧急分配链表中的保留内存页;
GFP_KERNEL,会引起睡眠;
GFP_HIGHUSER,可以使用非线性映射的高端内存,kmalloc不行,需要通过vmalloc;
申请内存时,如果没有指明gfp_mask在哪个内存区域申请内存,默认在ZONE_NORMAL中分配,NORMAL内的空闲页不足时,会到ZONE_DMA中分配。

2.2 alloc_pages

alloc_pages,最终会调用__alloc_pages函数,负责分配2^order个连续物理页面,返回值为连续物理页面的起始页面的struct page;
*如果gfp_mask未指定从高端内存获取物理页面,那么内核会使用page_address来获取对应页面的虚拟地址
kva(kernel virtual address);
page_address原理:
1)获取页号:pfn = page - mem_map,mem_map存放所有物理页指针,由于NORMAL或DMA是线性映射;
2)获取页物理地址 pg_pa=pfn<3)返回物理页对应的内核虚拟地址: _va(pg_pa)

如果指定从高端内存获取物理页面,由于内核没在页表中做映射,需要如下操作
1)在内核动态映射去分配一个kva,
2)通过页表将这个kva映射到高端内存的物理页面上
通过kmap实现;kmap可能会导致睡眠,kmap_atomic是原子操作。kunmap,来释放页表中对kva与高端内存页面的映射关系;

2.3 __get_free_pages

函数负责分配2^order次方个连续物理页面,返回起始页面所在的内核线性地址;通过返回值也能说明此函数不能申请高端内存页,因为高端内存页是非线性映射的;

3 slab

slab分配器思想,先利用页面分配器分配出一组连续的物理页面,然后在此基础上划分成多个相等的小内存单元,以满足小内存空间的分配需要;分配的对象在释放后,slab并不会把占用的内存还给伙伴系统,当下次分配对象时,会直接使用,不需要重新分配,初始化等操作;

资料中说的slab分配器不是指slab,而是kmem_cache;cache_chain是个双向链表,将kmem_cache链接起来。
kmem_cache与slab的关系:内核在初始化时会通过kmem_cache_init初始化kmem_cache,kmem_cache中的每个对象的大小 按照32字节,64字节……131072字节(128K);

内存管理——学习笔记 - 图2

3.1 重要数据

3..1.1 struct kmem_cache

成员
unsigned int gfporder,指明该kmem_cache中的每个slab占用的页面数量,即2^order个物理页面;
struct list_head next,将该kmem_cache加入到cache_chain链表中;
kmem_list3 有三个链表:
struct list_head slabs_partial,将该kmem_ache中所有半空的slab加入到这链表,链表内的每个slab所在的物理内存页还有部分空闲,可以继续分配
struct list_head slabs_free,将该kmem_ache中所有完全空闲的slab加入到这链表,链表内的每个slab所在的物理内存页完全空闲,没有任何内存对象;
struct list_head slabs_full,将该kmem_ache中所有已满的slab加入到这链表,,链表内的每个slab所在的物理内存页都已分配完;

3.1.2 struct slab

用来管理一块连续的物理内存页中内存对象的分配;

3.2 kmem_cache的操作

slab分配器需要kmem_cache这个实例,在slab还没有建立的时候,系统静态的定义了cache_cache(kernel4.4版本为kmem_cache_boot),如下
/ internal cache of cache description objs /
static struct kmem_cache kmem_cache_boot = {
.batchcount = 1,
.limit = BOOT_CPUCACHE_ENTRIES,
.shared = 1,
.size = sizeof(struct kmem_cache),
.name = “kmem_cache”,
};
通过kmem_cache_init 初始化kmem_chain的链表上的kmem_cache,按照2^order倍数分配内存;

3.2.1 kmem_cache_create

创建一个kmem_cache,对齐方式必须是硬件缓存行对齐;此时心创建的kmem_cache内没有内存;

3.2.2 kmem_cache_alloc

向指定的kmem_cache中分配对象,,这个函数从缓存中返回一个对象。注意如果缓存目前为空,那么这个函数就会调用 cache_alloc_refill 向缓存中增加内存

4 kmalloc

void *kmalloc(size_t size,gfp_t flgas)
1)根据size查找到kmem_chain链表中,满足size分配需求且最小的kmem_cache
2)调用kmem_cache_alloc函数在这个kmem_cache进行内存分配,返回一个内存对象;

注意:
1、kmalloc不能申请高端内存,在cache_grow中会擦掉flags中高端内存的标志;
2、kmalloc单次最大能申请128k字节大小的内存,这是因为kmalloc基于slab机制,而slab分配字节的范围是32——128K字节;

5 伙伴系统简述

6 MMU简述