内容


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

Comments

介绍

本文介绍了 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. 大页结构图
图 1. 大页结构图
图 2. 大页使用的时序图
图 2. 大页使用的时序图
图 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() 分配页表
图 3. mmap() 系统调用 fault() 分配页表
图 3. mmap() 系统调用 fault() 分配页表

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

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

页表管理

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

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

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

图 5. 2MB 大页的页表管理
图 5. 2MB 大页的页表管理
图 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 调用流程
图 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。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Linux
ArticleID=931439
ArticleTitle=Linux 内核中大页的实现与分析,第 1 部分
publish-date=05272013