一. 什么是高端内存
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_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; } |
如果发现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; } |