LINUX内核(之3):内存管理之页page、区Zone、节点node、分配器slab

最近在学内核内存管理方面知识,查看相关书籍后整理了一下笔记。内核中对内存管理的实现涵盖了如下几方面知识:

一、内存中的物理内存管理


----页(Page)的概念

页有时也成为页帧。在内核中,内存管理单元MMU(负责虚拟地址和物理地址转换的硬件)是把物理页Page作为内存管理的基本单位。体系结构不同,支持的页大小也不尽相同。通常来说32位体系结构支持4KB的页,64位体系结构一般会支持8KB的页大小。我当前使用的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结构与物理页相关,而并非与虚拟页相关。
通过这个结构,我们在内核中不仅可以知道一个页是否空闲,还能知道这个页的拥有者是谁。拥有者可能是用户空间进程、动态分配的内核数据、静态内核代码或者页高速缓存等。
脏页:如果与硬盘上的数据相比,页的内容已经改变,则置位 PG_dirty 。设置了该标志的页称为脏的。
关于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)的概念

在NUMA模型中,一个存储节点Node结点关联到系统中的一个处理器,在内核中表示为 pg_data_t 的实例。从系统体系架构来说,主机可以分为如下种类:

  • UMA(一致内存访问)结构 各CPU共享相同的物理内存,每个 CPU访问内存中的任何地址所需时间是相同的。因此此类结构也称对称多处理器结构(SMP
  • NUMA(非一致内存访问)各CPU都有本地内存,可支持特别快速的访问。各个CPU之间通过总线连接起来,以支持对其他CPU的本地内存访问,当然比访问本地内存慢些。
    这两种架构系统的差别如下图(引子《深入linux内核架构》一书)
    在这里插入图片描述
    在NUMA架构的内核中,内存被划分为节点Node。每个节点关联到系统中的一个处理器(这个不太严禁,在下面的例子中会体现)。在内核中表示为pg_data_t的实例。结构体里面的具体内容可以参考内核源码(定义在include/linux/mmzone.h)。
    各个节点有可以划分为内存区Zone。

到此,我们就可以知道内核中对物理内存的管理从上到下是节点Node、区(Zone)和页(Page)。

一个CPU或多个CPU对应一个节点Node, 一个节点可以划分为多个区Zone,一个区管理多个页Page、页为内核管理的最小单位。

这3个概念的数据结构对应关系如下图:

在这里插入图片描述

下面是我所用的主机(龙芯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上,也就是龙芯3A3000是SMP架构,不是上面所说的NUMA架构。
2、该node被分为2个区。
     mem_type:1 就是ZONE_DMA32 区,大小为0xeeMB ,此区被分为0x3b80个块。
     mem_type:2 就是ZONE_NORMAL,大小为0x1cfeMB,此区被分为0x77b00个块。

龙芯3A3000处理器是用在桌面电脑上的,而龙芯3B3000是用在服务器机上的,通常是双路或4路,所以里面架构是NUMA的,通过内核开机log可以查看到:

[    0.000000] NUMA: Discovered 8 cpus on 2 nodes
[    0.000000] Debug: node_id:0, mem_type:1, mem_start:0x0, mem_size:0xf0 MB
[    0.000000]        start_pfn:0x0, end_pfn:0x3c00, num_physpages:0x3c00
[    0.000000] Debug: node_id:0, mem_type:2, mem_start:0x410000000, mem_size:0x1f00 MB
[    0.000000]        start_pfn:0x104000, end_pfn:0x180000, num_physpages:0x7fc00
[    0.000000] Debug: node_id:0, mem_type:2, mem_start:0x610000000, mem_size:0x1f00 MB
[    0.000000]        start_pfn:0x184000, end_pfn:0x200000, num_physpages:0xfbc00
[    0.000000] node0's addrspace_offset is 0x0
[    0.000000] Node0's start_pfn is 0x0, end_pfn is 0x200000, freepfn is 0xcea
[    0.000000] Debug: node_id:1, mem_type:1, mem_start:0x0, mem_size:0xf0 MB
[    0.000000]        start_pfn:0x40000000, end_pfn:0x40003c00, num_physpages:0xff800
[    0.000000] Debug: node_id:1, mem_type:2, mem_start:0x410000000, mem_size:0x1f00 MB
[    0.000000]        start_pfn:0x40104000, end_pfn:0x40180000, num_physpages:0x17b800
[    0.000000] Debug: node_id:1, mem_type:2, mem_start:0x610000000, mem_size:0x1f00 MB
[    0.000000]        start_pfn:0x40184000, end_pfn:0x40200000, num_physpages:0x1f7800

里面的具体意思不再做解释,读者可以自行思考。

二、页表(pgtable)

用来将虚拟地址空间映射到物理地址空间的数据结构称为页表。
内存地址分解: 内核内存管理总是假定使用四级页表,而不管底层处理器是否如此。根据四级页表结构的需要,虚拟内存地址分为5部分(4个表项用于选择页, 1个索引表示页内位置)。如下图:
在这里插入图片描述
    虚拟地址的第一部分称为全局页目录(Page Global Directory,PGD)。PGD用于索引进程中的一个数组(每个进程有且仅有一个),该数组是所谓的全局页目录或PGD。PGD的数组项指向另一些数组的起始地址,这些数组称为中间页目录(Page Middle Directory,PMD)。如果存在上层页目录(Page Up Directory PUD),那么PGD指向PUD。
    虚拟地址中的第二个部分称为PMD,在通过PGD中的数组项找到对应的PMD之后,则使用PMD来索引PMD。PMD的数组项也是指针,指向下一级数组,称为页表或页目录。
    虚拟地址的第三个部分称为PTE(Page Table Entry,页表数组),用作页表的索引。虚拟内存页和页帧之间的映射就此完成,因为页表的数组项是指向页帧的。
    虚拟地址最后的一部分称为偏移量。它指定了页内部的一个字节位置。归根结底,每个地址都指向地址空间中唯一定义的某个字节。
内核中每个体系架构都对此4级页表的每一级大小做了宏定义:

#define PAGE_SIZE       (1UL<<PAGE_SHIFT)
#define PUD_SIZE	       (1UL<<PUD_SHIFT)
#define PMD_SIZE        (1UL<<PMD_SHIFT)
#define PGDIR_SIZE    (1UL<<PGDIR_SHIFT)

各层页表对应的数据结构:
 pgd_t 用于全局页目录项。
 pud_t 用于上层页目录项。
 pmd_t 用于中间页目录项。
 pte_t 用于直接页表项。
用于处理内存页的体系结构相关状态的函数:

函数功能
pte_present页在内存中吗(页可能换出到交换区)
pte_read从用户空间可以读取该页吗
pte_write可以写入到该页吗
pte_exec该页中的数据可以作为二进制代码执行吗
pte_dirty页是脏的吗?(即页的内容是否已经修改过)
pte_file该页表项属于非线性映射吗
pte_young访问位(通常是_ PAGE_ACCESS )设置了吗
pte_rdprotect清除该页的读权限
pte_wrprotect清除该页的写权限
pte_exprotect清除执行该页中二进制数据的权限
pte_mkread设置读权限
pte_mkwrite设置写权限
pte_mkexec允许执行页的内容
pte_mkdirty将页标记为脏
pte_mkclean“清除”页,通常是指清除 _PAGE_DIRTY 位
pte_mkyoung设置访问位,在大多数体系结构上是 _PAGE_ACCESSED
pte_mkold清除访问位

# 三、分配连续大内存的接口 内核提供了请求内存的底层机制,并提供了相关接口。所有接口都是以页为单位分配内存。最核心的函数有(请查看 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);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

海棠花败

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值