前言:这里来分析一下Linux内核内存分配的原理
1、物理内存的管理
先来简单了解几个概念
1.1 内存节点 node
在计算机世界中,有两种物理内存的管理方式被广泛使用,他们分别是:
UMA(一直内存访问)模型
NUMA(非一致内存访问)模型
两种模型的区别如下:
在linux源码中以struct pglist_data数据结构来表示单个的内存节点,对于NUMA模型,多个内存节点通过链表链接起来,对于UMA模型,因为只有一个这种node,所以不存在链表。
1.2 内存区域ZONE
内存区域属于单个内存节点的概念,linux将每个内存节点管理的物理内存划分为不同的内存区域,linux中使用struct zone数据结构表示 每一个内存区域,内存区域的类型用zone_type表示,是一枚举变量:
enum zone_type{
ZONE_DMA,
ZONE_DMA32,
ZONE_NORMAL,
ZONE_HIGHMEM,
ZONE_MOVEBLE,
_MAX_NR_ZONES
};
1.3 内存页
内存页是物理内存管理中的最小单位,也叫页帧,linux会为系统物理内存的每个页都创建一个struct page对象,系统用一个全局变量struct page *mem_map 来存放所有的物理页page对象的指针,页的大小取决于系统中的内存管理单元MMU,后者用来将虚拟空间的地址转化为物理空间的地址。
2.页面分配器
Linux系统对物理内存进行分配的核心建立在页面级的伙伴系统之上,在系统的初始化期间,伙伴系统负责对物理内存页面进行跟踪,记录哪些是已经被内核使用的页面,哪些是空闲页面。
这里先给出一个图,供大家了解:
由上图可以看出,每个物理内存被分为三个区域,mem_map链表与这三个物理内存空间也是对应的,linux初始化期间,会将虚拟地址的物理页面直接映射区做线性地址映射到ZONE_NORMAL与ZONE_DMA,这就意味着如果页面分配器分配的页面位于这两个区,对应的内核虚拟地址到物理地址的映射的页目录表项已经建立,而且是线性映射。如果页面分配器去ZONE_HIGHMEM区域分配页面,这种情况下首先需要内核在动态映射区或者固定映射区分配一个虚拟地址,然后映射到该物理页面上。当然内核实现了这些接口函数,我们下面来看看这些接口函数
2.1 gpf_mask
gpf_mask是页面分配函数的重要参数,使用用于控制分配行为的掩码,并告诉内核应该到哪个zone中区分配空间。我们这里重点说一下GPF_KERNEL和GPF_ATOMIC:
GPF_ATOMIC内核模块中最长使用的掩码之一,用于原子分配,此掩码告诉分配器,在分配内存页面时,绝对不行中断当前进程或把当前进程移除调度器,在驱动程序中,一般在中断例程后者非进程上下文的代码中使用。这两种情况下分配都必须保证当前进程不能睡眠。
GPF_KERNEL内核模块中最长使用的掩码之一,带有该掩码的 内存分配可能导致当前进程进入睡眠状态。
对于驱动开发人员来说,我们可能更加关心分配器到那个区域去分配物理页面,如果gpf_mask中没有明确指定**_GFP_DMA或者_GFP_HIGHMEM**,那么默认的就去ZONE_NORMAL中区分配,如果当前区没有,则去ZONE_DMA。
若指定了**_GFP_DMA**,则只能在ZONE_DMA中分配物理页面,若无法满足,则分配失败。
若指定了**_GFP_HIGHMEM**,则先去ZONE_HIGHMEM区域中查找内存,如果无法满足,则去ZONE_NORMAL,若是还无法满足,则去ZONE_DMA区域中。
2.2 alloc_pages
在linux源码中alloc_pages以宏的形式出现,
_alloc_pages函数负责分配2的order次方个连续的物理页面并返回起始页面的struct page实例。在调用这个函数的时候,分为两种情况,如下:
1)如果gpf_mask没有明确指定_GFP_HIGHMEM,那么分配的页面就来自于ZONE_NORMAL或者ZONE_DMA。由于这两个区域内核在初始化阶段就为之建立了映射关系,所以内核可以使用page_address来获得对应页面的内核虚拟地址KVA。
2)如果指定了_GFP_HIGHMEM,那么页分配器将优先在ZONE_HIGHGMEM中分配物理页,但是也不排除没有足够空间而导致去另外两个区域分配。对于新分配的高端屋里页面,由于内核尚未在页表中为之建立映射关系,所以此时需要:
a) 在内核的动态映射区分配一个KVA
b) 通过操作页表,将1中的KVA映射到该物理页面上,内核为此提供了一个函数Kmap与Kunmp。
2.3 _get_free_pages
这个函数原型这里不贴出来了,这个函数主要的作用是在非高端内存区分配2的order次方个物理内存页面,返回起始页面所在的内核线性地址,因为行数内部自己调用了page_address函数。
这里再介绍两个在非高端分配屋里页面的函数:
get_zeroed_pages 用于分配一个物理页面并将页面对应的内容填充为0,函数返回页面所在的内核线性地址。
_get_dma_pages 用于从ZONE_DMA区域分配物理页。返回页面所在的线性地址。
2.4 释放函数_free_pages、free_pages
两个函数的别去就是第一个参数的类型,_free_pages释放的是alloc_pages分配的page实例,所以传入的类型是page,free_pages释放的是_get_free_pages系列分配的内存,所以第一个参数是线性地址。
3.slab分配器
上面提到的连续物理页面的分配,但是只有物理页面的分配不够的,因为大多数情况下我们内核使用内存都是很小的,没有到达4KB,所以如果都使用页分配,物理空间是肯定不够用的,所以内核实现了slab分配器。slab分配器的思想就是先利用页分配器分配出一个单个或者一组的物理页面,然后将在此基础上将整个页面分割成多个相等的小内存单元,以满足小内存空间分配的需要。
slab的实现原理是很复杂的,这里我们就不讨论了,只列出下面几个常用的接口函数:
void *kmalloc(size_t size, gfp_t flags) ----->kzalloc(size_t size, gfp_t flags) 在kmalloc的基础上初始化分配的内存为0
功能:分配一块物理内存,这块内存是连续的,最大是128K,分配的内存的2的次幂的格式
参数:
@size :分配内存的大小
@flags: GFP_ATOMIC(不会休眠,可以在中断上下文使用)
GFP_KERNEL(使用这个宏分配内存的时候,可以休眠,只能在进程上下文使用)
返回值: 成功内存的地址 失败NULL
void kfree(const void *objp)
功能:释放内存
参数:
@objp :使用kmalloc分配到的内存的首地址
返回值:无
4.虚拟地址的管理
主流的32位处理器,能寻址2的32次方也就是4GB大小的地址空间,这部分空间称为虚拟地址空间。从虚拟地址空间到物理地址的转换通过处理器中的一个部件内存单元MMU。为了完成这种转换,操作系统必须建立适当的页表。我们知道4GB的虚拟地址空间,高的3G-4G属于内核空间,我们这里只研究内核空间的地址。
4.1 内核虚拟地址空间构成
由上图可知,内核空间分为三部分,每个部分中间有黑洞,空洞不做任何映射,防止越界。
我们可以看到其中有部分是vmalloc区,我们分配内存的接口中就有一个vmalloc函数,该函数就是对vmalloc区进行操作,他的特点是分配的虚拟地址空间是连续的,但是分配的物理地址不一定连续。
vmalloc实现的步骤如下:
1)在vmalloc区分配出一段连续的内存
2)通过伙伴系统获得物理页
3)通过对页表的操作,将步骤一种分配的虚拟地址内存映射到步骤2中获得的物理页上。
但是在驱动的开发过程中,不建议使用该函数来申请内存。原因比较多,这里就不研究了。
4.2 ioremap
ioremap的作用是将vmalloc区的某段虚拟内存块映射到IO空间,其实现原理与vmalloc完全一样。
释放使用iounmap。