免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
12下一页
最近访问板块 发新帖
查看: 13918 | 回复: 12

关于高端内存的一些笔记 [复制链接]

论坛徽章:
0
发表于 2010-01-29 16:52 |显示全部楼层
闲着无聊,把一些东西写下来给大家分享下吧,有什么不对的,欢迎质疑

注:本文提及的物理地址空间可以理解为就是物理内存,但是在某些情况下,把他们理解为物理内存是不对的。
本文讨论的环境是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)的线性地址空间来动态的映射所有的物理地址空间,于是就产生了所谓的高端内存映射。
二.内核如何管理高端内存


file:///tmp/moz-screenshot.png

上面的图展示了内核如何使用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;
}




[ 本帖最后由 fido_zy 于 2010-1-29 16:54 编辑 ]

评分

参与人数 1可用积分 +30 收起 理由
T-Bagwell + 30 精品文章

查看全部评分

论坛徽章:
0
发表于 2010-01-29 16:53 |显示全部楼层
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);
}

论坛徽章:
0
发表于 2010-01-29 16:56 |显示全部楼层
怎么回复的格式不对呀,

论坛徽章:
12
2015年辞旧岁徽章
日期:2015-03-03 16:54:1515-16赛季CBA联赛之同曦
日期:2017-03-17 19:13:162016科比退役纪念章
日期:2016-11-07 08:28:12luobin
日期:2016-06-17 17:46:36wusuopu
日期:2016-06-17 17:43:4515-16赛季CBA联赛之福建
日期:2016-01-14 12:49:22程序设计版块每日发帖之星
日期:2015-12-13 06:20:00程序设计版块每日发帖之星
日期:2015-06-08 22:20:00程序设计版块每日发帖之星
日期:2015-06-08 22:20:002015年亚洲杯之科威特
日期:2015-03-24 14:21:272015年迎新春徽章
日期:2015-03-04 09:57:092016科比退役纪念章
日期:2018-04-10 16:20:18
发表于 2010-01-29 17:58 |显示全部楼层
有一个精华帖诞生了。

顶一下。
学习。

论坛徽章:
0
发表于 2010-02-03 02:41 |显示全部楼层
不错。不过好像还没写完?

论坛徽章:
0
发表于 2010-02-04 21:39 |显示全部楼层
这里我有点疑问,我看的的代码是2.6.31.9
max_low_pfn = find_low_pfn_range();
void __init find_low_pfn_range(void)
{
        /* it could update max_pfn */

        if (max_pfn <= MAXMEM_PFN)
                lowmem_pfn_init();
        else
                highmem_pfn_init();
}

#define MAXMEM_PFN        PFN_DOWN(MAXMEM)

#define MAXMEM        (VMALLOC_END - PAGE_OFFSET - __VMALLOC_RESERVE)

unsigned int __VMALLOC_RESERVE = 128 << 20;

#ifdef CONFIG_X86_PAE
#define LAST_PKMAP 512
#else
#define LAST_PKMAP 1024
#endif

#define PKMAP_BASE ((FIXADDR_BOOT_START - PAGE_SIZE * (LAST_PKMAP + 1))        \
                    & PMD_MASK)

#ifdef CONFIG_HIGHMEM
# define VMALLOC_END        (PKMAP_BASE - 2 * PAGE_SIZE)
#else
# define VMALLOC_END        (FIXADDR_START - 2 * PAGE_SIZE)
#endif

很明显,#define MAXMEM        (VMALLOC_END - PAGE_OFFSET - __VMALLOC_RESERVE)
这一句可以说明:
如果物理内存超过896M,高端内存很明显不是从896M开始的。不知道这个怎么解释?

论坛徽章:
0
发表于 2010-02-05 00:04 |显示全部楼层
回复 6# hengshan
对,最早的时候是896M:
#define MAXMEM (-__PAGE_OFFSET - __VMALLOC_RESERVE)
这个宏算出来的MAXMEM是896M,但是这时128M的vmalloc区域就包含了高端内存的永久映射区和固定映射区,在某些情况下会导致内核OOPS。
所以,从patch(e621bd189)之后,MAXMEM就把上面的两部分去掉了,变成了现在看到的这个样子,那么这个时候得出来的高端内存的分界点应该是比896M要小一点。

论坛徽章:
0
发表于 2011-08-02 16:37 |显示全部楼层
没看的太明白,先标记一下。

论坛徽章:
0
发表于 2011-08-29 09:54 |显示全部楼层
分析的很好,多谢楼主。代码部分,背景色给遮掉了好多代码。

论坛徽章:
0
发表于 2011-08-31 11:19 |显示全部楼层
"剩下的128M的线性空间用来映射剩下的大于896M的物理地址空间"
"从high_memory开始的128M(4G-high_memory)就是用作用来映射剩下的大于896M的内存的"

与楼主探讨,vamalloc()返回的地址(内核虚拟地址)在此128M的区域内,但分配的物理页框并不一定非在>896m的物理地址空间内!

因为具体分配是由buddy system负责的,分配得来的物理页框有可能在物理地址空间的任意位置!
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP