Linux对内存的管理划分成三个层次,分别是Node、Zone、Page。对这三个层次简介如下:
层次 说明
Node(存储节点) CPU被划分成多个节点,每个节点都有自己的一块内存,可以参考NUMA架构有关节点的介绍
Zone(管理区) 每一个Node(节点)中的内存被划分成多个管理区域(Zone),用于表示不同范围的内存
Page(页面) 每一个管理区又进一步被划分为多个页面,页面是内存管理中最基础的分配单位
它们之间的关系,用一张经典的图表示如下:图片来源
接下来我们重点讲述的是ZONE_HIGNMEM,也就是高端内存。
1 高端内存的由来
在32bit的x86机器中,4G的虚拟地址空间被划分成3G的用户空间(0—3G),1G的内核空间(3-4G)。在64bit的机器中于此不同。也就是说虚拟地址空间从0x0000 0000~0xBFFF FFFF为用户空间,0xC000 0000~0xFFFF FFFF为内核空间。Linux把每个内存节点的物理内存划分成3个管理区,也就是划分成ZONE_DMA、ZONE_NORMAL、ZONE_HIGHMEM。它们的介绍如下:
内存区域 说明
ZONE_DMA 范围是0~16M,该区域的物理页面专门供I/O设备的DMA使用
ZONE_NORMAL 范围是16~896M,该区域的物理页面是内核能够直接使用的
ZONE_HIGHMEM 范围是896~结束,高端内存,内核不能直接使用
上面说到虚拟地址空间中的3~4G为Linux内核虚拟地址空间,其中内核空间3G~3G+896M对应物理内存空间的0~896M,并且在内核初始化时就已经将3G~3G+896M(0xC0000 0000~0xF7FF FFFF)虚拟地址空间与物理内存空间的0~896M进行了对应,内核在访问0~896M的内存的时候,只需要将其对应的虚拟地址空间增加一个偏移量就可以了。对于超过896M的物理地址空间,它们属于高端内存,高端内存区包含的内存页不能由内核直接访问,尽管它们也线性地映射到了线性地址空间的第4个G。也就是说对于高端内存区,只有128M的线性地址(0xF800 0000~0xFFFF FFFF)留给它进行映射。
有这样一个问题:内核虚拟地址空间只有1G,也就是说内核只能访问1G物理内存空间,但是如果物理内存是2G,那内核如何访问剩余的1G物理内存空间呢?按照我们刚才说的,这2G的物理内存地址被划分成了3个Zone,物理内存0~896M是内核直接可以访问的,896M~2G这一部分内核要如何访问呢?实际上,当内核想要访问高于896M的物理内存空间时,会从0xF800 0000~0xFFFF FFFF这一块线性地址空间中找一段,然后映射到想要访问的那一块超过896M部分的物理内存,用完之后就归还。由于0xF800 0000~0xFFFF FFFF这一块没有和固定的物理内存空间进行映射,也就是说,这128M的线性地址空间可以和高于896M的物理内存空间短暂的、任意的建立映射,循环使用者128M线性地址空间,这样内核就可以访问剩下的高于896M的物理内存空间了。
2 高端内存简介
刚才说虚拟地址空间的3G~4G部分属于Linux内核空间,其中1G的虚拟内核空间如下图:
上图中PAGE_OFFSET通常为0xC000 0000,而high_memory指的是0xF7FF FFFF,在物理内存映射区和和第一个vmalloc区之间插入的8MB的内存区是一个安全区,其目的是为了“捕获”对内存的越界访问。处于同样的理由,插入其他4KB大小的安全区来隔离非连续的内存区。Linux内核可以采用三种不同的机制将页框映射到高端内存区,分别叫做永久内核映射、临时内核映射以及非连续内存分配。相关区域介绍如下:
虚拟内存区域 说明
物理内存映射 映射了物理内存空间0~896M部分,这一部分是内核可以直接访问的
永久内核映射 允许内核建立高端页框到内核虚拟地址空间的长期映射。永久内核映射不能用于中断处理程序和可延迟函数,因为建立永久内核映射可能阻塞当前进程
固定映射的线性地址空间 其中的一部分用于建立临时内核映射
vmalloc区 该区用于建立非连续内存分配,本文重点讨论的内容
实际上,把内存区映射到一组连续的页框是最好的选择,这样可以充分利用高速缓存就并获得较低的平均访问时间。不过,如果对内存区的请求不是很频繁,那通过连续的线性地址来访问非连续的页框这样一种分配方式将会很有意义,因为这样可以避免外部碎片,而缺点是必须打乱内核页表。在内核虚拟地址空间中,为非连续内存区保留的线性地址空间的起始地址由VMALLOC_START宏定义,而末尾地址有VMALLOC_END宏定义。下面重点讲述非连续内存区,也就是vmalloc区。
3 vmalloc区相关数据结构
在Linux的实现中,每一个非连续内存区都对应着一个类型为struct vm_struct的描述符,源代码如下:源代码网址
struct vm_struct {
struct vm_struct *next;/*指向下一个vm_struct结构体的指针*/
void *addr;/*内存区内第一个内存单元的线性地址*/
unsigned long size;/*内存区的大小(以字节为单位)加4096字节(4096为内存区之间的安全区间的大小)*/
unsigned long flags;/*非连续内存区映射的内存类型*/
struct page **pages;/*指向nr_pages数组的指针,该数组有指向页描述符的指针组成*/
unsigned int nr_pages;/*内存区填充的页的个数*/
phys_addr_t phys_addr;
const void *caller;
};
下面我们重点分析vmalloc()和vfree()函数的实现。下面是vmalloc()函数的源代码:源代码网址
static void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask,
pgprot_t prot, int node)
{
const int order = 0;
struct page **pages;
unsigned int nr_pages, array_size, i;
gfp_t nested_gfp = ((GFP_KERNEL | __GFP_HIGHMEM) & GFP_RECLAIM_MASK) | __GFP_ZERO;
/*
计算连续的线性地址空间对应多少个页框
*/
nr_pages = get_vm_area_size(area) >> PAGE_SHIFT;
/*
接下来需要创建一个数组,数组元素为指向page的指针,数组元素个数nr_pages
这一步是用来计算创建数组需要多大的内存空间
*/
array_size = (nr_pages * sizeof(struct page *));
/*设置area结构体中nr_pages域的值*/
area->nr_pages = nr_pages;
/* Please note that the recursion is strictly bounded. */
/*下面的if...else...就是创建一个数组,返回的是一个二级指针*/
if (array_size > PAGE_SIZE) {
pages = __vmalloc_node(array_size, 1, nested_gfp|__GFP_HIGHMEM,
PAGE_KERNEL, NUMA_NO_NODE, area->caller);
area->flags |= VM_VPAGES;
} else {
pages = kmalloc_node(array_size, nested_gfp, NUMA_NO_NODE);
}
/*设置设置area结构体中pages域*/
area->pages = pages;
if (!area->pages) {//错误处理
remove_vm_area(area->addr);
kfree(area);
return NULL;
}
/*
接下来就是重头戏,要分配页框了...
需要说明的是,这里是一个一个页框的分配,这些页框在物理内存上不一定连续
刚才创建的数组,在下面也用到了...
如果页面分配正常,没有出错就继续下面的处理,分配失败则需要进行出错处理
*/
for (i = 0; i < area->nr_pages; i++) {
struct page *page;
gfp_t tmp_mask = (GFP_KERNEL | __GFP_HIGHMEM) | __GFP_NOWARN;
if (NUMA_NO_NODE == NUMA_NO_NODE)
page = alloc_page(tmp_mask);
else
page = alloc_pages_node(NUMA_NO_NODE, tmp_mask, order);
if (unlikely(!page)) {
/* Successfully allocated i pages, free them in __vunmap() */
area->nr_pages = i;
goto fail;
}
area->pages[i] = page;
}
/*
执行到这里,说明前面的线性地址空间获取、页面分配都没有错误
连续线性地址空间有了,物理页面也获取到了,接下来需要做的就是将线性地址空间与这些
页框建立联系,而map_vm_area函数所做的就是修改内核使用的页表项,以此表明分配给
非连续内存区的每个页框现在对应着一个线性地址
看一下map_vm_area函数的调用链,其中缩进表示函数调用,处于同一缩进的表示函数的调用先后
--map_vm_area
--vmap_page_range
-- vmap_page_range_noflush
--pgd_offset_k
--pud_alloc
--vmap_pud_range
--pud_alloc
--vmap_pmd_range
--pmd_alloc
--vmap_pte_range
--pte_alloc_kernel
--set_pte_at
*/
if (map_vm_area(area, PAGE_KERNEL, &pages))
goto fail;
return area->addr;
fail:
warn_alloc_failed(GFP_KERNEL | __GFP_HIGHMEM, order,
"vmalloc: allocation failure, allocated %ld of %ld bytes\n",
(area->nr_pages*PAGE_SIZE), area->size);
vfree(area->addr);
return NULL;
}
/**
* vmalloc - allocate virtually contiguous memory
* @size: allocation size
* Allocate enough pages to cover @size from the page level
* allocator and map them into contiguous kernel virtual space.
*
* For tight control over page level allocator and protection flags
* use __vmalloc() instead.
*/
void *vmalloc(unsigned long size)
{
struct vm_struct *area;
void *addr;
unsigned long real_size = size;
size = PAGE_ALIGN(size);
/*
如果申请的字节数为0或者申请的页面数(size >> PAGE_SHIFT就是申请的页面数)大于系统总页面数时,申请失败
*/
if (!size || (size >> PAGE_SHIFT) > totalram_pages)
goto fail;
/*
从__get_vm_area_node()函数调用的参数中我们看到了之前提到的VMALLOC_START和VMALLOC_END,这其实就是
vmalloc区的开始和结束,在这里,__get_vm_area_node这个函数简单来说就是在VMALLOC_START与VMALLOC_END之间
找个空闲的线性地址空间,然后返回内存区结构体vm_struct,而vm_struct中的addr代表了返回的内存区的线性地址的其实
*/
area = __get_vm_area_node(size, 1, VM_ALLOC | VM_UNINITIALIZED,
VMALLOC_START, VMALLOC_END, NUMA_NO_NODE, GFP_KERNEL | __GFP_HIGHMEM, __builtin_return_address(0));
if (!area)
goto fail;
/*
连续的线性地址空间得到了,接下来就需要得到这些线性地址对应的页框了,这将由__vmalloc_area_node函数来实现,关于本函数的分析,请看上面
*/
addr = __vmalloc_area_node(area, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL, NUMA_NO_NODE);
if (!addr)
return NULL;
/*
* In this function, newly allocated vm_struct has VM_UNINITIALIZED
* flag. It means that vm_struct is not fully initialized.
* Now, it is fully initialized, so remove this flag here.
*/
clear_vm_uninitialized_flag(area);
/*
* A ref_count = 2 is needed because vm_struct allocated in
* __get_vm_area_node() contains a reference to the virtual address of
* the vmalloc'ed block.
*/
kmemleak_alloc(addr, real_size, 2, GFP_KERNEL | __GFP_HIGHMEM);
return addr;
fail:
warn_alloc_failed(GFP_KERNEL | __GFP_HIGHMEM, 0,
"vmalloc: allocation failure: %lu bytes\n",
real_size);
return NULL;
}
上面的代码其实并不是真正的vmalloc函数的实现,真正的实现是函数嵌套函数的,我把这些函数调用进行展开得到的就是上面的代码。有关vmalloc函数实现的具体分析,请看上面的代码以及其中的中文注释。
这里说一个宏:PAGE_ALIGN,这是一个嵌套的宏,最后翻译过来就是:#define PAGE_ALIGN(addr) (addr + PAGE_SIZE-1)&PAGE_MASK,其中,PAGE_SIZE=1<<12,PAGE_MASK= ~(PAGE_SIZE-1),说人话,这个宏定义到底是做什么的呢?其实就是将size的值设置为4096的整数倍,举个例子,我们要申请1byte,也就是原来的size=1,执行size=PAGE_ALIGN(size)后,相当于size=(1+4096-1)&PAGE_MASK=4096&PAGE_MASK=4096,正好是4096的一倍,再举个例子,要申请4097字节,即size=4098,执行size=PAGE_ALIGN(size)后,size=(4098+4096-1)&PAGE_MASK=(8192+1)&PAGE_MASK=8192,也就是说,当我们申请的字节数不足4096字节(一个普通页面大小为4K,也就是4096字节)时,按照一个页面进行申请,超过一个页面但不足两个页面时按照两个页面进行申请。
与vmalloc函数相对的是vfree,其过程类似,这里不再展开。
常见问题:问题来自参考博客
1、用户空间(进程)是否有高端内存概念?
用户进程没有高端内存概念。只有在内核空间才存在高端内存。用户进程最多只可以访问3G物理内存,而内核进程可以访问所有物理内存。
2、64位内核中有高端内存吗?
目前现实中,64位Linux内核不存在高端内存,因为64位内核可以支持超过512GB内存。若机器安装的物理内存超过内核地址空间范围,就会存在高端内存。
3、用户进程能访问多少物理内存?内核代码能访问多少物理内存?
32位系统用户进程最大可以访问3GB,内核代码可以访问所有物理内存。
64位系统用户进程最大可以访问超过512GB,内核代码可以访问所有物理内存。
4、为什么不把所有的地址空间都分配给内核?
若把所有地址空间都给内存,那么用户进程怎么使用内存?怎么保证内核使用内存和用户进程不起冲突?
本人水平有限,如您发现文中有错误,请评论或者发邮箱:ieyaolulu@163.com,谢谢!
---------------------
作者:ibless
来源:优快云
原文:https://blog.youkuaiyun.com/ibless/article/details/81545359
版权声明:本文为博主原创文章,转载请附上博文链接!