【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, -1, NULL);
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哈哈~。
本文完!!!