关于高端内存的一些笔记

 

来源:http://linux.chinaunix.net/bbs/viewthread.php?tid=1156030

作者:fido_zy

闲着无聊,把一些东西写下来给大家分享下吧,有什么不对的,欢迎质疑

注:本文提及的物理地址空间可以理解为就是物理内存,但是在某些情况下,把他们理解为物理内存是不对的。
本文讨论的环境是NON-PAE的i386平台,内核版本2.6.31-14
一. 什么是高端内存
linux中内核使用3G-4G的线性地址空间,也就是说总共只有1G的地址空间可以用来映射物理地址空间。但是,如果内存大于1G的情况下呢?是不是超过1G的内存就无法使用了呢?为此内核引入了一个高端内存的概念,把1G的线性地址空间划分为两部分:小于896M物理地址空间的称之为低端内存,这部分内存的物理地址和3G开始的线性地址是一一对应映射的,也就是说内核使用的线性地址空间3G--(3G+896M)和物理地址空间0-896M一一对应;剩下的128M的线性空间用来映射剩下的大于896M的物理地址空间,这也就是我们通常说的高端内存区。
所谓的建立高端内存的映射就是能用一个线性地址来访问高端内存的页。如何理解这句话呢?在开启分页后,我们要访问一个物理内存地址,需要经过MMU的转换,也就是一个32位地址vaddr的高10位用来查找该vaddr所在页目录项,用12-21位来查找页表项,再用0-11位偏移和页的起始物理地址相加得到paddr,再把该paddr放到前端总线上,那么我们就可以访问该vaddr对应的物理内存了。在低端内存中,每一个物理内存页在系统初始化的时候都已经存在这样一个映射了。而高端内存还不存在这样一个映射(页目录项,页表都是空的),所以我们必须要在系统初始化完后,提供一系列的函数来实现这个功能,这就是所谓的高端内存的映射。那么我们为什么不再系统初始化的时候把所有的内存映射都建立好呢?主要原因是,内核线性地址空间不足以容纳所有的物理地址空间(1G的内核线性地址空间和最多可达4G的物理地址空间),所以才需要预留一部分(128M)的线性地址空间来动态的映射所有的物理地址空间,于是就产生了所谓的高端内存映射。
二.内核如何管理高端内存

 



 

上面的图展示了内核如何使用3G-4G的线性地址空间,首先解释下什么是high_memory
在arch/x86/mm/init_32.c里面由如下代码:

#ifdef CONFIG_HIGHMEM
       highstart_pfn = highend_pfn = max_pfn;
      if (max_pfn > max_low_pfn)
            highstart_pfn = max_low_pfn;
       e820_register_active_regions(0, 0, highend_pfn);
       sparse_memory_present_with_active_regions(0);
       printk(KERN_NOTICE "%ldMB HIGHMEM available.\n",
            pages_to_mb(highend_pfn - highstart_pfn));
       num_physpages = highend_pfn;
      high_memory = (void *) __va(highstart_pfn * PAGE_SIZE-1)+1;
#else
       e820_register_active_regions(0, 0, max_low_pfn);
       sparse_memory_present_with_active_regions(0);
       num_physpages = max_low_pfn;
      high_memory = (void *) __va(max_low_pfn * PAGE_SIZE - 1)+1;
#endif

high_memory是“具体物理内存的上限对应的虚拟地址”,可以这么理解:当内存内存小于896M时,那么 high_memory = (void *) __va(max_low_pfn * PAGE_SIZE),max_low_pfn就是在内存中最后的一个页帧号,所以high_memory=0xc0000000+物理内存大小;当内存大于896M时,那么highstart_pfn = max_low_pfn, 此时max_low_pfn就不是物理内存的最后一个页帧号了,而是内存为896M时的最后一个页帧号,那么high_memory=0xc0000000+896M.总之high_memory是不能超过0xc0000000+896M.
由于我们讨论的是物理内存大于896M的情况,所以high_memory实际上就是0xc0000000+896M,从high_memory开始的128M(4G-high_memory)就是用作用来映射剩下的大于896M的内存的,当然这128M还可以用来映射设备的内存(MMIO)。
从上图我们看到有VMALLOC_START,VMALLOC_END,PKMAP_BASE,FIX_ADDRESS_START等宏术语,其实这些术语划分了这128M的线性空间,一共分为三个区域:VMALLOC区域(本文不涉及这部分内容,关注本博客的其他文章),永久映射区(permanetkernel mappings), 临时映射区(temporary kernel mappings).这三个区域都可以用来映射高端内存,本文重点阐述下后两个区域是如何映射高端内存的。


三. 永久映射区(permanet kernel mappings)
1. 介绍几个定义:
PKMAP_BASE:永久映射区的起始线性地址。
pkmap_page_table:永久映射区对应的页表
LAST_PKMAP:pkmap_page_table里面包含的entry的数量=1024
pkmap_count[LAST_PKMAP]数组:每一个元素的值对应一个entry的引用计数。 关于引用计数的值,有以下几种情况:

0:说明这个entry可用。
1:entry不可用,虽然这个entry没有被用来映射任何内存,但是他仍然存在TLB entry没有被flush,

    所以还是不可用。


N:有N-1个对象正在使用这个页面


首先,要知道这个区域的大小是4M,也就是说128M的线性地址空间里面,只有4M的线性地址空间是用来作永久映射区的。至于到底是哪4M,是由PKMAP_BASE决定的,这个变量表示用来作永久内存映射的4M区间的起始线性地址。
在NON-PAE的i386上,页目录里面的每一项都指向一个4M的空间,所以 永久映射区只需要一个页目录项就可以了。而一个页目录项指向一张页表,那么 永久映射区正好就可以用一张页表来表示了,于是我们就用 pkmap_page_table来指向这张页表。
                                                           
            pgd = swapper_pg_dir + pgd_index(vaddr);
       pud = pud_offset(pgd, vaddr);//pud==pgd
       pmd = pmd_offset(pud, vaddr);//pmd==pud==pgd
       pte = pte_offset_kernel(pmd, vaddr);
       pkmap_page_table = pte;


2. 具体代码分析(2.6.31)

                                                                                                                              
void *kmap(struct page *page)
{
       might_sleep();
      if (!PageHighMem(page))
            return page_address(page);
      return kmap_high(page);
}
kmap()函数就是用来建立永久映射的函数:由于调用kmap函数有可能会导致进程阻塞,所以它不能在中断处理函数等不可被阻塞的上下文下被调用,might_sleep()的作用就是当该函数在不可阻塞的上下文下被调用是,打印栈信息。接下来判断该需要建立永久映射的页是否确实属于高端内存,因为我们知道低端内存的每个页都已经存在和线性地址的映射了,所以,就不需要再建立了,page_address()函数返回该page对应的线性地址。(关于page_address()函数,参考本博客的专门文章有解释)。最后调用kmap_high(page),可见kmap_high()才真正执行建立永久映射的操作。

/**
* kmap_high - map a highmem page into memory
* @page: &struct page to map
*
* Returns the page's virtual memory address.
*
* We cannot call this from interrupts, as it may block.
*/

void *kmap_high(struct page *page)
{
      unsigned long vaddr;
      /*
      * For highmem pages, we can't trust "virtual" until
      * after we have the lock.
      */

       lock_kmap();
       vaddr = (unsigned long)page_address(page);
      if (!vaddr)
            vaddr = map_new_virtual(page);
       pkmap_count[PKMAP_NR(vaddr)]++;
       BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2);
       unlock_kmap();
      return (void*) vaddr;
}
kmap_high函数分析:首先获得对pkmap_page_table操作的锁,然后再调用page_address()来返回该page是否已经被映射,我们看到前面在kmap()里面已经判断过了,为什么这里还要再次判断呢?因为再获的锁的时候,有可能锁被其他CPU拿走了,而恰巧其他CPU拿了这个锁之后,也是执行这段code,而且映射的也是同一个page,那么当它把锁释放掉的时候,其实就表示该page的映射已经被建立了,我们这里就没有必要再去执行这段code了,所以就有必要在获得锁后再判断下。
如果发现vaddr不为空,那么就是刚才说的,已经被其他cpu上执行的任务给建立了,这里只需要把表示该页引用计数的pkmap_count[]再加一就可以了。同时调用BUG_ON来确保该引用计数确实是不小于2的,否则就是有问题的了。然后返回vaddr,整个建立就完成了。
如果发现vaddr为空呢?调用map_new_virtual()函数,到此我们看到,其实真正进行建立映射的代码在这个函数里面

static inline unsigned long map_new_virtual(struct page *page)
{   
      unsigned long vaddr;
      int count;
                    
start:               
      count = LAST_PKMAP;//LAST_PKMAP=1024
      /* Find an empty entry */
      for (;;) {   
            last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK;
            if (!last_pkmap_nr) {
                     flush_all_zero_pkmaps();
                     count = LAST_PKMAP;
            }
            if (!pkmap_count[last_pkmap_nr])
                     break;  /* Found a usable entry */
            if (--count)
                     continue;
      
            /*
                * Sleep for somebody else to unmap their entries
                */
   
            {   
                     DECLARE_WAITQUEUE(wait, current);
           
                     __set_current_state(TASK_UNINTERRUPTIBLE);
                     add_wait_queue(&pkmap_map_wait, &wait);
                     unlock_kmap();
                     schedule();
                     remove_wait_queue(&pkmap_map_wait, &wait);
                     lock_kmap();

                     /* Somebody else might have mapped it while we slept */
                     if (page_address(page))
                              return (unsigned long)page_address(page);

                     /* Re-start */
                     goto start;
            }
      }
       vaddr = PKMAP_ADDR(last_pkmap_nr);
       set_pte_at(&init_mm, vaddr,
               &(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot));

       pkmap_count[last_pkmap_nr] = 1;
       set_page_address(page, (void *)vaddr);

      return vaddr;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值