申请连续高端线性而物理内存非连续函数分析之vmalloc()

 这个函数的大致过程我在这里讲述一下,首先,他要在VMALLOC_START~VMALLOC_END之间的高端线性地址空间中申请要求大小的按页对齐的地址空间,什么事高端线性地址空间?这个你到我前面的文章中看看--“linux内存管理之基础篇”里面有提到什么是高端线性地址空间的。然后,我们调用伙伴系统机制中的1页内存分配函数alloc_page()在高端物理内存中逐页为申请到的高端线性地址空间中的每一页地址空间申请一页物理内存。什么事高端物理内存,这个在我说的那篇文章中会有讲解的。最后为这些一一对应的高端线性地址空间和高端物理内存建立页表就可以了。链表建立后,我们就可以任意访问这些高端线性地址空间了。如果大家实在对高端线性地址空间和高端物理内存不理解的话,可以给我留言,我会尽量解答的。

  我们先来分析一下vmalloc()函数吧。

  void *vmalloc(unsigned long size)
{
       return __vmalloc(size, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL);
}

  void *__vmalloc(unsigned long size, int gfp_mask, pgprot_t prot)
{
   struct vm_struct *area;
   struct page **pages;
   unsigned int nr_pages, array_size, i;

   size = PAGE_ALIGN(size);//使申请的内存大小按页对齐。
   if (!size || (size >> PAGE_SHIFT) > num_physpages)
      return NULL;//如果申请大小为0或者申请大小按页数来说大于系统物理内存总页数,系统崩溃。

   area = get_vm_area(size, VM_ALLOC);//这个函数主要是先调用函数kmalloc(),为vm_struct结构体申请内存空间,很明显最终还是会调用我们在slab分配器为之分配,最后返回内存对象的指针给area。在有了内存空间来存储这个vm_struct结构体后,函数会上锁,同时遍历一个全局链表struct vm_struct *vmlist。我来说下这个链表是这么来的,vmlist所指向的单元里面存放着第一个已经映射好的高端线性地址空间对应的struct vm_struct结构的指针,接着这个结构体的next成员所指单元里面存放着下一个映射好的高端线性地址空间对应的vm_struct结构体的指针,这样一直到最后没有映射好的线性地址位置。他们排列的顺序就是按申请到的地址开始由低到高一直往下排列。这样遍历就是要当发现在已经映射好的空间之间如果还存在空隙,足以容纳下这次要申请映射的线性空间时,我们就使vm_struct->addr=紧接上一个的空间结束地址,vm_struct->next指向后面的那个空间,前者空间的next指向这次申请到的vm_struct结构本身。这样链表就接上了,vm_struct其他的成员赋值可以自己分析代码。vm_struct->flags=VM_ALLOC这表示这个结构用于vmalloc()函数。其实这里已经完成了申请高端线性地址空间,其实就是简单的链表操作。
  if (!area)
     return NULL;

  nr_pages = size >> PAGE_SHIFT;//nr_pages是vm_struct结构体统计内存块的页数。
  array_size = (nr_pages * sizeof(struct page *));//每个内存页需要一个struct page结构体,我们这里统计这个一共需要多大的空间来存储这些结构体。

  area->nr_pages = nr_pages;
  area->pages = pages = kmalloc(array_size, (gfp_mask & ~__GFP_HIGHMEM));//申请可以存放这些struct page结构体的空间,area->pages是指向这个空间的起始地址。要求在低端内存中申请。
  if (!area->pages) {//如果申请空间失败
     remove_vm_area(area->addr);//我们先释放掉我们刚才在高端地址空间申请的空间,其实就是从全局链表上断开。
     kfree(area);//释放刚才为vm_struct结构申请的空间。
     return NULL;
  }
  memset(area->pages, 0, array_size);//把struct page存放的空间清零。

  for (i = 0; i < area->nr_pages; i++) {//为高端线性地址空间的每页线性地址空间,在高端内存中申请内存页。
        area->pages[i] = alloc_page(gfp_mask);//alloc_page()函数在伙伴系统中申请一页的函数,返回这个内存页的struct page结构指针。关于这个函数的理解,请你看前面的文章,它的来龙去脉会有很详细的解答,如果网友们发现哪里有不懂的,请给我留言,最好在对应文章中留言,我很乐意为你们解答。
  if (unlikely(!area->pages[i])) {//如果发现没有在伙伴系统中申请到内存页,就会跳转到fail处。
      area->nr_pages = i;//这里一直记录着内存块申请到的内存页的数量。
      goto fail;
  }

}
 
  if (map_vm_area(area, prot, &pages))//如果成功完成了申请高端线性地址空间和物理内存的申请后,我们就要为虚拟和物理空间之间建立映射,其实就是建立页表。
      goto fail;
  return area->addr;//返回高端线性空间的起始地址。

fail:
  vfree(area->addr);//如果失败就释放高端线性空间的起始地址空间。
  return NULL;
}

  int map_vm_area(struct vm_struct *area, pgprot_t prot, struct page ***pages)
{
   unsigned long address = (unsigned long) area->addr;
   unsigned long end = address + (area->size-PAGE_SIZE);//area->size-PAGE_SIZE的原因是在先前的那个函数中我们调用了get_vm_area()函数,看里面会发现size会被多加了PAGE_SIZE大小。

   pgd_t *dir;//pgd_t表示一个页描述符,大小为8个字节。这个描述符是一级页表描述(就是一级描述符的条目),我们在原来的文章中说过,一级页表描述系统的内存情况,它的每个条目对应1mb的映射关系。一般一个条目只需4个字节描述就足够了,但是linux用了pgd_t 8字节来管理一级页表的条目,这样linux系统一次就要在一级页表中取出两个条目来。后面代码中我会做更详细的介绍的。

   int err = 0;

   dir = pgd_offset_k(address);//我们通过这个函数可以发现address是属于哪个段的,这样我们就可以知道这个段在一级页表中的偏移位置了,我们通过全局变量init_mm可以求到address对应的一级描述符的指针。因为init_mm的pgd存放着swapper_pg_dir。swapper_pg_dir就是一级页表的起始虚拟地址。

   spin_lock(&init_mm.page_table_lock);//将页表锁锁上。
   do {
          pmd_t *pmd = pmd_alloc(&init_mm, dir, address);//这里无需理会,只要记住pmd=dir。如果系统需要建立三级页表的话,这里有具体的代码去实现的。由于我们这里只是建立二级页表,我们这里可以忽略不计。

          if (!pmd) {
             err = -ENOMEM;
             break;
         }                                                                                 
         if (map_area_pmd(pmd, address, end - address, prot, pages)) {//这里就是只为2MB虚拟空间建立映射,起始地址为address大小为end-address空间建立映射。具体的代码我会在后面详解的。

            err = -ENOMEM;
            break;
         }

         address = (address + PGDIR_SIZE) & PGDIR_MASK;//如果把前2MB的虚拟内存映射成功后,我们这里要把起始线性地址做调整,以便下次循环接着2MB后面的线性地址空间再建立页表。PGDIR_SIZE=1<<21=2MB

则PGDIR_MASK=小于2MB以后的地址我们都清零。为什么要这样做呢?原因是一上来第一次为2MB虚拟内存映射的时候,并没有做到全部的映射(除非一上来的起始位置就是按2MB对齐的),只是映射了address到address所在的2MB的下个2MB的起始位置。所以我们这条语句在与上PGDIR_MASK后,下次为2MB的映射时,我们就可以继续衔接上次的结束位置。
         dir++;//移到下一个一级页表条目,也就是指向一级描述符。这里说的下一个是2个描述符条目,因为我们在上面map_area_pmd函数中就完成了2MB的映射,此时这两个条目分别存放着第一张二级页表基址和第二张二级页表基址。每张页表都指向1MB的地址空间,二级页表中的每个条目中都存放着在物理内存中申请到的内存页的基址。每页是4kb大小,这样一张二级页表就需要1KB的大小来存放256项条目,为什么是256项条目呢?因为一张二级页表对应1MB的虚拟内存映射,每个条目对应一个内存页,内存页的大小为4KB,这样我们就需要256项了。为什么一张二级页表需要1KB空间来存储呢?由于一个条目(二级页表描述符)需要4个字节来表示。
         }while (address && (address < end));//一直把整个高端线性地址空间都建立对应的页表后,结束循环。  

    spin_unlock(&init_mm.page_table_lock);//将页表锁解开。
    flush_cache_vmap((unsigned long) area->addr, end);//刷新从area->addr开始到end的高端线性地址空间的cache。
    return err;//成功是返回0.
}

下面是讲解map_vm_area()函数中的pgd_offset_k()宏定义。 

  #define pgd_offset_k(addr) pgd_offset(&init_mm, addr)
  #define pgd_offset(mm, addr) ((mm)->pgd+pgd_index(addr))//mm就是上面的init_mm这个全局变量,它记录了整个系统内存的情况。它的pgd就是系统4GB虚拟内存中一级页表的起始位置,swapper_pg_dir是它的初始化值,我们一般在内核代码中看到的PAGE_OFFSET就是它=0xc0000000。从这里开始我们把低端的物理内存其中包括一级页表都从这个地址开始映射,一直映射到high_memery为止。swapper_pg_dir就是指向一级页表映射后的虚拟起始地址。

  #define pgd_index(addr)  ((addr) >> PGDIR_SHIFT)//PGDIR_SHIFT=21,这里很明显是要判断这个addr是属于哪个段的,为什么不是20呢?因为我们在上面的函数发现,Linux都是以2MB为单位进行映射的。我们有了前者加上这个偏移量,我们就可以得到我们需要的以及描述符的指针了。

  下面介绍的是map_vm_area()函数中调用的为2MB虚拟线性空间建立二级页表的函数map_area_pmd()函数。

  static int map_area_pmd(pmd_t *pmd, unsigned long address,unsigned long size, pgprot_t prot,struct page ***pages)
{
   unsigned long base, end;

   base = address & PGDIR_MASK;//base记录着address所在双段(2MB)的起始地址。
   address &= ~PGDIR_MASK;//这里记录了address在这个双段中的偏移位置。
   end = address + size;//记录这次申请高端线性空间的结束位置。
   if (end > PGDIR_SIZE)//如果这次建立页表的空间大于2MB的话,我们要限制它。
      end = PGDIR_SIZE;//起始在这里就可以知道这个函数最多为2MB空间建立二级页表。

   do {
         pte_t * pte = pte_alloc_kernel(&init_mm, pmd, base + address);//这个函数很重要,也是难点,base+address开始到address所在段(1M)这个范围是由某个二级页表描述的,这个函数首先就要为这个二级页表建立一个空间,它使用__get_free_page()向伙伴系统申请一页内存页。这里为什么申请一页内存页呢?起始Linux在arm体系中在每次都会连续建立两张2级页表的,一张页表我们在上面已经分析过是1kb那么大。出此之外,还要有2张硬件版本的二级页表。这样,我们就有4张页表。一共就是4kb了,刚好凑够一页内存页。接下来调用pmd_populate_kernel()函数主要是完成把刚才建立好的二级页表存的起始地址(通过__pa(x)转换为物理地址)存放在pmd和pmd+1的单元(这个单元的大小为4个字节)里面。这样这个范围对应的一级页表就填充完了,剩下就是在二级页表中对应的条目附上每个内存页的起始地址(物理),完成了这两步后,我们的映射就建立好了。后面那一步我们将会在下面提到。接着就是最后面的pte_offset_kernel()这个函数。这个函数就是找到这个范围对应的二级页表中一段条目的首条目的指针。这个也pte_alloc_kernel()函数的返回值。
        if (!pte)
             return -ENOMEM;
        if (map_area_pte(pte, address, end - address, prot, pages))//设置二级描述符,把它存放到二级页表中的某一条目,这就是我上面说的第二步了,下面我会对他仔细分析的。
             return -ENOMEM;
        address = (address + PMD_SIZE) & PMD_MASK;//PMD_SIZE=1<<20因为完成了第一段,我们调整起始位置,做第二段。
        pmd++;
        } while (address < end);//一般这里只是循环两次,注意在做第二次循环的时候,我们执行到pte_alloc_kernel()函数时,我们不会再做第一步了,因为此时我们的pmd+1里面不为空。这里的循环只是为了配合我们接下来要说的函数map_area_pte()。

     return 0;//成功返回0.
}
  static int map_area_pte(pte_t *pte, unsigned long address,unsigned long size, pgprot_t prot,struct page ***pages)
{
    unsigned long end;

    address &= ~PMD_MASK;//address记录着这个地址在1MB内的偏移量。
    end = address + size;//建立映射空间结束位置相对于1MB起始位置的偏移量。
    if (end > PMD_SIZE)//这里我们只是为1MB建立二级映射。
       end = PMD_SIZE;

   do {
        struct page *page = **pages;//这里是让page指向我们最前面在物理内存中申请到的内存页的首页指针。

        WARN_ON(!pte_none(*pte));//如果传进来的二级描述符组首位描述符不为空,系统发出警告。
        if (!page)
           return -ENOMEM;

  set_pte(pte, mk_pte(page, prot));//mk_pte(page,port)这个宏先通过page_to_pfn(page)找到struct page这个结构体对应页的页号,再由pfn_pte()通过pfn<<PAGE_SHIFT求得物理地址后再和port控制字相与。我们有了这页的物理起始地址后,再把它放入相应的二级条目中,这样就完成了第二步了。
       address += PAGE_SIZE;//完成一页接着下一页的起始位置调整,为了do whie循环的判断条件使用。
       pte++;//二级条目指针调整。
       (*pages)++;//物理内存页调整。
       } while (address < end);
 return 0;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值