最近在学内核内存管理方面知识,查看相关书籍后整理了一下笔记。内核中对内存管理的实现涵盖了如下几方面知识:
    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);

五、进程地址空间

未完待续。。。