最近在学内核内存管理方面知识,查看相关书籍后整理了一下笔记。内核中对内存管理的实现涵盖了如下几方面知识:
1、内存中的物理内存页的管理
2、分配连续内存的系统
3、分配较小块内存的slab、slub和slob分配器
4、分配非连续内存块的vmalloc机制
5、进程的地址空间
接下来逐个介绍。
一、内存中的物理内存管理
----页(Page)的概念
在内核中,内存管理单元MMU(负责虚拟地址和物理地址转换的硬件)是把物理页作为内存管理的基本单位。体系结构不同,支持的页大小也不尽相同。通常来说32位体系结构支持4KB的页,64位体系结构一般会支持8KBD的页大小。我当前使用的MIPS64架构的主机,支持的页大小为16KB。如果你使用的主机系统是Linux。那么你可以使用如下命令查看当前主机的也页大小:
$ getconf PAGE_SIZE
16384 //16KB
内核用struct page 结构来表示系统中的每个物理页,结构体信息如下所示:
struct page {
unsigned long flags; //表示当前页的状态。包括页是不是脏的、是否被锁定等。具体数值参考page-flags.h
atomic_t __count; //存放当前页的引用计数。代码中可以使用page_count()获取,返回0表示页空闲
struct address_space *mapping; //
...
pa-ff_t index;
struct list_head lru;
void *virtual; //页对应的虚拟地址
}
注意:page结构与物理页相关,而并非与虚拟页相关。
通过这个结构,我们在内核中不仅可以知道一个页是否空闲,还能知道这个页的拥有者是谁。拥有者可能是用户空间进程、动态分配的内核数据、静态内核代码或者页高速缓存等。
关于struct page结构体更为详细的介绍,可以参考https://www.cnblogs.com/still-smile/p/11564671.html
----区(Zone)的概念
区是比页更大的一个概念。内核中使用区(zone)对具有相似特性的页进行分组。Linux主要使用了四种区:
- ZONE_DMA 这个区的页面可以执行DMA操作
- ZONE_DMA32 功能同ZONE_DMA,区别在于在此区域的页只能被32位设备访问。
- ZONE_NORMAL 正常可寻址的页。一般用途的内存都可以从此区分配需要的页
- ZONE_HIGHEM 一般是为了解决物理内存大于虚拟内存数量时的地址映射。例如在IA-32系统上,可以直接管理的物理内存数量不超过896MiB。超过该值的内存只能通过高端内存寻址。目前多数大芯片为64位体系结构,所能管理的地址空间巨大,所以ZONE__HEGHEM基本不用。
区的结构为struct zone,具体内容定义在内核源码include/linux/mmzone.h中。
----存储节点(Node)的概念
从系统体系架构来说,主机可以分为如下种类:
- UMA(一致内存访问)结构 各CPU共享相同的物理内存,每个 CPU访问内存中的任何地址所需时间是相同的。因此此类结构也称对称多处理器结构(SMP)。
- NUMA(非一致内存访问)各CPU都有本地内存,可支持特别快速的访问。各个CPU之间通过总线连接起来,以支持对其他CPU的本地内存访问,当然比访问本地内存慢些。
这两种架构系统的差别如下图(引子《深入linux内核架构》一书)
在NUMA架构的内核中,内存被划分为节点Node。每个节点关联到系统中的一个处理器(这个不太严禁,在下面的例子中会体现)。在内核中表示为pg_data_t的实例。结构体里面的具体内容可以参考内核源码(定义在include/linux/mmzone.h)。
各个节点有可以划分为内存区Zone。
到此,我们就可以知道内核中对物理内存的管理从上到下是节点Node、区(Zone)和页(Page)。
下面是我所用的主机(龙芯3A3000处理器)开机启动时,内核的信息:
[ 0.000000] NUMA: Discovered 4 cpus on 1 nodes
[ 0.000000] Debug: node_id:0, mem_type:1, mem_start:0x200000, mem_size:0xee MB
[ 0.000000] start_pfn:0x80, end_pfn:0x3c00, num_physpages:0x3b80
[ 0.000000] Debug: node_id:0, mem_type:2, mem_start:0x90200000, mem_size:0x1cfe MB
[ 0.000000] start_pfn:0x24080, end_pfn:0x98000, num_physpages:0x77b00
[ 0.000000] Zone ranges:
[ 0.000000] DMA32 [mem 0x00200000-0xffffffff]
[ 0.000000] Normal [mem 0x100000000-0x25fffffff]
可以看到:
1、多处理器共用一个node
“4 cpus on 1 nodes”说明此3A3000是4个CPU挂载了一个node上,还不是上面所说的NUMA架构。
2、该node被分为2个区。
mem_type:1 就是ZONE_DMA32 区,大小为0xeeMB ,此区被分为0x3b80个块。
mem_type:2 就是ZONE_NORMAL,大小为0x1cfeMB,此区被分为0x77b00个块。
二、分配连续大内存的接口
内核提供了请求内存的底层机制,并提供了相关接口。所有接口都是以页为单位分配内存。最核心的函数有(请查看include/linux/gfp.h):
1、struct page *alloc_pages(gfp_t gfp_mask, unsigned int order) ;
获取2的order次方个连续的物理页。成功返回第一个页的首地址,失败返回NULL。
gfp_t为分配器标志,就是告诉内核应当如何分配内存。例如告诉内核分配内存时是否可以睡眠、在哪个区分配等。
2、unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);
功能同1,也是获取连续的物理页,不过成功后返回的是逻辑地址。
3、void __free_pages(struct page *page, unsigned int order);
释放页。
上面的接口函数是以页为单位分配物理内存。对于常用的以字节为单位的分配来说,内核提供的函数是kmalloc (include/linux/slab_def.h)
4、static __always_inline void *kmalloc(size_t size, gfp_t flags)
分配size大小的物理内存,此物理内存必须是连续的 。成功返回内存块的起始地址,失败返回NULL
5、void kfree(const void *);
释放由kmalloc申请的内存区域。
三、分配非连续内存块的vmalloc机制
大多数情况下,只有硬件设备才需要使用物理地址连续的内存(很多硬件设备存在于内存管理单元之外,无法理解什么是虚拟地址),或者出于性能考虑才要求物理地址连续。普通程序只关心虚拟地址连续即可。
vmalloc函数就是只确保虚拟地址连续,物理地址是否连续无所谓。
vmalloc()函数(请查看include/linux/gfp.h)用法同用户空间的malloc()。接口如下:
1、分配一段空间
void *vmalloc(unsigned long size);
2、释放一段空间
void vfree(const void * addr);
注意:malloc()的系统调用接口是sys_blk() 。而非vmalloc。后面文章会有介绍。
四、分配较小块内存的slab分配器
通常我们编程会面临的问题的某个数据结构(或者对象)会频繁的分配和释放,这会带来的两个问题是内存碎片和效率低下。要解决这个问题的办法就是使用缓存机制(高速缓存)。就是预先分配一个比较大的空间,后面每次的 内存申请和释放都是对这块内存空间的操作。这个高速缓存可以由多个slab组成。而slab又由一个或者多个物理上连续的页组成(通常一个slab仅仅由一页组成),里面存放多个有着相同数据结构的对象。
常用到的缓存接口如下(请查看include/linux/slab.h):
1、缓存区创建
kmem_cache_create(const char * name,** //高速缓存的名字
size_t size, //要缓存的数据结构(对象)的大小
size_t align, //slab内第一个对象的偏移,用来确保对齐
unsigned long flags, //可选项,用来控制高速缓存的行为
void (*)(void *));//
2、销毁缓存
void kmem_cache_destroy(struct kmem_cache *);
3、从缓存中获取一个数据结构(对象)
void *kmem_cache_alloc(struct kmem_cache *, gfp_t);
4、从缓存中释放一个数据结构(对象)
void kmem_cache_free(struct kmem_cache *,void *objp);
五、进程地址空间
未完待续。。。