• 小学
  • 初中
  • 高中
  • 小升初
  • 中考
  • 高考
  • 英语
  • 考研
  • 四六级
  • 单元
  • 节日
  • 母爱
  • 诚信
  • 父爱
  • 环保
  • 家庭
  • 感动
  • 成长
  • 感恩
  • 梦想
  • 爱国
  • 写景
  • 写人
  • 叙事
  • 状物
  • 议论
  • 说明
  • 抒情
  • 观后感
  • 诗歌
  • 读后感
  • 想象
  • 素材
  • 名言
  • 段落
  • 哲理
  • 诗词
  • 成语
  • 赏析
  • 基础
  • 演练
  • 教学
  • 操作系统的高端内存 高端内存页返回的地址

    时间:2018-07-19 19:23:21 来源:天一资源网 本文已影响 天一资源网手机站

    -

    一 为什么我们需要高端内存

    我们知道在x86_32架构下,linux中的进程的虚拟地址空间大小是4GB,其中的用户空间占用其中的低3GB,而内核空间占用其中的高1GB。而实际上内核的物理空间是从地址0开始的。所以 PA = VA - 0xC000 0000。

    从下图可以得出:

    虚拟地址 物理地址

    0xC000 0000 0x0000 0000

    0xFFFF 8FFF 0x3FFF 8FFF

    0xFFFF FFFF 0x4000 0000

    操作系统的高端内存

    这里就出现了问题,内核空间只能映射到前1GB的物理空间,为了解决这个问题。内核将每个节点的物理内存空间分成了三个部分:zone_dma zone_normal zone_highmem。zone_dma和zone_normal占用其中的896MB,而 zone_highmem占用的是》896MB的空间。而内核虚拟地址空间的高128MB用来专门映射高端内存。不过这种映射是动态的,也就是说该区域没有办法永久映射到内核的虚拟地址空间。

    操作系统的高端内存

    二 建立高端内存的映射

    操作系统的高端内存

    1 永久内核映射

    1.1 数据结构

    1 page_address_htable

    在函数page_address()中,为了加速从页框指针到线性地址的转换,内核使用哈希表保存页框指针和线性地址的关系。桶中的每一项都是一个page_address_map结构。

    操作系统的高端内存

    static struct page_address_slot {

    struct list_head lh; /* List of page_address_maps */

    spinlock_t lock; /* Protect this bucket's list */

    } page_address_htable[1《《PA_HASH_ORDER];

    struct page_address_map {

    struct page *page;

    void *virtual;

    struct list_head list;

    };

    2 pkmap_count数组

    永久映射区间的起始线性地址为 PKMAP_BASE。内核利用主内核页目录表( swapper_pg_dir)的中的一个页表来建立永久映射,该页表有指针

    pte_t * pkmap_page_table 来表示。页表中的页表项个数有宏LAST_PKMAP表示。PAE开启时,页表项个数为1024,反之则为512。

    与临时映射不同为了保证映射的持久,内核建立了一个数组 int pkmap_count[LAST_PKMAP],该数组元素的个数就是页表项的个数。数组是一个计数器的集合。

    count = 0 : 表示该页表项可用,相关映射还未建立,在TLB刷新前,TLB还没有相关页表项的存在。

    count = 1 : 表示该页目前没有映射到任何页框,但是TLB中的上次映射的表项还没有被flush。所以该页的映射无法创建。简单来说,就是该映射还存储在TLB中

    count = n : 表示相关的页表项已经建立,并且有 n-1 个进程在使用该映射。

    当然,仅仅一个计数器还不够,为了防止对页表项的并发访问,创建映射的过程需要用锁进行控制。

    永久映射由 kmap(struct page *page) 创建,该函数接收参数page作为被映射的页框指针。该函数返回一个线性地址。kmap的核心是函数kmap_high()

    3 kmap_high

    kmap_high 先调用page_address得到页框对应的线性地址

    如果该页框还没有被映射,则调用 map_new_virtual。在map_new_virtual中,如果发现一个count为0的映射,则将count置为1,随后将count加一,此时,count值等于2

    否则不调用mapp_new_virtual,直接将count加一,此时count应该大于2

    void fastcall *kmap_high(struct page *page)

    {

    unsigned long vaddr;

    // 申请kmap_lock自旋锁

    spin_lock(&kmap_lock);

    // 检查页框是否已经被映射

    vaddr = (unsigned long)page_address(page);

    // 如果还没有被映射,调用 map_new_virtual创建映射 返回一个新的线性地址

    if (!vaddr)

    vaddr = map_new_virtual(page);

    pkmap_count[PKMAP_NR(vaddr)]++;

    if (pkmap_count[PKMAP_NR(vaddr)] 《 2)

    BUG();

    spin_unlock(&kmap_lock);

    return (void*) vaddr;

    }

    4 map_new_virtual

    当一个页框还没有被映射到一个虚拟页时,就会调用map_new_virtual。为了防止对pkmap_count数组的重复遍历,函数使用last_pkmap_nr记录上次映射结束时,页表项的索引。map_new_virtual其实大致上做了三件事:

    第一,如果pkmap_count中有计数器为0的索引,则建立映射并令其count = 1。

    第二,如果last_pkmap_nr=0,也就是整个页表没有可用的页表项了,则调用flush_all_zero_pkmaps 将所有的计数器为1 的映射(也就是说映射仅仅在TLB中)的计数器置为0,冲刷TLB。

    第三,如果pkmap_count都大于1,则阻塞当前进程,将当前进程状态置为 TASK_UNINTERRUPTIBLE 并加入等待队列。之后调度其他进程,其他进程的时间片完后,再将原进程从等待队列移出 。如果当前没有其他进程映射该页框,则进行下次循环。

    map_new_virtual等价于以下代码(摘自ULK)

    static inline unsigned long map_new_virtual(struct page *page)

    {

    unsigned long vaddr;

    int count;

    for (;;) {

    int count;

    //声明一个阻塞队列

    DECLARE_WAITQUEUE(wait, current);

    //扫描页表中的每一个页表项

    for (count = LAST_PKMAP; count 》 0; --count) {

    //last_pkmap_nr 是函数上次结束时,页表项的索引。

    //这里使用了一些小诡计 等同于 last_pkmap_nr = (last_pkmap_nr + 1) % (LAST_PKMAP) 但是使用位运算更快

    last_pkmap_nr = (last_pkmap_nr + 1) & (LAST_PKMAP - 1);

    if (!last_pkmap_nr) {

    flush_all_zero_pkmaps( );

    count = LAST_PKMAP;

    }

    如果last_pkmap_nr指向的页表项可用

    if (!pkmap_count[last_pkmap_nr]) {

    unsigned long vaddr = PKMAP_BASE + (last_pkmap_nr 《《 PAGE_SHIFT);

    //创建并插入页表项,对应页的属性为0110_0011 即 G D A PCD PWT US RW P = 0110_0011

    set_pte(&(pkmap_page_table[last_pkmap_nr]), mk_pte(page, __pgprot(0x63)));

    pkmap_count[last_pkmap_nr] = 1;

    //插入一个新元素到哈希表page_address_htable并返回线性地址

    set_page_address(page, (void *) vaddr);

    return vaddr;

    }

    }

    //如果没有找到可用的页表项则

    current-》state = TASK_UNINTERRUPTIBLE;

    add_wait_queue(&pkmap_map_wait, &wait);

    spin_unlock(&kmap_lock);

    schedule( );

    remove_wait_queue(&pkmap_map_wait, &wait);

    spin_lock(&kmap_lock);

    if (page_address(page))

    return (unsigned long) page_address(page);

    }

    }

    2 临时内核映射

    2.1

    临时内核映射又称为原子映射,这里先抛个问题:为什么临时映射要称作原子映射。

    临时内核映射区域位于固定映射区内,固定映射区内的线性地址可以随意映射到任意一个物理地址,而不是使用 物理地址 = 线性地址 - 0xC000_0000 得到。

    临时内核映射区的起始和终止的线性地址的索引(关于什么是线性地址的索引,后面会说明)由 enum fixed_address 中的常量 FIX_KMAP_BEGIN FIX_KMAP_END 分别指定。其中FIX_KMAP_END = FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1。内核根据CPU核心数划分临时映射区。

    操作系统的高端内存

    临时内核映射区的结构

    而在每个CPU独有的块内部又根据页面的用途分成了13个窗口,举个例子 KM_USER0 和 KM_USER1就是内核用来存储来自用户上下文的(通常是系统调用传递的局部变量和参数)。 每个窗口其实就是一个页面。这13个窗口在内核中由 enum km_type 表示,而每个窗口的线性地址由km_type 中的常量作为索引来计算。KM_TYPE_NR是窗口的分类个数,等于13。于是,临时映射区变成了这样:

    操作系统的高端内存

    临时映射由kmap_atomic 创建相比于 kmap,kmap_atomic 不阻塞当前进程,不刷新TLB,从而带来了速度上的提升。但是由于kmap_atomic并不阻塞当前进程,如果同一个CPU 上先后有两个进程都要在同一个window上建立映射,并且前一个进程还没有释放映射,那么后一个进程创建的映射就会覆盖前一个进程所创建的映射(其实质是页表项的覆盖)。所以必须原子性的创建和释放映射,这就是kmap_atomic名字的由来。

    2.2 kmap_atomic

    kmap_atomic接收两个参数,page是被映射的页面指针,type表明此次映射位于临时区间的那个window。

    函数返回一个线性地址。

    void *kmap_atomic(struct page *page, enum km_type type)

    {

    //idx是用来完成线性地址转换的索引

    enum fixed_addresses idx;

    //vaddr用来保存映射建立后的线性地址

    unsigned long vaddr;

    /* even !CONFIG_PREEMPT needs this, for in_atomic in do_page_fault */

    //将task_struct的preempt_count字段加一

    inc_preempt_count();

    //如果被映射的页框不在高端内存 则直接利用__va 计算该页框的线性地址

    //__va(page_to_pfn(page) 《《 PAGE_SHIFT);

    //先获取该页框的页框号根据公式 pn * 4K(页大小) 得到页框的物理地址(PA) 再用 PA + 0xC000 0000 得到线性地址

    if (!PageHighMem(page))

    return page_address(page);

    /**如果该页框位于高端内存

    /* 以下公式需要说明一下, smp_processor_id指明映射发生即当前进程所在的CPU ID type指明了本次映射发生在那个window

    /*根据该公式我们就能得到映射实际发生的索引,注意该索引只是一个相对索引

    idx = type + KM_TYPE_NR*smp_processor_id();

    vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);

    //设置页表项

    set_pte(kmap_pte-idx, mk_pte(page, kmap_prot));

    //刷新TLB

    __flush_tlb_one(vaddr);

    return (void*) vaddr;

    }

    2.3 __fix_to_virt 宏

    关于__fix_to_virt需要重点说明一下,__fix_to_virt宏将索引转换为线性地址,注意此处使用的是位于固定映射区间的绝对索引FIXADDR_TOP 是固定映射区间的结束线性地址。固定映射位于线性地址 FIXADDR_START 与 FIXADDR_TOP之间,FIXADDR_TOP = 0xFFFF_F000 。在固定映射区间与虚拟地址空间的顶端(4G)之间还有一个1个页大小的空洞称为 FIX_HOLE ,更重要的是固定映射区间是向下拓展的(类似于栈)。

    内核中使用宏 #define __fix_to_virt(x) (FIXADDR_TOP - ((x) 《《 PAGE_SHIFT)) 完成从索引到线性地址的转换,结合下图可得区域 FIX_VSYSCALL的起始线性地址为

    0xFFFF_E000 = 0xFFFF_F000 - 1 * 0x1000

    操作系统的高端内存

    enum km_type {

    D(0) KM_BOUNCE_READ,

    D(1) KM_SKB_SUNRPC_DATA,

    D(2) KM_SKB_DATA_SOFTIRQ,

    D(3) KM_USER0,

    D(4) KM_USER1,

    D(5) KM_BIO_SRC_IRQ,

    D(6) KM_BIO_DST_IRQ,

    D(7) KM_PTE0,

    D(8) KM_PTE1,

    D(9) KM_IRQ0,

    D(10) KM_IRQ1,

    D(11) KM_SOFTIRQ0,

    D(12) KM_SOFTIRQ1,

    D(13) KM_TYPE_NR

    };

    fixmap.h

    #ifdef CONFIG_HIGHMEM

    FIX_KMAP_BEGIN, /* reserved pte's for temporary kernel mappings */

    FIX_KMAP_END = FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1,

    #define __FIXADDR_SIZE (__end_of_permanent_fixed_addresses 《《 PAGE_SHIFT)

    #define FIXADDR_START (FIXADDR_TOP - __FIXADDR_SIZE)

    #define __fix_to_virt(x) (FIXADDR_TOP - ((x) 《《 PAGE_SHIFT))

    #define FIXADDR_TOP ((unsigned long)__FIXADDR_TOP)

    #define __FIXADDR_TOP 0xfffff000

    相关关键词: 操作系统的高端内存
    相关热词搜索: 操作系统的高端内存 操作系统内存管理 操作系统内存分配算法

    • 范文大全
    • 教案下载
    • 优秀作文
    • 励志
    • 课件
    • 散文
    • 名人名言