Linux内存管理:HighMemory

HighMemory介绍

Linux一般把整个4GB可以map的内存中的1GB用于低端内存。从0xC0000000开始的话(CONFIG_PAGE_OFFSET配置),低端内存的地址范围就是0xC0000000到high_memory地址。
high_memory = __va(arm_lowmem_limit - 1) + 1,arm_lowmem_limit也是0xff00000减去vmalloc大小什么的算出来的,和vmalloc_min一样。所以可以直接map的lowmemory小于1GB。如果vmalloc区域等于340MB的话,大小一般也就600多MB。

high_memory = __va(arm_lowmem_limit - 1) + 1

static void * __initdata vmalloc_min =
    (void *)(VMALLOC_END - (240 << 20) - VMALLOC_OFFSET);

#define VMALLOC_OFFSET      (8*1024*1024)
#define VMALLOC_START       (((unsigned long)high_memory + VMALLOC_OFFSET) & ~(VMALLOC_OFFSET-1))
#define VMALLOC_END     0xff000000UL

high_memory保存了高端内存的开始地址。
Linux内存管理:HighMemory_第1张图片

那如果用大于1GB内存怎么办呢?这时候如果不想把大于1GB的内存浪费掉,就需要定义CONFIG_HIGHMEM。
但Highmemory区域是不会在内核初始化的时候,直接map到内存可以访问的。
内核采用三种不同的机制将高端内存的页框映射过来。分别叫做临时内核映射,永久内核映射以及非连续内存分配(vmalloc)。

永久内核映射

http://blog.csdn.net/xiaojsj111/article/details/11817587
kmap()函数建立永久内核映射。
这种映射的建立,可能会发生阻塞,不适用于中断上下文(中断处理程序及可延迟函数)
函数kmap()被用来建立永久内核映射,如下所示,该代码的核心在函数kunmap_high()中,所用到的数据结构有

  • 哈希数组page_address_htable,数组每个元素都是一个链表,链表的每个节点中都存放了一个页描述符指针及相应的虚拟地址
  • pte_t * pkmap_page_table,主内核页表中专门用于永久内核映射的页表项数组,其大小由LAST_PKMAP指出,如果不使用扩展物理内存,其值为1024,正好是占一页页表。
  • int pkmap_count[LAST_PKMAP],与每个用于永久内核映射的页表项是一对一的关系
    • 0,对应页表项没有映射任何高端内存,并且是可用的
    • 1,对应页表项没有映射任何高端内存,但不可用,因最后一次被使用后,相应的TLB表还没有被刷新
    • 大于1,n-1个内核成分在使用对应的页表项
  • PKMAP_BASE,数组pkmap_page_table中第一个页表项所对应的虚拟地址

函数kunmap_high()调用了函数page_address(),对该页描述符所代表的高端内存,在哈希数组page_address_htable中,查找其相应的虚拟地址,找不到,则返回NULL,然后调用函数map_new_virtual()。

函数map_new_virtual()全部代码如下,其使用数组pkmap_count查找空闲可用的页表项,找到后,设置该页表项指向函数void *kmap(struct page *page)的实参所引用的页框,同时,在哈希数组page_address_htable的某个元素链表上添加节点,以记录该页描述符所对应的虚拟地址。另外,要注意的是,找不到空闲可用的页表项时,会睡眠等待。

Linux内存管理:HighMemory_第2张图片

如果是通过 alloc_page() 获得了高端内存对应的 page,如何给它找个线性空间?
内核专门为此留出一块线性空间,从 PKMAP_BASE 到 FIXADDR_START,用于映射高端内存。在 2.4 内核上,这个地址范围是 4G-8M 到 4G-4M 之间。这个空间起叫“内核永久映射空间”或者“永久内核映射空间”。
这个空间和其它空间使用同样的页目录表,对于内核来说,就是 swapper_pg_dir,对普通进程来说,通过 CR3 寄存器指向。
通常情况下,这个空间是 4M 大小,因此仅仅需要一个页表即可,内核通过来 pkmap_page_table 寻找这个页表。通过 kmap(), 可以把一个 page 映射到这个空间来。由于这个空间是 4M 大小,最多能同时映射 1024 个 page。因此,对于不使用的的 page,及应该时从这个空间释放掉(也就是解除映射关系),通过 kunmap() ,可以把一个 page 对应的线性地址从这个空间释放出来。

PKMAP_BASE,PKMAP_SIZE这两个定义了PKMAP区域的开始地址和大小

在Highmemory区域里分配的page,需要map一下内核才能访问。以下看一下几个相关的接口函数。

//这个函数检查当前page是否是lowmemory,如果是的话就调用page_address()把当前page的地址转成虚拟地址返回。但如果是高端内存的page,则需要用到PKMAP区域重新map一下,才能让内核进行访问。这样map的page释放的时候必须使用kumap来释放。由于这种map用到的PKMAP区域大小有限,建议不要占据这种内存太长时间(ldd3书里边写)?
void *kmap(struct page *page)
{
    might_sleep();
    //当前内存可能会进入睡眠,如果在atomic上下文调用kmap函数,
    //migh_sleep函数会打印stack trace
    //这个函数需要使能CONFIG_DEBUG_ATOMIC_SLEEP

    if (!PageHighMem(page))
        return page_address(page);
    return kmap_high(page);
}

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

//如果是highmemory的page,则会调用如下函数
static inline unsigned long map_new_virtual(struct page *page)
{
    unsigned long vaddr;
    int count;

start:
    count = LAST_PKMAP;
    /* Find an empty entry */
    for (;;) {
        //last_pkmap_nr保存上一次map过的值,从这个值开始寻找。如果二级页表是512的话,最大只能到512
        //所以与了一个LAST_PKMAP_MASK。
        //如果二级页表是512的话,就可以猜到pkmap区域的大小应该至少是2MB,
        //http://blog.csdn.net/hongzg1982/article/details/47341881 

        //pkmap_count[]标志总共512个里边,哪个是可以map的。 
        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
         */
        { //如果找不到剩余的,则先进入睡眠,等有kumap的时候再醒来继续寻找。
          //这里用的waitqueue的方式,也可以看一下 
            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);
    //highmemory的虚拟地址是PKMAP_BASE +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;
}

kmap()可以看到,在分配不到的时候,可能会进入睡眠。
那还有一种kmap_atomic()接口是比kmap更为高效且不进入睡眠的,可以在atomic上下文进行调用的。
下面看一下其实现:

void *kmap_atomic(struct page *page)
{
    unsigned int idx;
    unsigned long vaddr;
    void *kmap;
    int type;

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

#ifdef CONFIG_DEBUG_HIGHMEM
    /*
     * There is no cache coherency issue when non VIVT, so force the
     * dedicated kmap usage for better debugging purposes in that case.
     */
    if (!cache_is_vivt())
        kmap = NULL;
    else
#endif
        kmap = kmap_high_get(page);
    if (kmap)
        return kmap;

    type = kmap_atomic_idx_push();

    idx = type + KM_TYPE_NR * smp_processor_id();
    vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);
#ifdef CONFIG_DEBUG_HIGHMEM
    /*
     * With debugging enabled, kunmap_atomic forces that entry to 0.
     * Make sure it was indeed properly unmapped.
     */
    BUG_ON(!pte_none(get_top_pte(vaddr)));
#endif
    /*
     * When debugging is off, kunmap_atomic leaves the previous mapping
     * in place, so the contained TLB flush ensures the TLB is updated
     * with the new mapping.
     */
    set_top_pte(vaddr, mk_pte(page, kmap_prot));

    return (void *)vaddr;
}

临时内核映射

临时内核映射比永久内核映射的实现要简单;此外它可以在中断处理程序和可延迟函数的内部,因为这个临时内核映射函数从来不阻塞当前进程。为了建立临时内核映射,内核调用kmap_atomic()函数。
内核在 FIXADDR_START 到 FIXADDR_TOP 之间保留了一些线性空间用于特殊需求。这个空间称为“固定映射空间”
在这个空间中,有一部分用于高端内存的临时映射。

分配page或者free page的时候会判断是不是highmemory。

highmemory的大小:
memblock或者meminfo()里边,highmemory的最大地址减去arm_lowmem_limit的地址之后算出来的大小就是highmemory的大小。比如像下面log这样,最后一段count = 3的区域为highmemory区域。其size为768MB,但highmemory开始地址小于arm_lowmem_limit,所以减去arm_lowmem_limit - reg->base的虚拟地址(18MB),highmemory的大小就是750MB。这个大小与/proc/meminfo里边读出来的HighTotal的大小是一致的。

<5>[0.000000]  [0:swapper:0] arm_lowmem_limit = 0xf1200000 
<6>[0.000000]  [0:swapper:0] count = 1 , reg->base =0x80000000 , reg->size =0x5500000
<6>[0.000000]  [0:swapper:0] count = 2 , reg->base =0x8cb00000 , reg->size =0x23200000
<6>[0.000000]  [0:swapper:0] count = 3 , reg->base =0xb0000000 , reg->size =0x30000000

然后再arm_bootmem_free里边

#ifdef CONFIG_HIGHMEM
    zone_size[ZONE_HIGHMEM] = max_high - max_low;//可以在find_limits函数里边找到max_high和max_low的定义
#endif
//max_high就是high memory最大地址对应的pfn,max_low是lowmemory的最大地址对应的pfn

用户空间映射:
虽然内核地址空间有限,但是每个进程的用户地址空间都可以达到3G,Highmem的页框可以不受限制的映射到用户线性地址空间。当访问用户地址空间地址发生缺页异常时,内核的page allocator会优先从highmem zone分配页面,只有当highmem zone没有足够的空闲页面时,才会选择Normal或者DMA zone进行分配。

因此highmem内存的主要使用者是应用进程的页面映射,内核kernel通过pkmap fixmap方式,同时使用的Highmem内存,理论上最多2MB/4MB + 3.xMB;由于Highmem的存在,使得应用地址空间缺页异常处理,文件映射,堆分配等操作优先使用highmem zone的内存,减轻了Normal zone的分配压力,某种程度上避免了Normal区的碎片化。我们甚至可以禁止用户空间地址的HIGH_MEM分配使用Normal zone和 DMA zone,使得Normal DMA只用于内核地址空间内存的分配,尽量减少碎片化,避免内存分配失败。我想这就是HighMem存在的意义吧。

非连续内存区管理 vmalloc

如果一段内存不是很频繁访问,那么通过连续的线性地址来访问非连续的页框这样一种分配模式就会有很大的意义。这种模式的优点是避免了外碎片(??),而缺点是必须重新建立页表。
显然通过vmalloc分配的内存大小必须是4096字节,也就是page大小的倍数!!!

如下图可以看到vmalloc开始和结束分别与high_memory与PKMAP_BASE大小相关,但一般都会插入一个8MB的区域,目的是为了”捕获”对内存的越界访问。
出于同样理由,vmalloc区域之间也会留下4kB的安全区来隔离非连续的内存区
Linux内存管理:HighMemory_第3张图片

这种方式很简单,因为通过 vmalloc() ,在”内核动态映射空间”(上图的VMALLOC_START到VMALLOC_END)申请内存的时候,就可能从高端内存获得页面(参看 vmalloc 的实现),因此说高端内存有可能映射到”内核动态映射空间” 中。
vmalloc区域的起始线性地址是VMALLOC_START,结束线性地址是VMALLOC_END。每个vmalloc内存区都对应着一个vm_struct数据结构。

struct vm_struct {
    struct vm_struct    *next;
    void            *addr;
    unsigned long       size;
    unsigned long       flags;
    struct page     **pages;
    unsigned int        nr_pages;
    phys_addr_t     phys_addr;
    const void      *caller;
};

 - addr,第一个内存单元的线性地址
 - size,内存区的大小加4096的安全区
 - pages,这个数组中存放的是被映射的物理页的页框描述符
 - nr_pages,数组pages的维数
 - phy_addr,除非内存被用来映射一个硬件设备的IO共享内存,否则为0
 - next,所有的非连续内存区的vm_struct描述符都通过该字段链接在全局变量vmlist中
 - flags,
     - VM_ALLOC,使用vmalloc()分配的非连续内存区
     - VM_MAP,使用vmap()映射已经分配好的页框
     - VM_IOREMAP,使用ioremap()映射的硬件设备上的内存

使用vmalloc(size)分配非连续的内存区,该函数的核心是__vmalloc(size, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL),下面介绍该函数

1) 使用kmalloc(sizeof(vm_struct), GFP_KERNEL),根据vm_struct数据结构的大小,从几何分布的
slab中为vm_struct分配内存,并在线性地址从VMALLOC_START到VMALLOC_END之间寻找一块空闲区域,大小至
少为size+4096,找到后,用该段线性地址的起始值初始化vm_struct->addr,并初始化vm_struct->flags,且将vm_struct->size记为size+4096

2)  初始化vm_struct->nr_pages为(size >> PAGE_SHIFT),然后,为页描述符指针数组
vm_struct->pages分配空间,需要分配的大小是
(vm_struct->nr_pages*sizeof(struct page *)),若数组占用内存大于一页,则使用__vmalloc(array_size, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL)分配,否则,使用
kmalloc(array_size, (GFP_KERNEL | __GFP_HIGHMEM& ~__GFP_HIGHMEM))分配。这
里需注意的是,所谓一页的安全区只是指线性地址需要,实际的页框中并没有

3) 使用函数alloc_page(GFP_KERNEL | __GFP_HIGHMEM)(单个页框分配函数)分配
vm_struct->nr_pages个页框,将页描述符记录在数组vm_struct->pages中

4)接下来,就是要修改内核页表,以表明非连续内存区的每个页框都对应着一个线性地址,使用的
函数是map_vm_area(area, PAGE_KERNEL,&vm_struct->pages),该函数先是通过
pgd_offset(&init_mm, vm_struct->addr)得到主内核页全局目录pgd,然后,为每段4KB大小的线性地址(除却最后4KB用作安全区的的线性地址),
分配所需的各级页中间目录项,并最终建立页表项,并将每个页描述符对应的页的物理地址,
连同PAGE_KERNEL标志设置到相应的页表项中。注意,PAGE_KERNEL等同于(_PAGE_PRESENT | _PAGE_RW | _PAGE_DIRTY | _PAGE_ACCESSED | _PAGE_NX)

需要注意的是,vmalloc(size)并未触及当前进程的页表,因此,内核态的进程访问非连续内存区时,由于在进程页表中找不到对应的表项,所以,缺页异常发生,然后,缺页处理程序发现这个缺页线性地址在主内核页表中,所以,就把主内核页表中相应的值拷贝到进程的页表中,最后恢复进程的执行,详见 缺页异常??????
使用vfree(void *addr)来释放非连续的内存区,该函数

- 调用remove_vm_area(void *addr),根据线性地址addr在vmlist中找到vm_struct,并清除该非连续内存区中的线性地址对应的所有的内核的页表项,注意,这里只清楚了页表项,不清楚各级页中间目录项,因为内核永远也不会回收扎根于主内核页全局目录中的页上级目录、页中间目录和页表。
- 调用函数__free_page(),将vm_struct->pages数组中的每个页归还到页框分配器,然后,调用vfree(area->pages)(该数组占用空间大于4096KB时)或kfree(vm_struct->pages)释放这个数组本身
- 调用kfree(vm_struct),释放数据结构vm_struct

你可能感兴趣的:(Linux内核架构)