17..内核中不连续页的分配(vmalloc,__get_vm_area_node和__vunmap)

根据上文的讲述,我们知道物理上连续的映射对内核是最好的,但并不总能成功地使用。在分配一大块内存时,可能竭尽全力也无法找到连续的内存块。在用户空间中这不是问题,因为普通进程设计为使用处理器的分页机制,当然这会降低速度并占用TLB。在内核中也可以使用同样的技术。
如图3-37所示,在IA-32系统中,紧随直接映射的前892 MiB物理内存,在插入的8 MiB安全隙之后,是一个用于管理不连续内存的区域。这一段具有线性地址空间的所有性质。分配到其中的页可能位于物理内存中的任何地方。通过修改负责该区域的内核页表,即可做到这一点。相关的描述请见6.内核地址空间的划分

 

 

每个vmalloc分配的子区域与其他vmalloc子区域通过一个内存页分隔。类似于直接映射和vmalloc区域之间的边界,不同vmalloc子区域之间的分隔也是为防止不正确的内存访问操作。因为分隔是在虚拟地址空间中建立的,不会浪费宝贵的物理内存页。
1. 用vmalloc分配内存
vmalloc是一个接口函数,内核代码使用它来分配在虚拟内存中连续但在物理内存中不一定连续的内存。
<vmalloc.h>
void *vmalloc(unsigned long size);
 
因为用于vmalloc的内存页总是必须映射在内核地址空间中,因此使用ZONE_HIGHMEM内存域的页要优于其他内存域。这使得内核可以节省更宝贵的较低端内存域,而又不会带来额外的坏处。因此,vmalloc(连同其他映射函数在后续讨论)是内核出于自身的目的(并非因为用户空间应用程序)使用高端内存页的少数情形之一。
 
 数据结构
内核在管理虚拟内存中的vmalloc区域时,内核必须跟踪哪些子区域被使用、哪些是空闲的。为此定义了一个数据结构,将所有使用的部分保存在一个链表中。
<vmalloc.h>
struct vm_struct {
  struct vm_struct* next;
  void*     addr;
  unsigned long size;
  unsigned long flags;
  struct page** pages;
  unsigned int nr_pages;
  unsigned long phys_addr;
};

 

对于每个用vmalloc分配的子区域,都对应于内核内存中的一个该结构实例。该结构各个成员的语义如下。
 addr定义了分配的子区域在虚拟地址空间中的起始地址。size表示该子区域的长度。可以根据该信息来勾画出vmalloc区域的完整分配方案。
 flags存储了与该内存区关联的标志集合,这几乎是不可避免的。它只用于指定内存区类型,当前可选值有以下3个。
   VM_ALLOC指定由vmalloc产生的子区域。
   VM_MAP用于表示将现存pages集合映射到连续的虚拟地址空间中。
   VM_IOREMAP表示将几乎随机的物理内存区域映射到vmalloc区域中。这是一个特定于体系结构的操作。
 pages是一个指针,指向page指针的数组。每个数组成员都表示一个映射到虚拟地址空间中的物理内存页的page实例。
 nr_pages指定pages中数组项的数目,即涉及的内存页数目。
 phys_addr仅当用ioremap映射了由物理地址描述的物理内存区域时才需要。该信息保存在phys_addr中。
 next使得内核可以将vmalloc区域中的所有子区域保存在一个单链表上。
图3-38给出了该结构使用方式的一个实例。其中依次映射了3个(假想的)物理内存页,在物理内存中的位置分别是1 023、725和7 311。在虚拟的vmalloc区域中,内核将其看作起始于VMALLOC_START+ 100的一个连续内存区

 

 

 创建vm_area
在创建一个新的虚拟内存区之前,必须找到一个适当的位置。vm_area实例组成的一个链表,管理着vmalloc区域中已经建立的各个子区域。定义在mm/vmalloc的全局变量vmlist是表头。
mm/vmalloc.c
struct vm_struct *vmlist;

 

内核在mm/vmalloc中提供了辅助函数get_vm_area。它充当__get_vm_area的前端,负责参数准备工作。类似地,后一个函数是负责实际工作的__get_vm_area_node函数的前端。根据子区域的长度信息,该函数试图在虚拟的vmalloc空间中找到一个适当的位置。
由于各个vmalloc子区域之间需要插入1页(警戒页)作为安全隙,内核首先适当提高需要分配的内存长度。
mm/vmalloc.c
struct vm_struct *__get_vm_area_node(unsigned long size, unsigned long flags,unsigned long start, unsigned long end, int node)
{
  struct vm_struct **p, *tmp, *area;
...
  size = PAGE_ALIGN(size);
....
  /*
  * 总是分配一个警戒页。
  */
  size += PAGE_SIZE;
...

 

start和end参数分别由调用者设置为VMALLOC_START和VMALLOC_END。
接下来循环遍历vmlist的所有表元素,直至找到一个适当的项。
mm/vmalloc.c
  for (p = &vmlist; (tmp = *p) != NULL ;p = &tmp->next) {
    if ((unsigned long)tmp->addr < addr) {
      if((unsigned long)tmp->addr + tmp->size >= addr)
        addr = ALIGN(tmp->size +(unsigned long)tmp->addr, align);
    continue;
  }
  if ((size + addr) < addr)
    goto out;
  if (size + addr <= (unsigned long)tmp->addr)
    goto found;
  addr = ALIGN(tmp->size + (unsigned long)tmp->addr, align);
  if (addr > end -size)
    goto out;
}
...

 

如果size+addr不大于当前检查区域的起始地址(保存在tmp->addr),那么内核就找到了一个合适的位置。接下来用适当的值初始化新的链表元素,并添加到vmlist链表。
mm/vmalloc.c
found:
  area->next = *p;
  *p = area;//重点关注
  area->flags = flags;
  area->addr = (void *)addr;
  area->size = size;
  area->pages = NULL;
  area->nr_pages = 0;
  area->phys_addr = 0;
  return area;
...
}

 

如果没有找到适当的内存区,则返回NULL指针表示失败。
remove_vm_area函数将一个现存的子区域从vmalloc地址空间删除。
<vmalloc.h>
struct vm_struct *remove_vm_area(void *addr);
该函数需要待删除子区域的虚拟起始地址作为一个参数。为找到该子区域,内核必须依次扫描vmlist的链表元素,直至找到匹配者。接下来将对应的vm_area实例从链表删除。
 分配内存区
 vmalloc发起对不连续的内存区的分配操作。该函数只是一个前端,为__vmalloc提供适当的参数,后者直接调用__vmalloc_node。相关的代码流程图见图3-39。

 

 

实现分为3部分。首先,get_vm_area在vmalloc地址空间中找到一个适当的区域。接下来从物理内存分配各个页,最后将这些页连续地映射到vmalloc区域中,分配虚拟内存的工作就完成了。这里不给出完整的代码了,其中包含了无趣的安全检查。①我们比较感兴趣的是物理内存区域的分配(忽略没有足够物理内存页可用的可能性)。
void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask,pgprot_t prot, int node)
{
...
  for (i = 0; i < area->nr_pages; i++) {
    if (node < 0)
      area->pages[i] = alloc_page(gfp_mask);
    else
      area->pages[i] = alloc_pages_node(node, gfp_mask, 0);//这个0说明是一页一页分配
}
...
  if (map_vm_area(area, prot, &pages))
    goto fail;
  return area->addr;
...
}

 

分配的页从相关结点的伙伴系统移除。在调用时,vmalloc将gfp_mask设置为GFP_KERNEL |__GFP_HIGHMEM,内核通过该参数指示内存管理子系统尽可能从ZONE_HIGHMEM内存域分配页帧。理由已经在上文给出:低端内存域的页帧更为宝贵,因此不应该浪费到vmalloc的分配中,在此使用高端内存域的页帧完全可以满足要求。

 

 

3. 释放内存
有两个函数用于向内核释放内存,vfree用于释放vmalloc和vmalloc_32分配的区域,而vunmap用于释放由vmap或ioremap创建的映射。这两个函数都会归结到__vunmap。
mm/vmalloc.c
void __vunmap(void *addr, int deallocate_pages)

 

addr表示要释放的区域的起始地址,deallocate_pages指定了是否将与该区域相关的物理内存页返回给伙伴系统。vfree将后一个参数设置为1,而vunmap设置为0,因为在这种情况下只删除映射,而不将相关的物理内存页返回给伙伴系统。图3-40给出了__vunmap的代码流程图。

 

 

不必明确给出需要释放的区域长度,长度可以从vmlist中的信息导出。因此__vunmap的第一个任务是在__remove_vm_area(由remove_vm_area在完成锁定之后调用)中扫描该链表,以找到相关项。
unmap_vm_area使用找到的vm_area实例,从页表删除不再需要的项。与分配内存时类似,该函数需要操作各级页表,但这一次需要删除涉及的项。它还会更新CPU高速缓存。
如果__vunmap的参数deallocate_pages设置为1(在vfree中),内核会遍历area->pages的所有元素,即指向所涉及的物理内存页的page实例的指针。然后对每一项调用__free_page,将页释放到伙伴系统。
最后,必须释放用于管理该内存区的内核数据结构
posted @ 2022-03-21 20:32  while(true);;  阅读(154)  评论(0编辑  收藏  举报