线性区的底层处理

在上一篇博文对控制内存处理所用的数据结构和状态信息有了基本理解以后,我们来看一组对线性区描述符进行操作的低层函数。这些函数应当被看作简化了do_map()和do_unmap()实现的辅助函数。这两个函数将在后面的相关博文中进行描述,它们分别扩大或者缩小进程的地址空间。这两个函数所处的层次比我们这里所考虑函数的层次要高一些,它们并不接受线性区描述符作为参数,而是使用一个线性地址区间的起始地址、长度和访问限权作为参数。

 

1 查找给定地址的最邻近区

 

find_vma()函数有两个参数:进程内存描述符的地址mm和线性地址addr:


struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr)
{
 struct vm_area_struct *vma = NULL;

 if (mm) {
  /* Check the cache first. */
  /* (Cache hit rate is typically around 35%.) */
  vma = mm->mmap_cache;
  if (!(vma && vma->vm_end > addr && vma->vm_start <= addr)) {
   struct rb_node * rb_node;

   rb_node = mm->mm_rb.rb_node;
   vma = NULL;

   while (rb_node) {
    struct vm_area_struct * vma_tmp;

    vma_tmp = rb_entry(rb_node,
      struct vm_area_struct, vm_rb);

    if (vma_tmp->vm_end > addr) {
     vma = vma_tmp;
     if (vma_tmp->vm_start <= addr)
      break;
     rb_node = rb_node->rb_left;
    } else
     rb_node = rb_node->rb_right;
   }
   if (vma)
    mm->mmap_cache = vma;
  }
 }
 return vma;
}


它查找所有的线性区,查找他们的vm_start和vm_end字段,如果出现addr位于其中,就返回第一个包含addr的线性区描述符的地址;如果没有这样的线性区存在,就返回一个NULL指针。注意,由find_vma()函数所选择的线性区并不一定要包含addr,也就是说vm_end可能会小于addr,因为addr可能位于任何线性区之外,这时候我们就要新建一个线性区对象,但不是在find_vma函数中创建,find_vma函数仅返回mm->mmap_cache,也就是当前的那个vma结构,这一点要注意了,后面的缺页异常会重点讨论的。

 

每个内存描述符包含一个mmap_cache字段,这个字段保存进程最后一次引用线性区的描述符地址。引进这个附加的字段是为了减少查找一个给定线性地址所在线性区而花费的时间。程序中引用地址的局部性使下面这种情况出现的可能性很大(代码注释里都说得很明白了,命中的几率高达35%):如果检查的最后一个线性地址属于某一给定的线性区,那么,下一个要检查的线性地址也属于这一个线性区。

 

因此,该函数一开始就检查由mmap_cache所指定的线性区是否包含addr(vma && vma->vm_end > addr && vma->vm_start <= addr)。如果是,就返回这个线性区描述符的指针:
    vma = mm->mmap_cache;
    if (vma && vma->vm_end > addr && vma->vm_start <= addr)
        return vma;

 

否则,必须扫描进程的线性区,红-黑树算法就用到了:
    rb_node = mm->mm_rb.rb_node;
    vma = NULL;
    while (rb_node) {
        vma_tmp = rb_entry(rb_node, struct vm_area_struct, vm_rb);
        if (vma_tmp->vm_end > addr) {
            vma = vma_tmp;
            if (vma_tmp->vm_start <= addr)
                break;
            rb_node = rb_node->rb_left;
        } else
            rb_node = rb_node->rb_right;
    }

 

函数使用的宏rb_entry,从指向红-黑树中一个节点的指针导出相应线性区描述符的地址:
#define rb_entry(ptr, type, member) container_of(ptr, type, member)
#define container_of(ptr, type, member) ({   /
        const typeof( ((type *)0)->member ) *__mptr = (ptr); /
        (type *)( (char *)__mptr - offsetof(type,member) );})
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

 

那么,根据上面的代码,翻译过来就是:
vma_tmp = ({ typeof( ((vm_area_struct *)0)->vm_rb ) *__mptr = (rb_node); /
        (type *)( (char *)__mptr - ((size_t) &((vm_area_struct *)0)->vm_rb) );})

你要是没有点C语言和数据结构的基本功,要读懂上面的代码还是有一定难度的,要不,我们来复习复习C语言知识?要分析上面的代码,最关键的是要晓得((vm_area_struct *)0)->vm_rb这句代码的意思,因为两行都用到了。这句代码是将0强制转换成vm_area_struct数据结构,就是说,假设系统中出现一个vm_area_struct数据结构,但它的地址为0。Linux经常用这种方式,这也是我们该从内核代码中学的基本C语言技巧之一,很重要,这样做的目的就是为了得到其该数据结构成员相对于顶部的偏移值。

 

好了,第一行typeof( ((vm_area_struct *)0)->vm_rb )得到vm_rb的类型,即指向顶地址0的那个vm_area_struct结构vm_rb字段的那个类型,即rb_node类型,把他用一个临时变量__mptr表示,其值为函数类的临时变量rb_node,所以*__mptr就是临时变量rb_node的真实地址值。那么第二行用这个*__mptr减去vm_rb成员相对于vm_area_struct顶部的偏移值,就得到了其宿主的vm_area_struct的地址的真实值,即最后得出对应的线性区vm_area_struct的地址。

 

结合while循环,我们就可以得知函数临时变量rb_node从根节点开始,反复测试vma_tmp->vm_end > addr,所以我们得出重要结论,红黑树的键值,也就是关键字不需要保存在红黑树节点数据结构里,其就是其宿主vm_area_struct数据结构的vm_end成员。

 

函数find_vma_prev()与find_vma()类似,不同的是它把函数选中的前一个线性区描述符的指针赋给附加参数的结果参数pprev:
struct vm_area_struct *
find_vma_prev(struct mm_struct *mm, unsigned long addr,
   struct vm_area_struct **pprev)
{
 struct vm_area_struct *vma = NULL, *prev = NULL;
 struct rb_node * rb_node;
 if (!mm)
  goto out;

 /* Guard against addr being lower than the first VMA */
 vma = mm->mmap;

 /* Go through the RB tree quickly. */
 rb_node = mm->mm_rb.rb_node;

 while (rb_node) {
  struct vm_area_struct *vma_tmp;
  vma_tmp = rb_entry(rb_node, struct vm_area_struct, vm_rb);

  if (addr < vma_tmp->vm_end) {
   rb_node = rb_node->rb_left;
  } else {
   prev = vma_tmp;
   if (!prev->vm_next || (addr < prev->vm_next->vm_end))
    break;
   rb_node = rb_node->rb_right;
  }
 }

out:
 *pprev = prev;
 return prev ? prev->vm_next : vma;
}

 

最后,函数find_vma_prepare()确定新叶子节点在与给定线性地址对应的红-黑树中的位置,并返回前一个线性区的地址和要插入的叶子节点的父节点的地址:
static struct vm_area_struct *
find_vma_prepare(struct mm_struct *mm, unsigned long addr,
  struct vm_area_struct **pprev, struct rb_node ***rb_link,
  struct rb_node ** rb_parent)
{
 struct vm_area_struct * vma;
 struct rb_node ** __rb_link, * __rb_parent, * rb_prev;

 __rb_link = &mm->mm_rb.rb_node;
 rb_prev = __rb_parent = NULL;
 vma = NULL;

 while (*__rb_link) {
  struct vm_area_struct *vma_tmp;

  __rb_parent = *__rb_link;
  vma_tmp = rb_entry(__rb_parent, struct vm_area_struct, vm_rb);

  if (vma_tmp->vm_end > addr) {
   vma = vma_tmp;
   if (vma_tmp->vm_start <= addr)
    return vma;
   __rb_link = &__rb_parent->rb_left;
  } else {
   rb_prev = __rb_parent;
   __rb_link = &__rb_parent->rb_right;
  }
 }

 *pprev = NULL;
 if (rb_prev)
  *pprev = rb_entry(rb_prev, struct vm_area_struct, vm_rb);
 *rb_link = __rb_link;
 *rb_parent = __rb_parent;
 return vma;
}

 

2 查找一个与给定的地址区间相重叠的线性区

 

find_vma_intersection()函数查找与给定的线性地址区间相重叠的第一个线性区。mm参数指向进程的内存描述符,而线性地址start_addr和end_addr指定这个区间。


static inline struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr)
{
 struct vm_area_struct * vma = find_vma(mm,start_addr);

 if (vma && end_addr <= vma->vm_start)
  vma = NULL;
 return vma;
}

 

如果没有这样的线性区存在,函数就返回一个NULL指针。准确地说,如果find_vma()函数回一个有效的地址,但是所找到的线性区是从这个线性地址区间的末尾开始的,vma就被置为NULL。

 

3 查找一个空闲的地址区间

 

static inline unsigned long get_unmapped_area(struct file * file, unsigned long addr,
  unsigned long len, unsigned long pgoff, unsigned long flags)

 

get_unmapped_area()搜查进程的地址空间以找到一个可以使用的线性地址区间(注意,这里不是返回vm_area_struct结构了,因为这段区间可能跨越多个该结构)。len参数指定区间的长度,而非空的addr参数表示必须从哪个地址开始进行查找。如果查找成功,函数返回这个新区间的起始地址;否则返回错误码-ENOMEM。

 

如果参数addr不等于NULL,函数就检查所指定的地址是否在用户态空间(if (addr > TASK_SIZE - len))并与页边界对齐(if (addr & ~PAGE_MASK))。接下来,函数根据线性地址区间是否应该用于文件内存映射或匿名内存映射,调用两个方法(get_unmapped_area文件操作file->f_op->get_unmapped_area和内存描述符的get_unmapped_area方法current->mm->get_unmapped_area(file, addr, len, pgoff, flags))中的一个。在前一种情况下,函数执行get_unmapped_area文件操作。

 

第二种情况下,函数执行内存描述符的get_unmapped_area方法。根据进程的线性区类型,由函数arch_get_unmapped_area()或arch_get_unmapped_area_topdown()实现get_unmapped_area方法。通过系统调用mmap(),每个进程都可能获得两种不同形式的线性区:一种从线性地址0x40000000(1G)开始并向高端地址增长,即所谓的“堆”;另一种正好从用户态堆栈开始并向低端地址增长,即所谓的“栈”。前者就调用arch_get_unmapped_area函数,后者就会调用arch_get_unmapped_area_topdown函数,后面博文会详细讨论。

 

现在我们讨论函数arch_get_unmapped_area(),在分配从低端地址向高端地址移动的线性区时使用这个函数。它本质上等价于下面的代码片段:
    if (len > TASK_SIZE)
        return -ENOMEM;
    addr = (addr + 0xfff) & 0xfffff000;
    if (addr && addr + len <= TASK_SIZE) {
        vma = find_vma(current->mm, addr);
        if (!vma || addr + len <= vma->vm_start)
            return addr;
    }
    start_addr = addr = mm->free_area_cache;
    for (vma = find_vma(current->mm, addr); ; vma = vma->vm_next) {
        if (addr + len > TASK_SIZE) {
            if (start_addr == (TASK_SIZE/3+0xfff)&0xfffff000)
                return -ENOMEM;
            start_addr = addr = (TASK_SIZE/3+0xfff)&0xfffff000;
            vma = find_vma(current->mm, addr);
        }
        if (!vma || addr + len <= vma->vm_start) {
            mm->free_area_cache = addr + len;
            return addr;
        }
        addr = vma->vm_end;
    }

 

函数首先检查区间的长度是否在用户态下线性地址区间的限长TASK_SIZE(通常为3GB)之内。如果addr不为0,函数就试图从addr开始分配区间。为了安全起见,函数把addr的值调整为4KB的倍数(addr = (addr + 0xfff) & 0xfffff000)。

 

如果addr等于0或前面的搜索失败,函数arch_get_unmapped_area()就扫描用户态线性地址空间以查找一个可以包含新区的足够大的线性地址范围,但任何已有的线性区都不包括这个地址范围。为了提高搜索的速度,让搜索从最近被分配的线性区后面的线性地址开始(vma = find_vma(current->mm, addr))。

 

把内存描述符的字段mm->free_area_cache初始化为用户态线性地址空间的三分之一(通常是1GB,start_addr = addr = (TASK_SIZE/3+0xfff)&0xfffff000),并在以后创建新线性区时对它进行更新。如果函数找不到一个合适的线性地址范围,就从用户态线性地址空间的三分之一的开始处重新开始搜索:其实,用户态线性地址空间的三分之一是为有预定义起始线性地址的线性区(典型的是可执行文件的正文段、数据段和bss段)而保留的。

 

函数调用find_vma()以确定搜索起点之后第一个线性区终点的位置。可能出现三种情况:
(1)如果所请求的区间大于正待扫描的线性地址空间部分(addr+len > TASK-SIZE),函数就从用户态地址空间的三分之一处重新开始搜索,如果已经完成第二次搜索,就返回-ENOMEM(没有足够的线性地址空间来满足这个请求)。
(2)刚刚扫描过的线性区后面的空闲区没有足够的大小(vma != NULL && vma->vm_start < addr+len)。此时,继续考虑下一个线性区。
(3)如果以上两种情况都没有发生,则找到一个足够大的空闲区,此时,函数返回addr。

 

4 向内存描述符链表中插入一个线性区

 

int insert_vm_struct(struct mm_struct * mm, struct vm_area_struct * vma)
{
 struct vm_area_struct * __vma, * prev;
 struct rb_node ** rb_link, * rb_parent;

 if (!vma->vm_file) {
  BUG_ON(vma->anon_vma);
  vma->vm_pgoff = vma->vm_start >> PAGE_SHIFT;
 }
 __vma = find_vma_prepare(mm,vma->vm_start,&prev,&rb_link,&rb_parent);
 if (__vma && __vma->vm_start < vma->vm_end)
  return -ENOMEM;
 vma_link(mm, vma, prev, rb_link, rb_parent);
 return 0;

 

insert_vm_struct()函数在线性区对象链表和内存描述符的红-黑树中插入一个vm_area_struct结构。这个函数使用两个参数:mm指定进程内存描述符的地址,vmp指定要插入的vm_area_struct对象的地址。线性区对象的vm_start和vm_end字段必定已经初始化过。

该函数调用find_vma_prepare()在红-黑树mm->mm_rb中查找是否有一个__vma跟他有重复的线性地址,如果有,就返回-ENOMEM:


static struct vm_area_struct *
find_vma_prepare(struct mm_struct *mm, unsigned long addr,
  struct vm_area_struct **pprev, struct rb_node ***rb_link,
  struct rb_node ** rb_parent)
{
 struct vm_area_struct * vma;
 struct rb_node ** __rb_link, * __rb_parent, * rb_prev;

 __rb_link = &mm->mm_rb.rb_node;
 rb_prev = __rb_parent = NULL;    /* rb_prev内部变量表示vma的前一个vm_area_struct结构在树中的位置 */
 vma = NULL;

 while (*__rb_link) {
  struct vm_area_struct *vma_tmp;

  __rb_parent = *__rb_link;
  vma_tmp = rb_entry(__rb_parent, struct vm_area_struct, vm_rb);

  if (vma_tmp->vm_end > addr) {
   vma = vma_tmp;
   if (vma_tmp->vm_start <= addr)
    return vma;
   __rb_link = &__rb_parent->rb_left;
  } else {
   rb_prev = __rb_parent;
   __rb_link = &__rb_parent->rb_right;
  }
 }

 *pprev = NULL;
 if (rb_prev)
  *pprev = rb_entry(rb_prev, struct vm_area_struct, vm_rb);
 *rb_link = __rb_link;
 *rb_parent = __rb_parent;
 return vma;
}

 

然后,insert_vm_struct()又调用vma_link()函数:
static void vma_link(struct mm_struct *mm, struct vm_area_struct *vma,
   struct vm_area_struct *prev, struct rb_node **rb_link,
   struct rb_node *rb_parent)
{
 struct address_space *mapping = NULL;

 if (vma->vm_file)
  mapping = vma->vm_file->f_mapping;

 if (mapping) {
  spin_lock(&mapping->i_mmap_lock);
  vma->vm_truncate_count = mapping->truncate_count;
 }
 anon_vma_lock(vma);

 __vma_link(mm, vma, prev, rb_link, rb_parent);
 __vma_link_file(vma);

 anon_vma_unlock(vma);
 if (mapping)
  spin_unlock(&mapping->i_mmap_lock);

 mm->map_count++;
 validate_mm(mm);
}
static void
__vma_link(struct mm_struct *mm, struct vm_area_struct *vma,
 struct vm_area_struct *prev, struct rb_node **rb_link,
 struct rb_node *rb_parent)
{
 __vma_link_list(mm, vma, prev, rb_parent);
 __vma_link_rb(mm, vma, rb_link, rb_parent);
 __anon_vma_link(vma);
}

 

vma_link依次执行以下操作:

 

1. 在mm->mmap所指向的链表中插入线性区 —— __vma_link_list(mm, vma, prev, rb_parent):
static inline void
__vma_link_list(struct mm_struct *mm, struct vm_area_struct *vma,
  struct vm_area_struct *prev, struct rb_node *rb_parent)
{
 if (vma->vm_flags & VM_EXEC)
  arch_add_exec_range(mm, vma->vm_end);
 if (prev) {
  vma->vm_next = prev->vm_next;
  prev->vm_next = vma;
 } else {
  mm->mmap = vma;
  if (rb_parent)
   vma->vm_next = rb_entry(rb_parent,
     struct vm_area_struct, vm_rb);
  else
   vma->vm_next = NULL;
 }
}

 

2. 在红-黑树mm->mm_rb中插入线性区:
void __vma_link_rb(struct mm_struct *mm, struct vm_area_struct *vma,
  struct rb_node **rb_link, struct rb_node *rb_parent)
{
 rb_link_node(&vma->vm_rb, rb_parent, rb_link);
 rb_insert_color(&vma->vm_rb, &mm->mm_rb);
}
static inline void rb_link_node(struct rb_node * node, struct rb_node * parent,
    struct rb_node ** rb_link)
{
 node->rb_parent_color = (unsigned long )parent;
 node->rb_left = node->rb_right = NULL;

 *rb_link = node;
}
void rb_insert_color(struct rb_node *node, struct rb_root *root)
{
 struct rb_node *parent, *gparent;

 while ((parent = rb_parent(node)) && rb_is_red(parent))
 {
  gparent = rb_parent(parent);

  if (parent == gparent->rb_left)
  {
   {
    register struct rb_node *uncle = gparent->rb_right;
    if (uncle && rb_is_red(uncle))
    {
     rb_set_black(uncle);
     rb_set_black(parent);
     rb_set_red(gparent);
     node = gparent;
     continue;
    }
   }

   if (parent->rb_right == node)
   {
    register struct rb_node *tmp;
    __rb_rotate_left(parent, root);
    tmp = parent;
    parent = node;
    node = tmp;
   }

   rb_set_black(parent);
   rb_set_red(gparent);
   __rb_rotate_right(gparent, root);
  } else {
   {
    register struct rb_node *uncle = gparent->rb_left;
    if (uncle && rb_is_red(uncle))
    {
     rb_set_black(uncle);
     rb_set_black(parent);
     rb_set_red(gparent);
     node = gparent;
     continue;
    }
   }

   if (parent->rb_left == node)
   {
    register struct rb_node *tmp;
    __rb_rotate_right(parent, root);
    tmp = parent;
    parent = node;
    node = tmp;
   }

   rb_set_black(parent);
   rb_set_red(gparent);
   __rb_rotate_left(gparent, root);
  }
 }

 rb_set_black(root->rb_node);
}

 

3. 如果线性区是匿名的,就把它插入以相应的anon_vma数据结构作为头节点的链表中(__anon_vma_link(vma))。

 

4. 递增mm->map_count计数器(mm->map_count++;)。

 

如果线性区包含一个内存映射文件,则vma_link()函数执行在回收页框相关博文描述的其他任务。

 

__vma_unlink()函数接收的参数为一个内存描述符地址mm和两个线性区对象地址vma和prev。两个线性区都应当属于mm,prev应当在线性区的排序中位于vma之前。该函数从内存描述符链表和红-黑树中删除vma,如果mm->mmap_cache(存放刚被引用的线性区)字段指向刚被删除的线性区,则还要对mm->mmap_cache进行更新。具体代码我们就不去分析了。

 

你可能感兴趣的:(线性区的底层处理)