高端内存页框的内核映射

from:高端内存页框的内核映射


与直接映射的物理内存末端,高端内存的始端所对应的线性地址存放在high_memory变量中,它被设置为896MB。896MB边界以上的页框并不映射再内核线性地址空间的第4个GB,因此,内核不能直接访问它们。这就意味着,返回所分配页框线性地址的页分配器函数不适用于高端内存,即不适用于ZONE_HIGHMEM内存管理区的页框。

之前也提到过,假定内核调用__get_free_pages(GFP_HIGHMEM, 0),则在高端内存区确实申请并分配了一个页框,但是__get_free_pages()不能返回它的线性地址,因为它根本就不存在,所以返回NULL1

在32位平台上,必须让内核使用者用所有可以使用的RAM,达到PAE所支持的64GB,所以采用如下方法。

  • 高端内存页框的分配只能通过alloc_pages()函数和它的快捷函数alloc_page()。这些函数不返回第一个被分配的页框的线性地址,因为如果该页属于高端内存,那么这样的线性地址根本不存在。取而代之,这些函数放回第一个被分配页框的页描述符(page)的线性地址,这些线性地址总是存在的2

  • 没有线性地址的高端内存中的页框不能被内核访问,因此,内核线性地址空间最后128MB中的一部分专门用于映射高端内存页框3。当然,这种映射是暂时的,否则只有128MB的高端内存可以被访问。取而代之,通过重复使用线性地址,使得整个高端内存能够在不同的时间被访问。

内核可以采用三种不同的机制将页框映射到高端内存,分别叫做永久内核映射、临时内核映射和非连续内存分配。

永久内核映射


建立永久内核映射可能阻塞当前进程,这发生在空闲页表不存在时,页就是高端内存上没有页表项可以用作页框使用时。因此,永久内核映射不能用于中断处理程序和可延迟函数。相反,建立零食内核映射绝不会要求阻塞当前进程。

永久内核映射允许内核建立高端页框到内核地址空间的长期映射,它们使用主内核页表中一个专门的页表,其地址存放在pkmap_page_table变量中。页表中的表项数由LAST_PKMAP宏产生。页表包含512或1024项,这取决于PAE机制是否被激活。因此,内核最多一次性访问2M或4M的高端内存。

页表映射的线性地址从PKMAP_BASE开始,pkmap_count数组包含LAST_PKMAP个计数器,pkmap_page_table页表中的每一个项都有一个。计数器可能为0、1或大于1。

<mm/highmem.c>



pte_t * pkmap_page_table;


如果计数器为0,则说明对应的页表项没有映射任何高端内存,所以是可用的。

如果计数器为1,则说明对应的页表项没有映射任何高端内存,但是不能被使用,因为自从它最后一次使用以来,其TLB表项还未被刷新。

如果计数器大于1,则说明映射一个高端内存页框,这意味着正好有n-1个内核成分在使用这个页框。

为了记录高端内存页框与永久内核映射包含的线性地址之间的联系,内核使用page_address_htable做散列表,它使用page_address_map数据结构用于为高端内存中的每一个页框进行映射。

<mm/highmem.c>



struct page_address_map {
    struct page *page;
    void *virtual;
    struct list_head list;
};


page_address()函数返回页框对应的线性地址,如果页框在高端内存中并且没有被映射,则返回NULL。

<mm/highmem.c>

void *page_address(struct page *page)
{
    unsigned long flags;
    void *ret;
    struct page_address_slot *pas;

    if (!PageHighMem(page))
        return lowmem_page_address(page);

    pas = page_slot(page);
    ret = NULL;
    spin_lock_irqsave(&pas->lock, flags);
    if (!list_empty(&pas->lh)) {
        struct page_address_map *pam;

        list_for_each_entry(pam, &pas->lh, list) {
            if (pam->page == page) {
                ret = pam->virtual;
                goto done;
            }
        }
    }
done:
    spin_unlock_irqrestore(&pas->lock, flags);
    return ret;
}


我们可以从上面的函数可以看出,如果页框不在高端内存中,就通过lowmem_page_address返回线性地址。如果在高端内存中,则通过函数page_slotpage_address_htable中查找,如果在散列表中查找到,就返回线性地址。

kmap()用来建立内存区映射,代码如下:

<arch/arm/x86/mm/highmem_32.c>

void *kmap(struct page *page)
{
    might_sleep();
    if (!PageHighMem(page))
        return page_address(page);
    return kmap_high(page);
}


本质上如果是高端内存区域,则使用kmap_high()函数用来建立高端内存区的永久内核映射,代码如下:

<include/linux/highmem.h>



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;
}


临时内存映射


虽然不像永久内存映射那样会阻塞当前进程,但缺点时只有很少的临时内核映射可以同时建立起来。临时内存映射的内核控制路径必须保证当前没有其他的内核控制路径在使用同样的映射。这意味着内核控制路径永远不能被阻塞,否则只其他的内核控制路径有可能使用同一个窗口来映射其他的高端内存页。

临时内核映射比永久内核映射要简单,此外,它们可以用在中断处理程序和可延迟函数的内部,因为它们从不阻塞当前进程。在高端内存的仁一页框都可以通过一个『窗口』映射到内核地址空间。留给临时内核映射的窗口是非常少的。

每个CPU都有它自己包含的13个窗口集合,它们用enum km_type数据结构表示。

<include/asm-generic/kmap_types.h>



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(8)   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,
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
};


这个数据结构中定义的每一个符号都标识了窗口的线性地址。

为了建立临时内核映射,内核调用kmap_atomic()函数。在后来的内核代码中,kmap_atomic()函数只是使用了kmap_atomic_prot

<arch/arm/x86/mm/highmem_32.c>

void *kmap_atomic_prot(struct page *page, enum km_type type)
{
    unsigned int idx;
    unsigned long vaddr;
    void *kmap;

    pagefault_disable();
    if (!PageHighMem(page))
        return page_address(page);

    debug_kmap_atomic(type);

    kmap = kmap_high_get(page);
    if (kmap)
        return kmap;

    idx = type + KM_TYPE_NR * smp_processor_id();
    vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);
#ifdef CONFIG_DEBUG_HIGHMEM    BUG_ON(!pte_none(*(TOP_PTE(vaddr))));
#endif    set_pte_ext(TOP_PTE(vaddr), mk_pte(page, kmap_prot), 0);
    local_flush_tlb_kernel_page(vaddr);

    return (void *)vaddr;
}

  1. 在64位的硬件平台上不存在这个问题。

  2. 因为页描述符一旦被分配在地段内存中,它们在内核初始化阶段就不会变。

  3. 所以虽然我们有896MB的空间可以分配页框,但实际上并没有这么多的地址空间。