利用early

    技术2024-07-17  66

    5.5 利用early_res分配内存

    回到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}

     

    这个函数也是很简单的,我们就不一行一行地分析了。注意,传进来的参数numentries0,所以要进入4877行的条件语句,把它赋值成nr_kernel_pages。而这个值被定义到.meminit.data段中的,所以,numentries使用的初始化期间的数据段。4914行之前的代码都是在计算这个numentries,也就是我们散列项数的值。前面说了,跟内存大小相关,当计算出来后,由于我们传入的flagHASH_EARLY,所以调用alloc_bootmem_nopanic宏。还要注意,这时候slab分配器还没初始化,也就是根本不存在,所以,不能通过vmallockmalloc来分配内存空间。

     

    好了,numentries得到了,那么这个散列表需要多大呢?4912行做一个log计算,得到这个tablesize,传入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数组,存放着32early_res类型的变量,而且还有一个全局指针early_res指向这个数组的第一个元素,还有一个全局变量max_early_res32

     

    那么,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_resi,那么bad_addr493行就会对这个addr进行处理:*addrp = addr = round_up(r->end, align);强制它等于那个early_res[i]元素的end值。然后返回等于1changed,让find_early_area中的那个while循环再做同样的检查,直到find_overlapped_early返回等于max_early_resi,从而跳出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文件。就是把刚刚分配的addraddr + 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_areastart参数是0x7000find_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函数,用于释放我们初始化期间占有的临时内存,后面一定会碰到。

     

    最新回复(0)