求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
     
   
分享到
Linux 内核中大页的实现与分析
 

发布于2013-6-08

 

简介:

大页对于系统的性能有着重要的影响。本文主要通过介绍大页中内存管理以及 hugetlbfs 来具体阐述 Linux 操作系统中大页的管理,并介绍了用户在具体应用中如何使用大页的。可以让您更深入理解大页以及如何使用它。

介绍:

本文介绍了 Linux 操作系统中大页的实现。分别从 memory 层、文件系统层、libhugetlbfs,以及用户如何使用大页等这几个方面进行了分析和介绍。让您更好的了解 大页在内核的实现机制以及用户使用方法。

大页主要是为了用户使用大量的内存时提供优化的方法。它通过硬件平台提供的支持,操作系统对内存操作进行优化,提高了系统的效率。本篇文章首先介绍了硬件平台对大页的支持,然后分析它在 Linux 内核中的实现,最后通过一个例子来了解用户如何使用这些大页的。 随着硬件的价格越来越低,用户需要访问更多的内存,系统有两种方法来适应内存的增加。一种方法就是保持页的大小不变而增加页表的级数,另一种方法就是页表的级别不变而增加页的大小。第一种方法,会容易出现性能的问题。页表级数的增加和小页就会增加访问内存的次数。而第二种方法可以减少访问内存的次数。相对于小页来说,系统的性能是比较高的。这就是为何有越来越多的方法支持大页。

大页的硬件支持

这里以 x86 架构为例,介绍硬件平台对大页的支持。下面表格显示了页的大小与物理地址长度的关系。控制寄存器 CR0、CR4 中的某些位决定了页的大小。此表格来自 Intel 64 IA and IA32 Architectures Software Developer ’s Manual。

Paging Mode PG Flag CR0 PAE Flag CR4 LME IA32_EFFER Page Size Linear Address Physical Address Width
None 0 x x - 32 bit 32 bit
32 bit 1 0 0 4KB 4MB 32 bit Up to 40 bit
PAE 1 1 0 4KB 2 MB 32 bit Up to 52 bit
IA-32e 1 1 2 4KB 2MB 1GB 48 bit Up to 52 bit

大页总体结构

大页的结构主要有内核代码中的 hugetlb.c, memory.c,hugtlbpage.c 和 fs/hugetlbfs/inode.c,还有用户空间提供的 libhugetlbfs。其中 hugetlb.c, memory.c 属于内存管理的部分,hugetlbpage.c 是跟具体的架构相关的页表的管理,fs/hugetlbfs/inode.c 是文件系统层,hugetlbfs 是一个伪文件系统,没有一个提供的设备文件,它提供了使用和管理大页的一种方式。最后,libhugetlbfs 为用户提供了管理大页的工具。这几部分的关系如下图所示:

图 1. 大页结构图

图 2. 大页使用的时序图

上面时序图展示了用户使用大页时,从用户空间调用到内核空间,最终分配页给用户的过程。

大页文件系统

大页文件系统作为一个伪文件系统,它通过 mmap 将文件映射到内存中,对内存操作。内存分配的页即是大页。在 hugetlbfs 文件系统中实现了 mmap 的回调函数。本文的代码都是基于 Linux 内核 -3.0.4 的版本。下面为 hugetlbfs 的文件操作的定义。

清单 1. 大页文件操作的函数

const struct file_operations hugetlbfs_file_operations = { 
.read= hugetlbfs_read,
.mmap= hugetlbfs_file_mmap,
.fsync= noop_fsync,
.get_unmapped_area= hugetlb_get_unmapped_area,
.llseek= default_llseek,
};

大页文件系统中仅仅提供了这几个回调函数,其中重要的一对函数为 hugetlbfs_file_mmap、hugetlb_get_unmapped_area。基本的文件读操作函数 hugetlbfs_read,这个函数有点儿类似 do_generic_mapping_read()。这里没有使用它是因为它假设了 PAGE_CACHE_SIZE 的大小。文件系统并没有提供文件写的操作,这个操作对于用户来说没有意义的。通常用户会通过 mmap 获得内存地址,通过内存地址对内存进行读写

文件与内存间的映射

在 Linux 内核中,文件系统 hugetlbfs 提供了 mmap 的回调函数,为映射的文件保留一个内存区域。通过调用函数 hugetlb_reserve_pages() 来实现。mmap 的回调函数定义如下。

清单 2. hugetlbfs 提供的 mmap 函数

static int hugetlbfs_file_mmap(struct file *file, struct vm_area_struct *vma) 
{
struct inode *inode = file->f_path.dentry->d_inode;
loff_t len, vma_len;
int ret;
struct hstate *h = hstate_file(file);

/*
* vma address alignment (but not the pgoff alignment) has
* already been checked by prepare_ 大页 _range. If you add
* any error returns here, do so after setting VM_HUGETLB, so
* is_vm_hugetlb_page tests below unmap_region go the right
* way when do_mmap_pgoff unwinds (may be important on powerpc
* and ia64).
*/
vma->vm_flags |= VM_HUGETLB | VM_RESERVED;
vma->vm_ops = &hugetlb_vm_ops;

if (vma->vm_pgoff & ~(huge_page_mask(h) >> PAGE_SHIFT))
return -EINVAL;

vma_len = (loff_t)(vma->vm_end - vma->vm_start);

mutex_lock(&inode->i_mutex);
file_accessed(file);

ret = -ENOMEM
len = vma_len + ((loff_t)vma->vm_pgoff << PAGE_SHIFT);

if (hugetlb_reserve_pages(inode,
vma->vm_pgoff >> huge_page_order(h),
len >> huge_page_shift(h), vma,
vma->vm_flags))
goto out;

ret = 0;
hugetlb_prefault_arch_hook(vma->vm_mm);
if (vma->vm_flags & VM_WRITE && inode->i_size < len)
inode->i_size = len;
out:
mutex_unlock(&inode->i_mutex);

return ret;
}

在上面的代码中,将 VMA 的 flags 设置为 VM_HUGETLB,并赋值 VMA 的操作为 hugetlb_vm_ops。另外 hugetlb_reserve_pages() 函数会保留 大页的内存的区域,并从 buddy 系统中分配所请求的大小的内存。

下面分析一下 hugetlb_reserve_pages() 函数,它的定义如下:

清单 3. hugetlb_reserve_pages in mm/hugetlb.c

				 
int hugetlb_reserve_pages(struct inode *inode,
long from, long to,
struct vm_area_struct *vma,
vm_flags_t vm_flags)
{
long ret, chg;
struct hstate *h = hstate_inode(inode);

/*
* Only apply 大页 reservation if asked. At fault time, an
* attempt will be made for VM_NORESERVE to allocate a page
* and filesystem quota without using reserves
*/
if (vm_flags & VM_NORESERVE)
return 0;

/*
* Shared mappings base their reservation on the number of pages that
* are already allocated on behalf of the file. Private mappings need
* to reserve the full area even if read-only as mprotect() may be
* called to make the mapping read-write. Assume !vma is a shm mapping
*/
if (!vma || vma->vm_flags & VM_MAYSHARE)
chg = region_chg(&inode->i_mapping->private_list, from, to);
else {
struct resv_map *resv_map = resv_map_alloc();
if (!resv_map)
return -ENOMEM;

chg = to - from;

set_vma_resv_map(vma, resv_map);
set_vma_resv_flags(vma, HPAGE_RESV_OWNER);
}

if (chg < 0)
return chg;

/* There must be enough filesystem quota for the mapping */
if (hugetlb_get_quota(inode->i_mapping, chg))
return -ENOSPC;

/*
* Check enough 大页 s are available for the reservation.
* Hand back the quota if there are not
*/
ret = hugetlb_acct_memory(h, chg);
if (ret < 0) {
hugetlb_put_quota(inode->i_mapping, chg);
return ret;
}

/*
* Account for the reservations made. Shared mappings record regions
* that have reservations as they are shared by multiple VMAs.
* When the last VMA disappears, the region map says how much
* the reservation was and the page cache tells how much of
* the reservation was consumed. Private mappings are per-VMA and
* only the consumed reservations are tracked. When the VMA
* disappears, the original reservation is the VMA size and the
* consumed reservations are stored in the map. Hence, nothing
* else has to be done for private mappings here
*/
if (!vma || vma->vm_flags & VM_MAYSHARE)
region_add(&inode->i_mapping->private_list, from, to);
return 0;
}

在上面的函数中,主要处理了为映射请求足够的内存。内存的映射分两种情况,一种是私有的映射,另一种是共享的映射。用户在映射的时候,可以指定 flag 为私有还是共享。那么下面分析一下对于这两种映射的不同的处理。

私有映射:内核在保留映射的内存区域时,将内存区域存放在 resv_map 中。这个结构体用来对一个保留的页表进行跟踪。共享映射:这些被多个进程共享的区域被存放在文件的 inode 的 page cache 中。也就是 inode->i_mapping->private_list。这些内存映射区域,会通过 hugetlb_acct_memory() 函数分配内存。下面介绍 memory 层定义的内存操作。

memory 层大页的管理

在 memory 层,定义了内存操作与文件关联的 vm_operation_struct 的函数,以及一系列的 VMA 的相关的操作。定义如下:

清单 4. 大页文件系统提供的 mmap 函数

const struct vm_operations_struct hugetlb_vm_ops = { 
.fault = hugetlb_vm_op_fault,
.open = hugetlb_vm_op_open,
.close = hugetlb_vm_op_close,
};

这个结构体中,定义了三个回调函数。我们下面分析一下这三个函数的用处。

Hugetlb_vm_op_fault(),这个函数中只是包含了一个 BUG() 方法,在 handle_mm_fault 中不会调用 hugetlb_vm_ops->fault()。在 handle_mm_fault 中,对于大页有特殊的处理。在大页中,定义了 hugetlb_fault() 函数,它会被 handle_mm_fault() 调用来处理大页的缺页异常。

下图描述了从系统调用到 hugetlb_fault 的调用。

图 3. mmap() 系统调用 fault() 分配页表

从上图中,可以看出,对于大页情况,会调用 hugetlb_fault()。对于小页情况,会调用它们的 vm_ops->fault()。另外,mmap() 系统调用时,页表最终会被分配好,不是在写数据时分配,这样提高了系统的效率。

那么大页的页表是如何管理的呢?下面介绍简单介绍一下页表管理。

页表管理

下面是以 x86_64 的系统为例,系统支持 48 位虚拟地址和 36 位的物理地址(PAE enabled),4KB 和 2M 的页表分别如下面的图。

图 4. 4KB 小页的页表管理

由上图可以看出,对于小页的管理,页表分为 4 级页表,每次需要访问一次页表也就是 4K 的内存,需要访问 4 次内存。

图 5. 2MB 大页的页表管理

由上图可以看出,PTE 不再使用。PMD 页表的 entry 直接指向页的物理地址。读一个 2M 的页,需要访问 3 次内存。

我们来比较一下小页和大页的访问内存的效率。如果使用小页的话,若访问一个 2M 的内存,那么至少需要放问 512 × 4 次。而如果使用大页的话,如果访问 2M 页表,需要访问内存次数为 3 次。使用小页的话,访问内存的次数是 2M 的内存的 512 倍多。可见使用大页提高的系统性能。

清单 5. 在 memory.c 中的 huge_pte_offset 定义

 pte_t *huge_pte_offset(struct mm_struct *mm, unsigned long addr) 
{
pgd_t *pgd;
pud_t *pud;
pmd_t *pmd = NULL;

pgd = pgd_offset(mm, addr);
if (pgd_present(*pgd)) {
pud = pud_offset(pgd, addr);
if (pud_present(*pud)) {
if (pud_large(*pud))
return (pte_t *)pud;
pmd = pmd_offset(pud, addr);
}
}
return (pte_t *) pmd;
}

从这个函数,可以看到,大页的 pte 是从普通页中的 pmd 获得。也就是上面我们介绍的大页的页表,pte 不再使用,pmd 的 entry 直接指向物理内存的地址。

hugetlb 模块

这个模块初始化大页,向内核的命令行提供了参数的设置,使得大页在内核启动阶段即可进行初始化页的大小。另外内核也提供了 sys 文件系统,用户可以在内核启动以后,通过写 sys 的文件来设置大页的参数。这个模块提供的参数有:nr_hugepages、nr_overcommit_hugepages、free_hugepages、surplus_hugepages、nr_hugepages_mempolicy。

下面介绍一下这几个参数。

nr_hugepages: 这个参数为系统所有的大页的总数。

nr_overcommit_hugepages: 这个参数的意思是,当用户需求更多的内存,这个内存大于 nr_hugepages 的数目,那么内核就会从 surplus 中获得内存来满足这个需求。

surplus_hugepages: 分配超过 nr_hugepages 大页的个数。

nr_hugepages_mempolicy: 设置 NUMA memory 的策略。例如下面的一行,设置某些 node 中 nr_hugepages 的数目。

numactl --interleave <node-list> echo 20 \ >/proc/sys/vm/nr_hugepages_mempolicy

系统调用 mmap

我们来看一下,mmap 如何调用到 hugetlbfs 的。

图 6. mmap 调用流程

上图为一个简单的流程,中间还有很多细节,这里不在详细的介绍了。hugetlb_file_mmap() 向内存的调用前面已经介绍过了。
下面是一个大页被用户使用的一个例子,这是内核代码中的例子,document/vm/hugepage-mmap.c

清单 6. 大页使用的例子

#include <stdlib.h> 
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <fcntl.h>

#define FILE_NAME "/mnt/ 大页 file"
#define LENGTH (256UL*1024*1024)
#define PROTECTION (PROT_READ | PROT_WRITE)

/* Only ia64 requires this */
#ifdef __ia64__
#define ADDR (void *)(0x8000000000000000UL)
#define FLAGS (MAP_SHARED | MAP_FIXED)
#else
#define ADDR (void *)(0x0UL)
#define FLAGS (MAP_SHARED)
#endif

static void check_bytes(char *addr)
{
printf("First hex is %x\n", *((unsigned int *)addr));
}

static void write_bytes(char *addr)
{
unsigned long i;

for (i = 0; i < LENGTH; i++)
*(addr + i) = (char)i;
}

static void read_bytes(char *addr)
{
unsigned long i;

check_bytes(addr);
for (i = 0; i < LENGTH; i++)
if (*(addr + i) != (char)i) {
printf("Mismatch at %lu\n", i);
break;
}
}

int main(void)
{
void *addr;
int fd;

fd = open(FILE_NAME, O_CREAT | O_RDWR, 0755);
if (fd < 0) {
perror("Open failed");
exit(1);
}

addr = mmap(ADDR, LENGTH, PROTECTION, FLAGS, fd, 0);
if (addr == MAP_FAILED) {
perror("mmap");
unlink(FILE_NAME);
exit(1);
}

printf("Returned address is %p\n", addr);
check_bytes(addr);
write_bytes(addr);
read_bytes(addr);

munmap(addr, LENGTH);
close(fd);
unlink(FILE_NAME);

return 0;
}

这里的映射是一个 SHARED 的映射。当写完数据以后,close() 和 unlink() 被调用,close 函数会将 page 的引用减小。unlink() 会帮助删除文件,并刷新页缓存,最后将内存释放到预存的大页的池中。

libhugetlbfs

这个为用户提供了上层操作系统的接口,而且也提供了一套工具。在 Fedora 或者 Redhat 中提供了 libhugetlbfs 以及 libhugetlbfs-utils 的 rpm 包。安装以后可以使用它提供的工具来分配和管理大页。例如:hugeadm --pool-pages-min 2M:512,这个命令创建了 512 个 2MB 大小的页,一共有 1GB 的内存。

总结

本文从内核到用户层来分析大页的管理,可以更多地了解到大页在内核中如何实现,以及对系统的性能的影响。本文主要介绍通过 hugetlbfs 使用和分配大页,但是这种方式还存在一些弊端。内核中又引入了另一种新的方法来管理和使用大页。那就是 THP(Transparent 大页)。我们将在第 2 部分来介绍一下 THP。

参考资料

1、关于大页分析:对大页进行了深入的分析。

2、HugeTLB - Large Page Support in the Linux Kernel:介绍内核对大页的支持

3、在 developerWorks Linux 专区寻找为 Linux 开发人员(包括 Linux 新手入门)准备的更多参考资料。

 
相关文章

中台产品面面观
如何在互联网产品中建立中台?
什么是产品生命周期管理?
产品设计之前,如何分析业务需求和用户痛点?
 
相关文档

产品经理是怎样炼成的
APP产品规划方法
产品经理培训文档
产品生命周期管理PLM
 
相关课程

产品经理与产品管理
卓越产品经理训练营
产品需求分析与管理
基于用户体验的产品设计
 
分享到
 
 


基于模型的整车电子电气架构设计
嵌入式设备上的 Linux 系统开发
Linux 的并发可管理工作队列
ARM嵌入式系统的问题总结分析
嵌入式系统设计与实例开发
WinCE6.0的EBOOT概要
更多...   


UML +RoseRealtime+嵌入式
C++嵌入式系统开发
嵌入式白盒测试
手机软件测试
嵌入式软件测试
嵌入式操作系统VxWorks


中国航空 嵌入式C高质量编程
使用EA和UML进行嵌入式系统分析设计
基于SysML和EA的嵌入式系统建模
上海汽车 嵌入式软件架构设计
北京 嵌入式C高质量编程
北京 高质高效嵌入式开发
Nagra linux内核与设备驱动原理
更多...