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

介绍

本文介绍了 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

None0XX-32 bit32 bit32 bit1004KB4MB32 bitUp to 40 bitPAE1104KB2MB32 bitUp to 52 bitIA-32e1124KB2MB1GB48 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 内核中大页的实现与分析,第 1 部分

相关文章:

你感兴趣的文章:

标签云: