vlambda博客
学习文章列表

【linux内核】start_kernel函数详解系列之mm_init



一、开篇聊一聊二、mm_init函数定义(2-1)page_ext_init_flatmem(2-2)mem_init(2-3)kmem_cache_init(2-4)percpu_init_late(2-5)pgtable_init_late(2-6)vmalloc_init(2-7)ioremap_huge_init三、文末小记

一、开篇聊一聊

本文将继续分析linux内核中的重磅函数:mm_init(),该函数用于设置linux内核的内存分配器。本文源码基于linux内核版本:4.1.15


二、mm_init函数定义

start_kernel()函数中将调用mm_init(),该函数用于设置linux内核的内存分配器,其定义如下:

 1/*
2 * Set up kernel memory allocators
3 */

4static void __init mm_init(void)
5
{
6    page_ext_init_flatmem();
7    mem_init();
8    kmem_cache_init();
9    percpu_init_late();
10    pgtable_init();
11    vmalloc_init();
12    ioremap_huge_init();
13}

从以上函数中可见,mm_init函数中内容是一系列的函数调用操作,下文将依次分析每个函数。

(2-1)page_ext_init_flatmem函数

如果linux内核配置了CONFIG_PAGE_EXTENSION宏且没有定义CONFIG_SPARSEMEM宏,则表明linux内核配置为FLATMEM内存模型,那么page_ext_init_flatmem()函数才有具体内容,否则为空。FLATMEM是linux内核中最简单的内存模型。此模型适用于具有连续或大部分连续物理内存的non-NUMA系统,或者通常是连续的物理内存。在FLATMEM内存模型中,有一个全局的mem_map数组(struct page *mem_map),用于映射整个物理内存。page_ext_init_flatmem函数定义如下(/mm/page_ext.c):

 1void __init page_ext_init_flatmem(void)
2
{
3    int nid, fail;
4    //运行need回调函数
5    if (!invoke_need_callbacks())
6        return;
7
8    for_each_online_node(nid)  {
9        fail = alloc_node_page_ext(nid);
10        if (fail)
11            goto fail;
12    }
13
14    //打印出目前已经分配的page_ext的大小
15    pr_info("allocated %ld bytes of page_ext\n", total_usage);
16    //运行init回调函数
17    invoke_init_callbacks();
18    return;
19
20fail:
21    pr_crit("allocation of page_ext failed.\n");
22    panic("Out of memory");
23}

上述第9行alloc_node_page_ext()是核心分配函数,我们继续往下看。

(2-1-1)alloc_node_page_ext

定义如下:

 1static int __init alloc_node_page_ext(int nid)
2
{
3    struct page_ext *base;
4    unsigned long table_size;
5    unsigned long nr_pages;
6
7    nr_pages = NODE_DATA(nid)->node_spanned_pages;
8    if (!nr_pages)
9        return 0;
10
11    /*
12     * Need extra space if node range is not aligned with
13     * MAX_ORDER_NR_PAGES. When page allocator's buddy algorithm
14     * checks buddy's status, range could be out of exact node range.
15     */

16    if (!IS_ALIGNED(node_start_pfn(nid), MAX_ORDER_NR_PAGES) ||
17        !IS_ALIGNED(node_end_pfn(nid), MAX_ORDER_NR_PAGES))
18        nr_pages += MAX_ORDER_NR_PAGES;
19
20    table_size = sizeof(struct page_ext) * nr_pages;
21    //分配启动内存块。分配成功返回启动内存块的虚拟地址,失败则返回NULL
22    base = memblock_virt_alloc_try_nid_nopanic(
23            table_size, PAGE_SIZE, __pa(MAX_DMA_ADDRESS),
24            BOOTMEM_ALLOC_ACCESSIBLE, nid);
25    if (!base)
26        return -ENOMEM;
27    NODE_DATA(nid)->node_page_ext = base;
28    total_usage += table_size;
29    return 0;
30}

(2-2)mem_init

mem_init()函数用于标记map映射中的空闲区域,并打印出可用的内存,打印出的内存信息是在内核镜像占用内存空间完成以后的信息。并计算出有多少内存可用。该函数是一个架构相关的函数,本文以arm架构为例,函数定义如下(/arch/arm/mm/init.c):

 1void __init mem_init(void)
2
{
3#ifdef CONFIG_HAVE_TCM
4    /* These pointers are filled in on TCM detection */
5    extern u32 dtcm_end;
6    extern u32 itcm_end;
7#endif
8
9    set_max_mapnr(pfn_to_page(max_pfn) - mem_map);
10
11    /*把所有未使用的低端内存放到空闲列表中 */
12    free_unused_memmap();
13
14    /* 将空闲物理页面加入到伙伴系统中 */
15    free_all_bootmem();
16
17#ifdef CONFIG_SA1111
18    /* now that our DMA memory is actually so designated, we can free it */
19    free_reserved_area(__va(PHYS_OFFSET), swapper_pg_dir, -1NULL);
20#endif
21
22    free_highpages();
23
24    mem_init_print_info(NULL);
25
26#define MLK(b, t) b, t, ((t) - (b)) >> 10
27#define MLM(b, t) b, t, ((t) - (b)) >> 20
28#define MLK_ROUNDUP(b, t) b, t, DIV_ROUND_UP(((t) - (b)), SZ_1K)
29
30    pr_notice("Virtual kernel memory layout:\n"
31            "    vector  : 0x%08lx - 0x%08lx   (%4ld kB)\n"
32#ifdef CONFIG_HAVE_TCM
33            "    DTCM    : 0x%08lx - 0x%08lx   (%4ld kB)\n"
34            "    ITCM    : 0x%08lx - 0x%08lx   (%4ld kB)\n"
35#endif
36            "    fixmap  : 0x%08lx - 0x%08lx   (%4ld kB)\n"
37            "    vmalloc : 0x%08lx - 0x%08lx   (%4ld MB)\n"
38            "    lowmem  : 0x%08lx - 0x%08lx   (%4ld MB)\n"
39#ifdef CONFIG_HIGHMEM
40            "    pkmap   : 0x%08lx - 0x%08lx   (%4ld MB)\n"
41#endif
42#ifdef CONFIG_MODULES
43            "    modules : 0x%08lx - 0x%08lx   (%4ld MB)\n"
44#endif
45            "      .text : 0x%p" " - 0x%p" "   (%4td kB)\n"
46            "      .init : 0x%p" " - 0x%p" "   (%4td kB)\n"
47            "      .data : 0x%p" " - 0x%p" "   (%4td kB)\n"
48            "       .bss : 0x%p" " - 0x%p" "   (%4td kB)\n",
49
50            MLK(UL(CONFIG_VECTORS_BASE), UL(CONFIG_VECTORS_BASE) +
51                (PAGE_SIZE)),
52#ifdef CONFIG_HAVE_TCM
53            MLK(DTCM_OFFSET, (unsigned long) dtcm_end),
54            MLK(ITCM_OFFSET, (unsigned long) itcm_end),
55#endif
56            MLK(FIXADDR_START, FIXADDR_END),
57            MLM(VMALLOC_START, VMALLOC_END),
58            MLM(PAGE_OFFSET, (unsigned long)high_memory),
59#ifdef CONFIG_HIGHMEM
60            MLM(PKMAP_BASE, (PKMAP_BASE) + (LAST_PKMAP) *
61                (PAGE_SIZE)),
62#endif
63#ifdef CONFIG_MODULES
64            MLM(MODULES_VADDR, MODULES_END),
65#endif
66
67            MLK_ROUNDUP(_text, _etext),
68            MLK_ROUNDUP(__init_begin, __init_end),
69            MLK_ROUNDUP(_sdata, _edata),
70            MLK_ROUNDUP(__bss_start, __bss_stop));
71
72#undef MLK
73#undef MLM
74#undef MLK_ROUNDUP
75
76#ifdef CONFIG_MMU
77    BUILD_BUG_ON(TASK_SIZE                > MODULES_VADDR);
78    BUG_ON(TASK_SIZE                 > MODULES_VADDR);
79#endif
80
81#ifdef CONFIG_HIGHMEM
82    BUILD_BUG_ON(PKMAP_BASE + LAST_PKMAP * PAGE_SIZE > PAGE_OFFSET);
83    BUG_ON(PKMAP_BASE + LAST_PKMAP * PAGE_SIZE    > PAGE_OFFSET);
84#endif
85
86    if (PAGE_SIZE >= 16384 && get_num_physpages() <= 128) {
87        extern int sysctl_overcommit_memory;
88
89        sysctl_overcommit_memory = OVERCOMMIT_ALWAYS;
90    }
91}

第14行代码,由于mem_map数组可能比较大,所以这里调用free_unused_memmap()函数释放内存映射中未使用的区域。

第15行代码,将调用free_all_bootmem()函数将空闲物理页面加入到伙伴系统中,该函数极其重要!

上述30-70行代码是一个pr_notice()linux内核中信息打印函数。在内核启动过程中,将向终端打印出信息,如下图所示:


(2-3)kmem_cache_init

kmem_cache_init()函数用于初始化小内存slab分配器,该函数在页面分配器被初始化之后调用,但是需要放在smp_init()之前。定义如下(/mm/slab.c):

 1void __init kmem_cache_init(void)
2
{
3    int i;
4
5    BUILD_BUG_ON(sizeof(((struct page *)NULL)->lru) <
6                    sizeof(struct rcu_head));
7    kmem_cache = &kmem_cache_boot;
8
9    if (num_possible_nodes() == 1)
10        use_alien_caches = 0;
11
12    for (i = 0; i < NUM_INIT_LISTS; i++)
13        kmem_cache_node_init(&init_kmem_cache_node[i]);
14
15    if (!slab_max_order_set && totalram_pages > (32 << 20) >> PAGE_SHIFT)
16        slab_max_order = SLAB_MAX_ORDER_HI;
17
18    //在引导期间,创建kmem_cache缓存
19    create_boot_cache(kmem_cache, "kmem_cache",
20        offsetof(struct kmem_cache, node) +
21                  nr_node_ids * sizeof(struct kmem_cache_node *),
22                  SLAB_HWCACHE_ALIGN);
23
24    //将kmem_cache->list添加到slab_caches全局链表中
25    list_add(&kmem_cache->list, &slab_caches);
26    slab_state = PARTIAL;
27
28    //为kmem_cache_node提供内存缓存。
29    kmalloc_caches[INDEX_NODE] = create_kmalloc_cache("kmalloc-node",
30                kmalloc_size(INDEX_NODE), ARCH_KMALLOC_FLAGS);
31    slab_state = PARTIAL_NODE;
32
33    slab_early_init = 0;
34
35    {
36        int nid;
37
38        for_each_online_node(nid) {
39            init_list(kmem_cache, &init_kmem_cache_node[CACHE_CACHE + nid], nid);
40
41            init_list(kmalloc_caches[INDEX_NODE],
42                      &init_kmem_cache_node[SIZE_NODE + nid], nid);
43        }
44    }
45
46    create_kmalloc_caches(ARCH_KMALLOC_FLAGS);
47}

上述第7行代码:

1    kmem_cache = &kmem_cache_boot;
1static struct kmem_cache kmem_cache_boot = {
2    .batchcount = 1,
3    .limit = BOOT_CPUCACHE_ENTRIES,
4    .shared = 1,
5    .size = sizeof(struct kmem_cache),
6    .name = "kmem_cache",
7};

第19行代码,使用create_boot_cache()函数在引导期间,创建kmem_cache缓存。

第25行代码,  使用list_add()  将kmem_cache->list添加到slab_caches全局链表中.。

第35-44行代码段,将会调用init_list()函数使用kmalloced内存交换静态的kmem缓存节点。

第46行代码,使用create_kmalloc_caches()创建kmalloc数组。

接下来,来分析percpu_init_late函数。

(2-4)percpu_init_late

percpu_init_late()函数用于在initdata中使用临时分配的map初始化保留块,便于在slab准备好之前使用。这个函这个函数在slab创建后被调用,并用正确分配的映射替换那些。定义如下(/mm/percpu.c):

 1void __init percpu_init_late(void)
2
{
3    struct pcpu_chunk *target_chunks[] =
4        {
 pcpu_first_chunk, pcpu_reserved_chunk, NULL };
5    struct pcpu_chunk *chunk;
6    unsigned long flags;
7    int i;
8
9    for (i = 0; (chunk = target_chunks[i]); i++) {
10        int *map;
11        const size_t size = PERCPU_DYNAMIC_EARLY_SLOTS * sizeof(map[0]);
12
13        BUILD_BUG_ON(size > PAGE_SIZE);
14
15        map = pcpu_mem_zalloc(size);
16        BUG_ON(!map);
17
18        spin_lock_irqsave(&pcpu_lock, flags);
19        memcpy(map, chunk->map, size);
20        chunk->map = map;
21        spin_unlock_irqrestore(&pcpu_lock, flags);
22    }
23}

上述第11行代码,将计算需要分配的pcpp内存的大小,该计算结果会被传入到pcpu_mem_zalloc()函数中分配对应大小的内存。pcpu_mem_zalloc函数定义如下:

 1static void *pcpu_mem_zalloc(size_t size)
2
{
3    if (WARN_ON_ONCE(!slab_is_available()))
4        return NULL;
5
6    if (size <= PAGE_SIZE)
7        return kzalloc(size, GFP_KERNEL);
8    else
9        return vzalloc(size);
10}

回到percpu_init_late函数中,第19行代码,将使用memcpy()函数进行内存拷贝。操作的对象是target_chunks这个pcpu_chunk表,该表中含有两个chunk:

(1)pcpu_first_chunk——始终存在的第一个块。注意,与其他块不同,这个块可以以几种不同的方式分配和映射,因此通常不存在于vmalloc区域。

(2)pcpu_reserved_chunk——预留的保留块。

(2-5)pgtable_init_late

patable_init_late()函数用于初始化pgtable,在arm架构下,该部分函数没有执行实质的操作。其定义如下(/include/linux/mm.h):

1static inline void pgtable_init(void)
2
{
3    ptlock_cache_init();
4    pgtable_cache_init();
5}

(2-6)vmalloc_init

vmalloc_init()函数功能是初始化vmalloc,定义如下(/mm/vmalloc.c):

 1void __init vmalloc_init(void)
2
{
3    struct vmap_area *va;
4    struct vm_struct *tmp;
5    int i;
6
7    //这部分代码用于设置per_cpu变量:vmap_block_queue和vfree_deferred,vmap_block_queue是空闲和脏vmap块的队列,用于分配和冲洗。
8    for_each_possible_cpu(i) {
9        struct vmap_block_queue *vbq;
10        struct vfree_deferred *p;
11
12        vbq = &per_cpu(vmap_block_queue, i);
13        spin_lock_init(&vbq->lock);
14        INIT_LIST_HEAD(&vbq->free);
15        p = &per_cpu(vfree_deferred, i);
16        init_llist_head(&p->list);
17        INIT_WORK(&p->wq, free_work);
18    }
19
20    //导入存在的vmlist条目
21    for (tmp = vmlist; tmp; tmp = tmp->next) {
22        //分配vmap_area内存空间
23        va = kzalloc(sizeof(struct vmap_area), GFP_NOWAIT);
24        //设置vmap_area内存空间的参数
25        va->flags = VM_VM_AREA;
26        va->va_start = (unsigned long)tmp->addr;
27        va->va_end = va->va_start + tmp->size;
28        va->vm = tmp;
29        //将分配出的vmap_area添加到红黑树中
30        __insert_vmap_area(va);
31    }
32
33    vmap_area_pcpu_hole = VMALLOC_END;
34    //设置vmap_initialized变量为true,用于标记vmap已经被初始化
35    vmap_initialized = true;
36}

(2-7)ioremap_huge_init

ioremap_huge_init()函数是在定义了CONFIG_HAVE_ARCH_HUGE_VMAP宏后,其函数中才有执行代码,否则该函数内容将为空。这里假设已经定义了CONFIG_HAVE_ARCH_HUGE_VMAP宏,那么ioremap_huge_init函数定义如下(/lib/ioremap.c):

1void __init ioremap_huge_init(void)
2
{
3    if (!ioremap_huge_disabled) {
4        if (arch_ioremap_pud_supported())
5            ioremap_pud_capable = 1;
6        if (arch_ioremap_pmd_supported())
7            ioremap_pmd_capable = 1;
8    }
9}

三、文末小记

本文所分析的mm_init()函数,在linux内核中用于设置内存分配器。在函数具体操作中,比较重要的函数有:
1.用于内存初始化的mem_init()函数
2.用于小内存slab分配器初始化的kmem_cache_init()函数。
3.用于初始化vmalloc内存的vmalloc_init()函数。

本文主要站在mm_init函数内部实现的角度展开,没有过多深入到内存管理部分内容!先从形体上认识一下该函数啦。O(∩_∩)O哈哈~。


本文完!!!