注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

Tsecer的回音岛

Tsecer的博客

 
 
 

日志

 
 

高端内存及用户态获得内存的初始化  

2013-09-08 23:25:58|  分类: Linux内核 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
一、高端内存
为了内存页面快速访问,避免内核态出现缺页异常,内核将物理页面和逻辑页面进行简单的对等映射,也就是将物理页面按照编号逐一映射到内核逻辑地址3G开始的逻辑地址空间中。但是如果系统的物理地址超过1G之后,此时内核占用的3G到4G逻辑地址空间无法映射到超过1G的物理页面。
如果内核将所有1G逻辑地址都和物理内存的1G地址做映射,那么此时内核逻辑空间将没有任何周转的余地,对于大于1G的内存也就没有办法使用。此时如果内核希望使用或者操作这些页面就会遇到问题,例如内核想清空一个页面的内容,此时就无法通过逻辑地址来完成对页面的清除操作。
还有一些设备驱动的地址,例如一些显卡的内存地址,或者一些PCI类型设备,这些设备可能存在大片的内存,这些内存和物理内存一样,占用相同的数据总线地址,访问时需要使用设备的物理地址来访问。和物理页面一样,它们也面临着在保护模式下地址访问的问题,所以此时也需要建立页面的映射,同样需要逻辑地址空间。
在内核的运行过程中,大部分数据结构的申请都是通过特定结构的slab来申请,但是也存在一些小的零散结构或者小的临时空间的需要,作用相当于用户态的malloc需求,此时内核也需要管理这些零散的地址。和使用page为单位分配相比,vmalloc有更好的用户体验,它面向的是结构大小,不需要知道page这种相对底层的结构,下面是内核中一个使用的例子
linux-2.6.21\net\ipv4\netfilter\arp_tables.c
static int do_replace(void __user *user, unsigned int len)
    counters = vmalloc(tmp.num_counters * sizeof(struct xt_counters));
……
    vfree(counters);
    xt_table_unlock(t);
    return ret;
由于内核堆栈比较小,最大8KB,所以这样的结构不能放在堆栈中,在函数的开始申请空间,退出时释放。
简单的说:物理内存如果位置在1G-128M之上的都是属于物理意义上的高端内存,而内核中3G+1G-128M之上的都算作是逻辑上的高端内存空间,内核这个地址空间中的地址使用的页面和物理地址没有确定的对应关系,对于新分配的页面,如果它在物理地址的高端内存中,在没有主动完成映射之前,无法访问该页面内容。
二、高端内存的使用
高端内存对内核使用不太方便,但是对用户来说不存在这种感觉,因为毕竟内核并不是用这个页面中的内容,只要将页表设置到内存的物理地址即可。所以在用户态申请页面的时候,内核从来都是把高端内存作为选择范围的。
在一个典型的文件映射的页面缺页处理函数filemap_nopage中,对于页面的分配流程为
page_cache_read--->>page_cache_alloc_cold
static inline struct page *page_cache_alloc_cold(struct address_space *x)
{
    return __page_cache_alloc(mapping_gfp_mask(x)|__GFP_COLD);
}
这里使用到的mapping_gfp_mask(x)结构在inode分配时初始化:
static struct inode *alloc_inode(struct super_block *sb)
mapping_set_gfp_mask(mapping, GFP_HIGHUSER);
#define GFP_HIGHUSER    (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL | \
             __GFP_HIGHMEM)
可以看到,高端内存始终是作为用户态文件页面内存存储的候选项。
三、高端内存的清零
上面说的是文件读取操作,在读取操作的过程中,由于内容是用来存储文件内容的,所以这个页面没有必要清零,因为马上会有文件数据来覆盖,反过来如果用户系统获得一些动态内存,例如C库的malloc就是通过mmap向系统批发内存页面。这种批发的页面相当于申请了一定额度的贷款,事实上并没有到帐,只有在用户实际使用的时候才会真正分配。
现在我们可以看一下对于通过mmap分配的页面,它的页面内容是否为确定值,纯粹内存访问在do_anonymous_page函数中完成
if (write_access) {
        /* Allocate our own private page. */
        pte_unmap(page_table);

        if (unlikely(anon_vma_prepare(vma)))
            goto oom;
        page = alloc_zeroed_user_highpage(vma, address);
从名字上看,这个页面申请的时候是要求被清零的,并且可以是高端内存,此时我们感兴趣的时内核如何完成对这个页面的清零操作。
alloc_zeroed_user_highpage---->>alloc_page_vma---->>>__alloc_pages---->>>get_page_from_freelist---->>>buffered_rmqueue--->>prep_new_page
    if (gfp_flags & __GFP_ZERO)
        prep_zero_page(page, order, gfp_flags);
prep_zero_page--->>>clear_highpage
static inline void clear_highpage(struct page *page)
{
    void *kaddr = kmap_atomic(page, KM_USER0);
    clear_page(kaddr);
    kunmap_atomic(kaddr, KM_USER0);
}
这里使用了一个每个CPU单独使用的不会所等待的一个页面,这个固定的地址空间具有原子性,在kmap中禁止抢占,并将指定页面映射入特定的USER0地址,在执行完操作之后马上返回,相当于一个流水线的操作点。
另外,对于共享内存中的空间,新分配的页面也是被清零的。但是由于glibc会缓存用户态地址空间,所以不能指望malloc返回的内存是被清零过的。
四、kmap操作
1、kmap流程
和之前的kmap_atomic相比,这个操作不是原子性的,它可能会被阻塞,并且所有同时执行过这个操作成功的页面数目不能超过1K个页面,这种用法通常在一些文件操作等不涉及控制结构,纯粹作为大数据中转的场景中,例如
static struct page * ext2_get_page(struct inode *dir, unsigned long n)
        kmap(page);

static inline void ext2_put_page(struct page *page)
{
    kunmap(page);
    page_cache_release(page);
}
完成的底层实现代码为
在kmap--->>>kmap_high--->>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个槽位。
        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)继续遍历,直到遍历了所有1K个槽位
            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);
            spin_unlock(&kmap_lock);
            schedule();
            remove_wait_queue(&pkmap_map_wait, &wait);
            spin_lock(&kmap_lock);

            /* 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;
}
2、set_page_address和page_address
和低端内存不同的是,高端内存并不能通过page结构减去page数组开始地址获得page的物理内存编号,进而通过物理内存编号直接获得逻辑地址。所以此时要建立专门的结构,来保存所有已经获得了逻辑地址的高端内存页面的地址,这个结构通过一个hash表存储
/*
 * Hash table bucket
 */
static struct page_address_slot {
    struct list_head lh;            /* List of page_address_maps */
    spinlock_t lock;            /* Protect this bucket's list */
} ____cacheline_aligned_in_smp page_address_htable[1<<PA_HASH_ORDER];

static struct page_address_slot *page_slot(struct page *page)
{
    return &page_address_htable[hash_ptr(page, PA_HASH_ORDER)];
}
hash的输入值是页面page结构的地址,bucket中保存页面的page结构地址和页面映射映射的逻辑地址,申请之后在hash中添加映射,释放后消除。高端内存已经映射页面的逻辑地址到struct page地址的映射即通过该hash表完成。
四、vmalloc操作
vmalloc--->>__vmalloc_node--->>>get_vm_area_node

struct vm_struct *get_vm_area_node(unsigned long size, unsigned long flags,
                   int node, gfp_t gfp_mask)
{
    return __get_vm_area_node(size, flags, VMALLOC_START, VMALLOC_END, node,
                  gfp_mask);
}
这里的宏控制了内核逻辑地址的分配区间,vmlist为内核vmalloc已经被分配了的地址链表,按照起始地址从小到大顺序排列。直观上看,这个链表的结构非常简单,就是在一个区间上定界出每一个被占用区间,区间的表示是原始的 start + size的形式。查找空间时,从一个地址结束到下一个地址开始之间的空间就是可用空间,当这段空间被分配出去之后,就分配一个结构加入链表,详细代码可以参考__get_vm_area_node函数。

void *vmalloc(unsigned long size)
{
    return __vmalloc(size, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL);也就是说vmalloc分配内存时,它是优先从高端内存中分配空间,如果没有高端内存,则使用常规内存,如果使用常规内存,此时该物理内存页面将会有两个不同的逻辑地址,通过这两个地址均可以访问相同的物理页面(未验证,猜测是这样)
}

五、kmalloc
http://www.spinics.net/lists/newbies/msg48138.html
kmalloc和vmalloc的区别
kmalloc allocates physically contiguous memory, while vmalloc
allocates memory which is only virtually contiguous and not
necessarily physically contiguous.

Usually physically contiguous memory is required for hardware devices
(dma etc) , thus kmalloc is useful for allocating such memory. And
though this is not always the requirement, still kmalloc is preferred
, because it is better in terms of performance, because vmalloc can
result in greater TLB thrashing .

vmalloc is useful when very large amounts of memory is to be
allocated, because when memory becomes heavily fragmented, you may not
get success while allocating large memory via kmalloc.

Slab layer serves like a cache for commonly used data structures,
again with a view to reduce fragmentation, and to speed up operations.
To allocate objects from slab you will have to use functions like
kmem_cache_create() and kmem_cache_alloc() ..
六、zonelist的前向覆盖性
在上面的例子中,vmalloc声明了从HIGH_MEM中分配空间,在alloc_pages_node函数中,它获得的zone_list为HIGHMEM对应的zonelist,但是如果此时系统没有配置高端内存或者系统没有高端内存,此时就需要到前一个分区中再查找,也就是说,如果highmem没有,则到normal区查找,再到dma区查找,虽然在alloc_pages_node中只体现了对highmem zonelist的遍历,但是这个zonelist在初始化的时候就已经包含了所有的前面的分区,代码调用链如下
Breakpoint 6, build_zonelists_node (pgdat=0x8c, zonelist=0x2, nr_zones=32,
    zone_type=ZONE_DMA) at mm/page_alloc.c:1653
1653    {
(gdb) bt
#0  build_zonelists_node (pgdat=0x8c, zonelist=0x2, nr_zones=32,
    zone_type=ZONE_DMA) at mm/page_alloc.c:1653
#1  0xc0aadf3c in build_zonelists (pgdat=0xc0a0e500) at mm/page_alloc.c:1814
#2  0xc0aae0c6 in __build_all_zonelists (dummy=0x0) at mm/page_alloc.c:1855
#3  0xc0aae0fd in build_all_zonelists () at mm/page_alloc.c:1864
#4  0xc0a860ba in start_kernel () at init/main.c:547
#5  0x00000000 in ?? ()
(gdb) c
Continuing.

Breakpoint 6, build_zonelists_node (pgdat=0xc0a0e500, zonelist=0xc0a11c80,
    nr_zones=1, zone_type=ZONE_NORMAL) at mm/page_alloc.c:1653
1653    {
(gdb) c
Continuing.

Breakpoint 6, build_zonelists_node (pgdat=0xc0a0e500, zonelist=0xc0a11c94,
    nr_zones=2, zone_type=ZONE_HIGHMEM) at mm/page_alloc.c:1653
1653    {
(gdb) bt
#0  build_zonelists_node (pgdat=0xc0a0e500, zonelist=0xc0a11c94, nr_zones=2,
    zone_type=ZONE_HIGHMEM) at mm/page_alloc.c:1653
#1  0xc0aadf3c in build_zonelists (pgdat=0xc0a0e500) at mm/page_alloc.c:1814
#2  0xc0aae0c6 in __build_all_zonelists (dummy=0x0) at mm/page_alloc.c:1855
#3  0xc0aae0fd in build_all_zonelists () at mm/page_alloc.c:1864
#4  0xc0a860ba in start_kernel () at init/main.c:547
#5  0x00000000 in ?? ()
(gdb)


/*
 * Builds allocation fallback zone lists.
 *
 * Add all populated zones of a node to the zonelist.
 */
static int __meminit build_zonelists_node(pg_data_t *pgdat,
            struct zonelist *zonelist, int nr_zones, enum zone_type zone_type)
{
    struct zone *zone;

    BUG_ON(zone_type >= MAX_NR_ZONES);
    zone_type++;

    do {
        zone_type--; 遍历所有前向zone列表,将这些zone全部添加到自己的zonelist对应的链表中
        zone = pgdat->node_zones + zone_type;
        if (populated_zone(zone)) {
            zonelist->zones[nr_zones++] = zone;
            check_highest_zone(zone_type);
        }

    } while (zone_type);
    return nr_zones;
}
  评论这张
 
阅读(1002)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017