您的位置:首页 > 运维架构 > Linux

Linux 内核中大页的实现与分析,第 1 部分

2015-01-23 09:44 609 查看


介绍

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

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

回页首


大页的硬件支持

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

4MB
32 bitUp to 40 bit
PAE1104KB

2MB
32 bitUp to 52 bit
IA-32e1124KB

2MB

1GB
48 bitUp 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() 函数,它的定义如下:

清单 4. 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 的相关的操作。定义如下:

清单 3. 大页文件系统提供的 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 倍多。可见使用大页提高的系统性能。

清单 4. 在 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

清单 5. 大页使用的例子


#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。


参考资料


学习

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

Linux 大页面使用与实现简介:本文对
Linux 大页面机制的使用和实现进行了简要的介绍和分析。

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

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


讨论

加入 developerWorks 中文社区。查看开发人员推动的博客、论坛、组和维基,并与其他
developerWorks 用户交流。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: