简介:
大页对于系统的性能有着重要的影响。本文主要通过介绍大页中内存管理以及 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 新手入门)准备的更多参考资料。
|