高端内存

一. 什么是高端内存


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;
}
last_pkmap_nr:记录上次被分配的页表项在pkmap_page_table里的位置,初始值为0,所以第一次分配的时候last_pkmap_nr等于1。

接下来判断什么时候last_pkmap_nr等于0,等于0就表示1023(LAST_PKMAP(1024)-1)个页表项已经被分配了,这时候就需 要调用flush_all_zero_pkmaps()函数,把所有pkmap_count[] 计数为1的页表项在TLB里面的entry给flush掉,并重置为0,这就表示该页表项又可以用了,可能会有疑惑为什么不在把pkmap_count置 为1的时候也就是解除映射的同时把TLB也flush呢?个人感觉有可能是为了效率的问题吧,毕竟等到不够的时候再刷新,效率要好点吧。

再判断pkmap_count[last_pkmap_nr]是否为0,0的话就表示这个页表项是可用的,那么就跳出循环了到下面了。

PKMAP_ADDR(last_pkmap_nr)返回这个页表项对应的线性地址vaddr.

#define PKMAP_ADDR(nr) (PKMAP_BASE + ((nr) << PAGE_SHIFT))

set_pte_at(mm, addr, ptep, pte)函数在NON-PAE i386上的实现其实很简单,其实就等同于下面的代码:

static inline void native_set_pte(pte_t *ptep , pte_t pte)
{
       *ptep = pte;
}
我们已经知道页表的线性起始地址存放在pkmap_page_table里面,那么相应的可用的页表项的地址就是& pkmap_page_table[last_pkmap_nr],得到了页表项的地址,只要把相应的pte填写进去,那么整个映射不就完成了吗?
pte由两部分组成:高20位表示物理地址,低12位表示页的描述信息。
怎么通过page查找对应的物理地址呢(参考page_address()一文)?其实很简单,用(page - mem_map) 再移PAGE_SHIFT位就可以了。
低12位的页描述信息是固定的:kmap_prot=(_PAGE_PRESENT | _PAGE_RW | _PAGE_DIRTY | _PAGE_ACCESSED | _PAGE_GLOBAL).
下面的代码就是做了这些事情:

mk_pte(page, kmap_prot));
#define mk_pte(page, pgprot) pfn_pte(page_to_pfn(page), (pgprot))
#define page_to_pfn __page_to_pfn
#define __page_to_pfn(page) ((unsigned long)((page) - mem_map) + /
                              ARCH_PFN_OFFSET)
static inline pte_t pfn_pte(unsigned long page_nr, pgprot_t pgprot)
{
       return __pte(((phys_addr_t)page_nr << PAGE_SHIFT) |
                  massage_pgprot(pgprot));
}

接下来把pkmap_count[last_pkmap_nr]置为1,1不是表示不可用吗,既然映射已经建立好了,应该赋值为2呀,其实这个操作是在他 的上层函数kmap_high里面完成的(pkmap_count[PKMAP_NR(vaddr)]++).
到此为止,整个映射就完成了,再把page和对应的线性地址加入到page_address_htable哈希链表里面就可以了(参考page_address一文)。


我们继续看所有的页表项都已经用了的情况下,也就是1024个页表项全已经映射了内存了,如何处理。此时count==0,于是就进入了下面的代码:

/*
                * 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;
            }
这段代码其实很简单,就是把当前任务加入到等待队列pkmap_map_wait,当有其他任务唤醒这个队列时,再继续goto start,重新整个过程。这里就是上面说的调用kmap函数有可能阻塞的原因。
那么什么时候会唤醒pkmap_map_wait队列呢?当调用kunmap_high函数,来释放掉一个映射的时候。
kunmap_high函数其实页很简单,就是把要释放的页表项的计数减1,如果等于1的时候,表示有可用的页表项了,再唤醒pkmap_map_wait队列

/**
* kunmap_high - map a highmem page into memory
* @page: &struct page to unmap
*
* If ARCH_NEEDS_KMAP_HIGH_GET is not defined then this may be called
* only from user context.
*/
void kunmap_high(struct page *page)
{
       unsigned long vaddr;
       unsigned long nr;
       unsigned long flags;
       int need_wakeup;

       lock_kmap_any(flags);
       vaddr = (unsigned long)page_address(page);
       BUG_ON(!vaddr);
       nr = PKMAP_NR(vaddr);

       /*
      * A count must never go down to zero
      * without a TLB flush!
      */
       need_wakeup = 0;
       switch (--pkmap_count[nr]) {//减一
       case 0:
            BUG();
       case 1:
            /*
                * Avoid an unnecessary wake_up() function call.
                * The common case is pkmap_count[] == 1, but
                * no waiters.
                * The tasks queued in the wait-queue are guarded
                * by both the lock in the wait-queue-head and by
                * the kmap_lock. As the kmap_lock is held here,
                * no need for the wait-queue-head's lock. Simply
                * test if the queue is empty.
                */
            need_wakeup = waitqueue_active(&pkmap_map_wait);
       }
       unlock_kmap_any(flags);

       /* do wake-up, if needed, race-free outside of the spin lock */
       if (need_wakeup)
            wake_up(&pkmap_map_wait);
}

 

<think>好的,我现在需要帮助用户理解高端内存映射的概念及其实现方式和原理。首先,我要回顾一下用户提供的引用资料,看看有哪些相关信息可以利用。 根据引用[1],高端内存被定义为不能通过直接偏移映射的虚拟地址空间,而低端内存可以通过直接映射。例如,在ARM架构中,永久映射可能在high_memory之下,但仍属于高端内存。这说明高端内存的界定更侧重于映射方式而非物理位置。 引用[4]提到Linux的直接映射区是线性空间从3G开始的896MB区域,而永久内核映射区用于访问高端内存。这帮助我理解不同内存区域的结构。此外,引用[3]中的系统调用如vmalloc()和kmap()可能涉及高端内存的管理,但需要进一步确认。 用户的问题集中在实现技术和原理上,所以我需要结合这些引用,整理出高端内存存在的背景、具体实现方式以及相关技术。可能需要解释为什么需要高端内存,比如32位系统的地址空间限制,导致无法直接映射所有物理内存,从而引入高端内存的概念。 然后,实现方式方面,引用[1]提到永久映射和动态映射,可能对应kmap和vmalloc等方法。同时,引用[4]中的永久内核映射区和动态内存映射区应详细说明,包括它们如何工作,比如页表动态管理、临时映射等。 还需要注意区分不同体系结构下的差异,如ARM和x86的可能不同处理,这能帮助用户理解为何高端内存的定义不单纯依赖物理位置。 最后,确保回答结构清晰,分点解释,并结合原理和实现技术。同时生成相关问题,比如应用场景、与低端内存的区别、32/64位系统的影响等,这些能帮助用户深入理解。</think>### 高端内存映射技术实现与原理 #### 1. **背景与定义** 高端内存(High Memory)主要用于解决 **32位系统虚拟地址空间不足** 的问题。在32位架构中,内核虚拟地址空间通常被划分为: - **低端内存(Low Memory)**:直接映射区(如Linux中3G~3G+896MB),通过固定偏移(如`PAGE_OFFSET`)实现物理地址到虚拟地址的线性映射,访问效率高[^4]。 - **高端内存(High Memory)**:无法通过直接偏移映射的物理内存,需动态管理虚拟地址与物理页的映射关系[^1]。 #### 2. **实现原理** 高端内存的核心是 **动态虚拟地址分配**,通过以下技术实现: - **页表动态管理**:内核维护临时映射(如`kmap()`),将高端内存页临时映射到内核虚拟地址空间的小块区域(如永久映射区或固定映射区)[^4]。 - **按需映射**:仅在需要时建立映射,使用后释放虚拟地址资源。例如,`vmalloc()`从动态内存映射区分配虚拟地址,支持非连续物理内存的访问[^4]。 - **体系结构适配**:ARM等架构可能通过永久映射区(位于低端内存之上)实现高端内存访问,打破“高端内存=高地址”的误解[^1]。 #### 3. **关键技术** - **永久映射(kmap)**:通过`kmap()`为高端内存页分配固定虚拟地址,需手动释放(`kunmap()`),适用于长期访问的场景。 - **临时映射(kmap_atomic)**:使用CPU的TLB快速建立短期映射,映射后需尽快释放,常用于中断上下文[^4]。 - **非连续内存分配(vmalloc)**:将离散的物理页映射到连续的虚拟地址空间,适合大块内存请求,但访问效率低于直接映射区[^3]。 #### 4. **典型应用场景** - **物理内存超过内核直接映射容量**:如32位系统物理内存超过896MB时,超出部分通过高端内存管理。 - **动态内核模块加载**:模块代码可能分配到高端内存区域。 - **用户空间与内核空间的数据交换**:如`get_user_pages()`将用户内存页映射到内核高端内存区域。 --- ### 示例:Linux内核中的高端内存映射 ```c // 使用kmap映射高端内存struct page *high_mem_page = alloc_pages(GFP_HIGHUSER, 0); // 分配高端内存页 void *vaddr = kmap(high_mem_page); // 建立虚拟地址映射 memcpy(vaddr, src_data, PAGE_SIZE); // 操作映射后的内存 kunmap(high_mem_page); // 释放映射 ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值