`
hro01hro
  • 浏览: 17511 次
社区版块
存档分类
最新评论

高端内存

阅读更多

高端内存
2010年10月21日
  :本文提及的物理地址 空间可以理解为就是物理内存 ,但是在某些情况下,把他们理解为物理内存是不对的。
  本文讨论的环境是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_S TART等宏术语,其实这些术语 划分了这128M的线性空间,一共分为三个区域:VMALLOC区域(本文不涉及这部分内容,关注本博客的其他文章),永久映射区(permanetkernel mappings), 临时映射区(temporary kernel mappings).这三个区域都可以用来映射高端内存,本文重点阐述下后两个区域是如何映射高端内存的。
  三. 永久映射区(permanet kernel mappings)
  1. 介绍几个定义:
  PKMAP_BASE        :永久映射区的起始线性地址。
  pkmap_page_table  :永久内核映射允许内核建立高端页框到内核地址空间的长期映射,它们使用主内核页表中一个专门的页表,其地址存放在
  pkmap_page_table 。
  LAST_PKMAP          :pkmap_page_table里面包含的entry的数量=1024 或512,取决于PAE是否激活。
  pkmap_count[LAST_PKMAP]数组:每一个元素的值对应一个entry的引用计数。关于引用计数的值,有以下几种情况:
  0:说明这个entry可用。
  1:语义特殊,表示该位置的页已经映射,但由于CPU的TLB没有更新而无法使用,见下面的情景。
  N:有N-1个对象正在使用这个页面
  首先,要知道这个区域的大小是4M,也就是说128M的线性地址空间里面,只有4M的线性地址空间是用来作永久映射区的。至于到底是哪4M,是由PKMAP_BASE决定的,这个变量表示用来作永久内存映射的4M区间的起始线性地址。(如果启动PAE,则只有2M)
  在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) ] 代码在这个函数里面
  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; }
  10          last_pkmap_nr 是全局变量,记录最后使用的位置,该行使 last_pkmap_nr曾1,
  当到达pkmap_count的最大索引值 LAST_PKMAP时,搜索绕回到位置0继续.这就
  实现了所谓的反向扫描pkmap_count数组。并且在绕回到位置0时顺便执行
  flush_all_zero_pkmaps()来刷出CPU高速缓存,但尚未更新TLB。
  15          pkmap_count[ last_pkmap_nr]=0,说明我们找到了一个位置,可以使用的。
  17          执行到这里,说明我们还没找到,如果--count不为0,说明还没有搜索完一整圈,继续
  搜索。如果为0,说明已经搜索了一整圈了,还没找到合适的位置。
  23~37   该流程进入睡眠状态,其中33~34是希望睡眠期间有其他哥们已经帮我把page映射了。
  如果没有则跳到start处再重新执行搜索过程。直到找到为止。
  39          终于找到了一个位置,通过PKMAP_ADDR计算出该位置处的虚拟地址
  #define PKMAP_ADDR(nr)  (PKMAP_BASE + ((nr) 内存映射中我们看到,如果pkmap_page_table页表里面没有空的entry,那么就会导致这次映射被阻塞,所以我们说不能在一些原子的上下文情况下调用kmap()函数。而在临时内存映射中,不会去判断该pte是否已经被用掉了,它采用的是覆盖的策略,所以把总是能成功的建立映射。会不会被阻塞就是临时内存映射和永久内存映射一个最明显的区别。
  
  enum fixed_addresses {
  #ifdef CONFIG_X86_32
  FIX_HOLE,
  FIX_VDSO,
  #else
  VSYSCALL_LAST_PAGE,
  VSYSCALL_FIRST_PAGE = VSYSCALL_LAST_PAGE
  +(( VSYSCALL_END- VSYSCALL_START)>> PAGE_SHIFT)- 1,
  VSYSCALL_HPET,
  #endif
  FIX_DBGP_BASE,
  FIX_EARLYCON_MEM_BASE,
  #ifdef CONFIG_X86_LOCAL_APIC
  FIX_APIC_BASE,/* local (CPU) APIC) -- required for SMP or not */
  #endif
  #ifdef CONFIG_X86_IO_APIC
  FIX_IO_APIC_BASE_0,
  FIX_IO_APIC_BASE_END = FIX_IO_APIC_BASE_0 + MAX_IO_APICS - 1,
  #endif
  #ifdef CONFIG_X86_VISWS_APIC
  FIX_CO_CPU,/* Cobalt timer */
  FIX_CO_APIC,/* Cobalt APIC Redirection Table */
  FIX_LI_PCIA,/* Lithium PCI Bridge A */
  FIX_LI_PCIB,/* Lithium PCI Bridge B */
  #endif
  #ifdef CONFIG_X86_F00F_BUG
  FIX_F00F_IDT,/* Virtual mapping for IDT */
  #endif
  #ifdef CONFIG_X86_CYCLONE_TIMER
  FIX_CYCLONE_TIMER,/*cyclone timer register*/
  #endif
  #ifdef CONFIG_X86_32
  FIX_KMAP_BEGIN,/* reserved pte's for temporary kernel mappings */
  FIX_KMAP_END = FIX_KMAP_BEGIN+( KM_TYPE_NR* NR_CPUS)- 1,
  #ifdef CONFIG_PCI_MMCONFIG
  FIX_PCIE_MCFG,
  #endif
  #endif
  #ifdef CONFIG_PARAVIRT
  FIX_PARAVIRT_BOOTMAP,
  #endif
  FIX_TEXT_POKE1,/* reserve 2 pages for text_poke() */
  FIX_TEXT_POKE0,/* first page is last, because allocation is backward */
  __end_of_permanent_fixed_addresses,
  /*
  * 256 temporary boot-time mappings, used by early_ioremap(),
  * before ioremap() is functional.
  *
  * We round it up to the next 256 pages boundary so that we
  * can have a single pgd entry and a single pte table:
  */
  #define NR_FIX_BTMAPS 64
  #define FIX_BTMAPS_SLOTS 4
  FIX_BTMAP_END = __end_of_permanent_fixed_addresses + 256 -
  ( __end_of_permanent_fixed_addresses & 255),
  FIX_BTMAP_BEGIN = FIX_BTMAP_END + NR_FIX_BTMAPS* FIX_BTMAPS_SLOTS - 1,
  #ifdef CONFIG_PROVIDE_OHCI1394_DMA_INIT
  FIX_OHCI1394_BASE,
  #endif
  #ifdef CONFIG_X86_32
  FIX_WP_TEST,
  #endif
  #ifdef CONFIG_INTEL_TXT
  FIX_TBOOT_BASE,
  #endif
  __end_of_fixed_addresses
  };
  从FIXADDR_START到FIXADDR_TOP这段地址空间被称为固定映射线性空间,这段地址空间主要用来映射一些设备的内存和寄存器,像FIX_APIC_BASE是用来映射local apic的寄存器的。其中的FIX_KMAP_BEGIN到FIX_KMAP_END是用来供临时内存映射的使用。 这段空间的大小是KM_TYPE_NR* NR_CPUS,也就是说,每个CPU都有独立的KM_TYPE_NR大小的空间,来看看看KM_TYPE_NR是什么
  #ifdef __WITH_KM_FENCE
  #define KMAP_D( n) __KM_FENCE_## n ,
  #else
  #define KMAP_D( n)
  #endif
  enum km_type {
  KMAP_D( 0) KM_BOUNCE_READ,
  KMAP_D( 1) KM_SKB_SUNRPC_DATA,
  KMAP_D( 2) KM_SKB_DATA_SOFTIRQ,
  KMAP_D( 3) KM_USER0,
  KMAP_D( 4) KM_USER1,
  KMAP_D( 5) KM_BIO_SRC_IRQ,
  KMAP_D( 6) KM_BIO_DST_IRQ,
  KMAP_D( 7) KM_PTE0,
  KMAP_D( KM_PTE1,
  KMAP_D( 9) KM_IRQ0,
  KMAP_D( 10) KM_IRQ1,
  KMAP_D( 11) KM_SOFTIRQ0,
  KMAP_D( 12) KM_SOFTIRQ1,
  KMAP_D( 13) KM_SYNC_ICACHE,
  KMAP_D( 14) KM_SYNC_DCACHE,
  /* UML specific, for copy_*_user - used in do_op_one_page */
  KMAP_D( 15) KM_UML_USERCOPY,
  KMAP_D( 16) KM_IRQ_PTE,
  KMAP_D( 17) KM_NMI,
  KMAP_D( 18) KM_NMI_PTE,
  KMAP_D( 19) KM_TYPE_NR
  };
  每个枚举项都代表了一个pte,每个CPU都有20个这样的pte供临时内核映射使用。我们先来看看内核如何来获得这些对应的枚举项所对应的pte地址: 这个宏返回的是固定线性映射里面枚举项所对应的线性地址,FIXADDR_TOP=0xfffff000,以FIX_APIC_BASE为例,__fix_to_virt(FIX_APIC_BASE),FIX_APIC_BASE等于4,那么他的线性地址就是0xfffff000-(4代码是完整的如何通过枚举项,获得对应的pte项所对应的线性地址。
  staticinline pte_t * kmap_get_fixmap_pte(unsignedlong vaddr)
  {
  return pte_offset_kernel( pmd_offset( pud_offset( pgd_offset_k( vaddr), vaddr), vaddr), vaddr);
  }
  staticvoid __init kmap_init(void)
  {
  unsignedlong kmap_vstart;
  /*
  * Cache the first kmap pte:
  */
  kmap_vstart = __fix_to_virt( FIX_KMAP_BEGIN);
  kmap_pte = kmap_get_fixmap_pte( kmap_vstart);
  kmap_prot = PAGE_KERNEL;
  }
  注意:kmap_pte是临时内存映射起始的页表项,接下来的其他页表项根据他们的相对位置差就可以求得。比如idx对应的pte=kmap_pte - idx,为什么是减不是加,看上面的说明(越靠前的枚举项对应的线性地址越靠后 )。 /*
  * kmap_atomic/kunmap_atomic is significantly faster than kmap/kunmap because
  * no global lock is needed and because the kmap code must perform a global TLB
  * invalidation when the kmap pool wraps.
  *
  * However when holding an atomic kmap it is not legal to sleep, so atomic
  * kmaps are appropriate for short, tight code paths only.
  */
  void* kmap_atomic_prot(struct page * page,enum km_type type, pgprot_t prot)
  {
  enum fixed_addresses idx;
  unsignedlong vaddr;
  /* even !CONFIG_PREEMPT needs this, for in_atomic in do_page_fault */
  pagefault_disable();
  //判断要建立映射的页在高端内存中,否则其线性地址已经存在
  if(! PageHighMem( page))
  return page_address( page);
  debug_kmap_atomic( type);
  //求得对应的idx
  idx = type + KM_TYPE_NR* smp_processor_id();         //内核可以访问该页的线性地址
  vaddr = __fix_to_virt( FIX_KMAP_BEGIN + idx);
  BUG_ON(! pte_none(*( kmap_pte- idx)));         //根据kmap_pte,建立对应的页表项映射,之后就可以通过vaddr访问该页了
  set_pte( kmap_pte- idx, mk_pte( page, prot));
  return(void*) vaddr;
  }
  void* kmap_atomic(struct page * page,enum km_type type)
  {
  return kmap_atomic_prot( page, type, kmap_prot);
  }
  解除映射也很简单,没有做什么特殊处理: void kunmap_atomic(void* kvaddr,enum km_type type)
  {
  unsignedlong vaddr =(unsignedlong) kvaddr & PAGE_MASK;
  enum fixed_addresses idx = type + KM_TYPE_NR* smp_processor_id();
  /*
  * Force other mappings to Oops if they'll try to access this pte
  * without first remap it. Keeping stale mappings around is a bad idea
  * also, in case the page changes cacheability attributes or becomes
  * a protected page in a hypervisor.
  */
  if( vaddr == __fix_to_virt( FIX_KMAP_BEGIN+ idx))
  kpte_clear_flush( kmap_pte- idx, vaddr);
  else{
  #ifdef CONFIG_DEBUG_HIGHMEM
  BUG_ON( vaddr =(unsignedlong) high_memory);
  #endif
  }
  pagefault_enable();
  }
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics