回到start_kernel中,570行,page_alloc_init()函数:
void __init page_alloc_init(void)
{
hotcpu_notifier(page_alloc_cpu_notify, 0);
}
这个函数会调用hotcpu_notifier函数。当然,在编译选项CONFIG_HOTPLUG_CPU起作用时,这个函数才有效。这个编译选项就是性感的热插拔技术,而且是CPU的热插拔。如果定义了,就去新建一个notifier_block结构,并调用register_cpu_notifier函数,将新建的这个结构挂到全局cpu_chain链中,具体的代码我就不去分析了。
继续走,572行是一个printk函数,用于打印刚刚setup_arch拷贝到boot_command_line的信息。随后573行,调用parse_early_param函数对boot_command_line进行解析。它跟574行一样,都是调用parse_args进行解析。这个函数来自kernel/params.c,函数本身比较简单,就是把解析后的结果放到start_kernel的内部变量__start___param数组中。
继续走,581行,pidhash_init函数,又遇到了一个重点函数,来自kernel/pid.c:
501void __init pidhash_init(void)
502{
503 int i, pidhash_size;
504
505 pid_hash = alloc_large_system_hash("PID", sizeof(*pid_hash), 0, 18,
506 HASH_EARLY | HASH_SMALL,
507 &pidhash_shift, NULL, 4096);
508 pidhash_size = 1 << pidhash_shift;
509
510 for (i = 0; i < pidhash_size; i++)
511 INIT_HLIST_HEAD(&pid_hash[i]);
512}
pid_hash是个全局变量:
static struct hlist_head *pid_hash;
为什么说它是重点函数呢,因为这里涉及到了初始化期间,在slab分配器还不存在的时候,只有前面刚刚建立好的初始化阶段的内存管理体系,如何分配一个内存空间来存放pid散列表。而pid散列表这么一个东西,它跟物理内存大小有关,也就是1GB的内存空间就可能有16~4096个散列项。不管怎样,调用alloc_large_system_hash函数:
4863void *__init alloc_large_system_hash(const char *tablename,
4864 unsigned long bucketsize,
4865 unsigned long numentries,
4866 int scale,
4867 int flags,
4868 unsigned int *_hash_shift,
4869 unsigned int *_hash_mask,
4870 unsigned long limit)
4871{
4872 unsigned long long max = limit;
4873 unsigned long log2qty, size;
4874 void *table = NULL;
4875
4876 /* allow the kernel cmdline to have a say */
4877 if (!numentries) {
4878 /* round applicable memory size up to nearest megabyte */
4879 numentries = nr_kernel_pages;
4880 numentries += (1UL << (20 - PAGE_SHIFT)) - 1;
4881 numentries >>= 20 - PAGE_SHIFT;
4882 numentries <<= 20 - PAGE_SHIFT;
4883
4884 /* limit to 1 bucket per 2^scale bytes of low memory */
4885 if (scale > PAGE_SHIFT)
4886 numentries >>= (scale - PAGE_SHIFT);
4887 else
4888 numentries <<= (PAGE_SHIFT - scale);
4889
4890 /* Make sure we've got at least a 0-order allocation.. */
4891 if (unlikely(flags & HASH_SMALL)) {
4892 /* Makes no sense without HASH_EARLY */
4893 WARN_ON(!(flags & HASH_EARLY));
4894 if (!(numentries >> *_hash_shift)) {
4895 numentries = 1UL << *_hash_shift;
4896 BUG_ON(!numentries);
4897 }
4898 } else if (unlikely((numentries * bucketsize) < PAGE_SIZE))
4899 numentries = PAGE_SIZE / bucketsize;
4900 }
4901 numentries = roundup_pow_of_two(numentries);
4902
4903 /* limit allocation size to 1/16 total memory by default */
4904 if (max == 0) {
4905 max = ((unsigned long long)nr_all_pages << PAGE_SHIFT) >> 4;
4906 do_div(max, bucketsize);
4907 }
4908
4909 if (numentries > max)
4910 numentries = max;
4911
4912 log2qty = ilog2(numentries);
4913
4914 do {
4915 size = bucketsize << log2qty;
4916 if (flags & HASH_EARLY)
4917 table = alloc_bootmem_nopanic(size);
4918 else if (hashdist)
4919 table = __vmalloc(size, GFP_ATOMIC, PAGE_KERNEL);
4920 else {
4921 /*
4922 * If bucketsize is not a power-of-two, we may free
4923 * some pages at the end of hash table which
4924 * alloc_pages_exact() automatically does
4925 */
4926 if (get_order(size) < MAX_ORDER) {
4927 table = alloc_pages_exact(size, GFP_ATOMIC);
4928 kmemleak_alloc(table, size, 1, GFP_ATOMIC);
4929 }
4930 }
4931 } while (!table && size > PAGE_SIZE && --log2qty);
4932
4933 if (!table)
4934 panic("Failed to allocate %s hash table/n", tablename);
4935
4936 printk(KERN_INFO "%s hash table entries: %d (order: %d, %lu bytes)/n",
4937 tablename,
4938 (1U << log2qty),
4939 ilog2(size) - PAGE_SHIFT,
4940 size);
4941
4942 if (_hash_shift)
4943 *_hash_shift = log2qty;
4944 if (_hash_mask)
4945 *_hash_mask = (1 << log2qty) - 1;
4946
4947 return table;
4948}
这个函数也是很简单的,我们就不一行一行地分析了。注意,传进来的参数numentries为0,所以要进入4877行的条件语句,把它赋值成nr_kernel_pages。而这个值被定义到.meminit.data段中的,所以,numentries使用的初始化期间的数据段。4914行之前的代码都是在计算这个numentries,也就是我们散列项数的值。前面说了,跟内存大小相关,当计算出来后,由于我们传入的flag是HASH_EARLY,所以调用alloc_bootmem_nopanic宏。还要注意,这时候slab分配器还没初始化,也就是根本不存在,所以,不能通过vmalloc或kmalloc来分配内存空间。
好了,numentries得到了,那么这个散列表需要多大呢?4912行做一个log计算,得到这个table的size,传入alloc_bootmem_nopanic宏中。
#define alloc_bootmem_nopanic(x) /
__alloc_bootmem_nopanic(x, SMP_CACHE_BYTES, __pa(MAX_DMA_ADDRESS))
void * __init __alloc_bootmem_nopanic(unsigned long size, unsigned long align,
unsigned long goal)
{
unsigned long limit = 0;
#ifdef CONFIG_NO_BOOTMEM
limit = -1UL;
#endif
return ___alloc_bootmem_nopanic(size, align, goal, limit);
}
static void * __init ___alloc_bootmem_nopanic(unsigned long size,
unsigned long align,
unsigned long goal,
unsigned long limit)
{
#ifdef CONFIG_NO_BOOTMEM
void *ptr;
if (WARN_ON_ONCE(slab_is_available()))
return kzalloc(size, GFP_NOWAIT);
restart:
ptr = __alloc_memory_core_early(MAX_NUMNODES, size, align, goal, limit);
if (ptr)
return ptr;
if (goal != 0) {
goal = 0;
goto restart;
}
return NULL;
#else
bootmem_data_t *bdata;
void *region;
restart:
region = alloc_arch_preferred_bootmem(NULL, size, align, goal, limit);
if (region)
return region;
list_for_each_entry(bdata, &bdata_list, list) {
if (goal && bdata->node_low_pfn <= PFN_DOWN(goal))
continue;
if (limit && bdata->node_min_pfn >= PFN_DOWN(limit))
break;
region = alloc_bootmem_core(bdata, size, align, goal, limit);
if (region)
return region;
}
if (goal) {
goal = 0;
goto restart;
}
return NULL;
#endif
}
我们看到,上面的函数层层包装,由于配置了CONFIG_NO_BOOTMEM,所以最后调用__alloc_memory_core_early函数(注意,在调用它之前仍然要先检查一下slab环境是否已经建立起来了,如果是,就用slab来分配)。
还记得我们在setup_arch()时,调用initmem_init来初始化的early_node_map数组吗?这里就用到了,__alloc_memory_core_early函数也是咱们的老熟人了,大家可以回头看看“添砖加瓦”相关的内容。那么这里就有一个非常重要的问题了。我们前面为页框分配空间的时候,当时调用__alloc_memory_core_early函数时,传给他的参数goal的值是宏MAX_DMA_ADDRESS的物理地址,也就是0x1000000,而__pa(MAX_DMA_ADDRESS)也是这次的goal参数。将来我们也会看到,初始化阶段所有的goal都是它。那么我们在调用最底层find_early_area函数时,岂不是全部都只从一个固定物理地址开始分配?这不乱套了?
要回答这个问题,我们还是得回顾一下find_early_area函数,来自kernel/early_res.c文件:
539u64 __init find_early_area(u64 ei_start, u64 ei_last, u64 start, u64 end,
540 u64 size, u64 align)
541{
542 u64 addr, last;
543
544 addr = round_up(ei_start, align);
545 if (addr < start)
546 addr = round_up(start, align);
547 if (addr >= ei_last)
548 goto out;
549 while (bad_addr(&addr, size, align) && addr+size <= ei_last)
550 ;
551 last = addr + size;
552 if (last > ei_last)
553 goto out;
554 if (last > end)
555 goto out;
556
557 return addr;
558
559out:
560 return -1ULL;
561}
这个函数我们前面仅仅是做了简单说明,其实看起简单的东西未必就能一下子吃透,需要花费大量的功夫。前面在讲find_early_area获得永久内核页表的首地址的时候,我们就漏掉了一个处理重叠问题的一个重要的步骤。既然所有的goal都是同一个值,那么肯定会有重叠,find_early_area函数的549行那个while循环就是处理这个重叠。主要是调用bad_addr函数,位于同一个文件中:
483static inline int __init bad_addr(u64 *addrp, u64 size, u64 align)
484{
485 int i;
486 u64 addr = *addrp;
487 int changed = 0;
488 struct early_res *r;
489again:
490 i = find_overlapped_early(addr, addr + size);
491 r = &early_res[i];
492 if (i < max_early_res && r->end) {
493 *addrp = addr = round_up(r->end, align);
494 changed = 1;
495 goto again;
496 }
497 return changed;
498}
注意,我们传递给bad_addr的参数是addr对应的地址,意思就是让该函数去探测addr对应的size个内存单元是否已经被使用,即发生重叠冲突,如果是,就修改addr本身,最终目的是确保addr对应的size个内存单元没有被任何其他数据结构所占据。
那么,bad_addr是如何处理这个冲突的呢?这里要介绍一个early_res体系,看到kernel/early_res.c文件的前几行代码:
18#define MAX_EARLY_RES_X 32
19
20struct early_res {
21 u64 start, end;
22 char name[15];
23 char overlap_ok;
24};
25static struct early_res early_res_x[MAX_EARLY_RES_X] __initdata;
26
27static int max_early_res __initdata = MAX_EARLY_RES_X;
28static struct early_res *early_res __initdata = &early_res_x[0];
29static int early_res_count __initdata;
在我们编译vmlinux时,内核初始化代码的数据区有一个early_res_x数组,存放着32个early_res类型的变量,而且还有一个全局指针early_res指向这个数组的第一个元素,还有一个全局变量max_early_res为32。
那么,bad_addr函数的490行就调用find_overlapped_early函数对这个addr进行调整:
31static int __init find_overlapped_early(u64 start, u64 end)
32{
33 int i;
34 struct early_res *r;
35
36 for (i = 0; i < max_early_res && early_res[i].end; i++) {
37 r = &early_res[i];
38 if (end > r->start && start < r->end)
39 break;
40 }
41
42 return i;
43}
我们看到,如果地址区间[addr, addr + size]落到early_res[]数组的某个元素的[start, end]范围,就表示发生了冲突,则会返回小于max_early_res的i,那么bad_addr的493行就会对这个addr进行处理:*addrp = addr = round_up(r->end, align);强制它等于那个early_res[i]元素的end值。然后返回等于1的changed,让find_early_area中的那个while循环再做同样的检查,直到find_overlapped_early返回等于max_early_res的i,从而跳出while循环,得到不与任何内核数据结构冲突的addr。
当__alloc_memory_core_early函数执行完find_early_area,获得一个正确的addr时,会把他变成虚拟地址,并memset它,随后调用函数:
reserve_early_without_check(addr, addr + size, "BOOTMEM");
void __init reserve_early_without_check(u64 start, u64 end, char *name)
{
struct early_res *r;
if (start >= end)
return;
__check_and_double_early_res(start, end);
r = &early_res[early_res_count];
r->start = start;
r->end = end;
r->overlap_ok = 0;
if (name)
strncpy(r->name, name, sizeof(r->name) - 1);
early_res_count++;
}
这个函数没有问题,同样来自kernel/early_res.c文件。就是把刚刚分配的addr和addr + size作为early_res[]保存起来,避免在伙伴系统和slab体系初始化之前,其他的通过_alloc_memory_core_early函数分配空间的函数与其发生冲突。
至此,针对初始化期间的内存管理就全介绍完了,不过我还有一点要说。初始化期间为各个数据结构分配物理页面都是通过_alloc_memory_core_early函数。内核发展到现在,已经抛弃了以前的BOOTMEM分配体系,而只是通过_alloc_memory_core_early函数调用find_early_area 函数进行简单的分配,大幅提高了系统初始化期间的性能。如果大家还对过去的那套初始化分配体系感兴趣,可以尝试一下取消编译配置CONFIG_NO_BOOTMEM。
另外,这个early_res[]数组最大不能超过该32个,所以即使效率很高,但初始化阶段借助early_res体系分配空间的次数也不能超过32次。前面已经用了一次了,也就是给pgdat->node_mem_map分配页面的时候,这里是第二次。后面还有,有兴趣的同学可以数一数,是不是没有超过32次。
可能细心的同学会问,我们在“着手建立永久内核页表”不是也用了find_early_area来为页表分配空间么。但是你回过头去看看,那个是个传递给find_early_area的start参数是0x7000(find_early_table_space函数的74行),跟我们的__pa(MAX_DMA_ADDRESS)相差甚远,所以打死都不会冲突的。
还有一点就是,通过_alloc_memory_core_early分配的页面是会释放的。所以大家要有一个概念了,只要当伙伴系统建立起来后,我们通过alloc_page函数分配的页面,其对应的释放函数是free_page。而_alloc_memory_core_early函数也有对应的free函数的,那就是free_all_memory_core_early函数,用于释放我们初始化期间占有的临时内存,后面一定会碰到。