介绍
本文介绍了 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 2MB | 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. 大页结构图
![图 1. 大页结构图](https://i-blog.csdnimg.cn/blog_migrate/a49e298529fd4c32d9a33bed1307886c.png)
图 2. 大页使用的时序图
![图 2. 大页使用的时序图](https://i-blog.csdnimg.cn/blog_migrate/602f94fd5d4ac9f11d0f08f330d2ced4.png)
上面时序图展示了用户使用大页时,从用户空间调用到内核空间,最终分配页给用户的过程。
大页文件系统
大页文件系统作为一个伪文件系统,它通过 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() 分配页表](https://i-blog.csdnimg.cn/blog_migrate/6e958595ba1e4ab7537b6f84cb961b25.png)
从上图中,可以看出,对于大页情况,会调用 hugetlb_fault()。对于小页情况,会调用它们的 vm_ops->fault()。另外,mmap() 系统调用时,页表最终会被分配好,不是在写数据时分配,这样提高了系统的效率。
那么大页的页表是如何管理的呢?下面介绍简单介绍一下页表管理。
页表管理
下面是以 x86_64 的系统为例,系统支持 48 位虚拟地址和 36 位的物理地址(PAE enabled),4KB 和 2M 的页表分别如下面的图。
图 4. 4KB 小页的页表管理
![图 4. 4KB 小页的页表管理](https://i-blog.csdnimg.cn/blog_migrate/3c7a7d47ecbdb1bdec6601a437745ecc.png)
由上图可以看出,对于小页的管理,页表分为 4 级页表,每次需要访问一次页表也就是 4K 的内存,需要访问 4 次内存。
图 5. 2MB 大页的页表管理
![图 5. 2MB 大页的页表管理](https://i-blog.csdnimg.cn/blog_migrate/6daf522f151f64048ebba665afaf4b9e.png)
由上图可以看出,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 调用流程](https://i-blog.csdnimg.cn/blog_migrate/ecb5d1b5be1952120b69d738d0c67659.png)
上图为一个简单的流程,中间还有很多细节,这里不在详细的介绍了。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 用户交流。