Linux内存管理 Bootmem管理器

  本地的笔记有点长,先把bootmem位图分配器的建立 及  使用过程做下梳理。

都是代码,上面做了标注。开始的汇编部分省略了(涉及的内容不多,除了swapper_pg_dir的分配)。

该记录不会再添加说明,看下记录中的注释就明白了bootmem的建立及使用。

该记录中考虑了高端内存……

从start_kernel开始……

物理内存大体面貌就有了,后续就需要进行内存的页表映射,完成实际的物理地址到虚拟地址的映射了。

那就待续吧。

Linux内核使用伙伴系统管理内存,那么在伙伴系统之前,内核使通过memblock来管理。在系统启动阶段,使用memblock记录物理内存的使用情况,首先我们知道在内核启动后,对于内存,分成好几块

内存中的某些部分使永久分配给内核的,例如代码段和数据段,ramdisk和dtb占用的空间等,是系统内存的一部分,不能被侵占,也不参与内存的分配,称之为静态内存
GPU/camera/多核共享的内存都需要预留大量连续内存,这部分内存平时不使用,但是必须为各个应用场景预留,这样的内存称之为预留内存
内存其余的部分,是需要内核管理的内存,称之为动态内存
那么memblock就是将以上内存按功能划分为若干内存区,使用不同的类型存放在memory和reserved的两个集合中,memory即为动态内存,而resvered包括静态内存等。

1. memblock介绍
memblock的算法实现是,它将所有的状态都保持在一个全局变量__initdata_memblock中,算法的初始化以及内存的申请释放都是在将内存块的状态做变更。那么从数据结构入手,__initdata_memblock是一个memblock结构体,其定义如下:

struct memblock {
    bool bottom_up;  /* is bottom up direction? */
    phys_addr_t current_limit;
    struct memblock_type memory;
    struct memblock_type reserved;
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
    struct memblock_type physmem;
#endif
};

定义    含义
bottom_up    表示分配器的分配方式,true表示从低地址向高地址分配,false则相反
current_limit    来表示用来限制alloc的内存申请
memory    表示可用可分配的内存
reserved    表示已经分配出去了的内存
memory和reserved是很关键的一个数据结构,memblock算法的内存初始化和申请释放都是围绕着他们展开工作。
往下看看memory和reserved的结构体memblock_type定义:

struct memblock_region {
    phys_addr_t base;
    phys_addr_t size;
    unsigned long flags;
#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP
    int nid;
#endif
};

struct memblock_type {
    unsigned long cnt;    /* number of regions */
    unsigned long max;    /* size of the allocated array */
    phys_addr_t total_size;    /* size of all regions */
    struct memblock_region *regions;
};

定义    含义
cnt    表示当前状态(memory/reserved)的内存块可用数
max    可支持的最大数
total_size    当前状态(memory/reserved)的空间大小,也就是内存总大小空间
regions    用于保存内存块信息的结构(包括基址、大小和标记等)
内核中memblock实例,定义了初始值,其定义如下

static struct memblock_region memblock_memory_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock;
static struct memblock_region memblock_reserved_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock;
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
static struct memblock_region memblock_physmem_init_regions[INIT_PHYSMEM_REGIONS] __initdata_memblock;
#endif

struct memblock memblock __initdata_memblock = {
    .memory.regions        = memblock_memory_init_regions,
    .memory.cnt        = 1,    /* empty dummy entry */
    .memory.max        = INIT_MEMBLOCK_REGIONS,

    .reserved.regions    = memblock_reserved_init_regions,
    .reserved.cnt        = 1,    /* empty dummy entry */
    .reserved.max        = INIT_MEMBLOCK_REGIONS,

#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
    .physmem.regions    = memblock_physmem_init_regions,
    .physmem.cnt        = 1,    /* empty dummy entry */
    .physmem.max        = INIT_PHYSMEM_REGIONS,
#endif

    .bottom_up        = false,
    .current_limit        = MEMBLOCK_ALLOC_ANYWHERE,
};

它初始化了部分成员,其定义如下

bottom_up.定义了内存申请方式,从高地址向低地址
current_limit:alloc的内存限制为0xFFFFFFFF
同时通过全局定义为memblock的算法管理器中的memory和reserved准备了内存空间
2. 获取物理内存大小
内核是如何知道内存的信息呢,这部分是由bootloader完成,然后使用fdt或者atag等方式传递给内核,然后内核解析其中的memory节点获取物理内存地址和大小,通过DTB获取物理属性,然后解析并添加到memblock子系统中。

 memory {
                reg = <0x80000000 0x20000000>;
 };
1
2
3
根据上面的dts,在start_kernel–>setup_arch–>setup_machine_fdt–>early_init_dt_scan_nodes–>of_scan_flat_dt(遍历Nodes)–>early_init_dt_scan_memory(初始化单个内存Node),这部分的分析已经在设备树详解(四)kernel的解析W中详细介绍过了,结果是从DTS解析出base size分别是0x80000000 0x20000000,后根据解析出的base/size,调用early_init_dt_add_memory_arch–>memblock_add–>memblock_add_range将解析出的物理内存加入到memblock子系统中,里首先看一下memblock_add_range()的函数实现:

int __init_memblock memblock_add_range(struct memblock_type *type,
                phys_addr_t base, phys_addr_t size,
                int nid, unsigned long flags)
{
    bool insert = false;
    phys_addr_t obase = base;
    phys_addr_t end = base + memblock_cap_size(base, &size);
    int idx, nr_new;
    struct memblock_region *rgn;

    if (!size)
        return 0;

    /* special case for empty array */                   
    if (type->regions[0].size == 0) {                           ------------------ (1)
        WARN_ON(type->cnt != 1 || type->total_size);
        type->regions[0].base = base;
        type->regions[0].size = size;
        type->regions[0].flags = flags;
        memblock_set_region_node(&type->regions[0], nid);
        type->total_size = size;
        return 0;
    }
repeat:
    base = obase;
    nr_new = 0;

    for_each_memblock_type(type, rgn) {                        ------------------ (2)
        phys_addr_t rbase = rgn->base;
        phys_addr_t rend = rbase + rgn->size;

        if (rbase >= end)
            break;
        if (rend <= base)
            continue;
        if (rbase > base) {
#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP
            WARN_ON(nid != memblock_get_region_node(rgn));
#endif
            WARN_ON(flags != rgn->flags);
            nr_new++;
            if (insert)
                memblock_insert_region(type, idx++, base,
                               rbase - base, nid,
                               flags);
        }
        /* area below @rend is dealt with, forget about it */
        base = min(rend, end);
    }

    /* insert the remaining portion */
    if (base < end) {
        nr_new++;
        if (insert)
            memblock_insert_region(type, idx, base, end - base,
                           nid, flags);
    }

    if (!nr_new)
        return 0;

    /*
     * If this was the first round, resize array and repeat for actual
     * insertions; otherwise, merge and return.
     */
    if (!insert) {
        while (type->cnt + nr_new > type->max)
            if (memblock_double_array(type, obase, size) < 0)       ------------------ (3)
                return -ENOMEM;
        insert = true;
        goto repeat;
    } else {
        memblock_merge_regions(type);                                ------------------ (4)
        return 0;
    }
}


如果memblock算法管理内存为空的时候,则将当前空间添加进去就可以了
如果memblock算法管理内存不为空,则先检查是否存在内存重叠的情况,如果有的话,则剔除该重叠部分,然后将其余非重叠部分添加进去
如果出现region[]数据空间不够的情况,则通过memblock_double_array添加新的region空间即可
如果region[]数据空间够的情况,则通过memblock_merge_regions把紧凑的空间合并即可
memblock_add_range函数的功能为将传入参数给定的内存块(以起始物理地址和长度表示)添加到memory或者reserved region中,这两种region 都用数组方式存储数据,在初始阶段会分配一个给定大小的数组,然后在本函数中添加memblock region。所添加的各region数组都以物理地址从低到高的顺序排列。实际测试的打印信息为

memblock_add将0x00000080000000-0x0000009fffffff内存加入到memblock memory region
将内核代码段地址加入到0x00000080200000-0x000000810e8eeb加入到memblock reserve
将fdt地址加入到0x00000088000000-0x00000088014303加入到memblock reserve
OF: fdt:Machine model: Freescale i.MX6 ULL 14x14 EVK Board
memblock_add: [0x00000080000000-0x0000009fffffff] flags 0x0 early_init_dt_scan_memory+0xe4/0xf4
memblock_reserve: [0x00000080200000-0x000000810e8eeb] flags 0x0 arm_memblock_init+0x44/0x1b0
memblock_reserve: [0x00000080003000-0x00000080007fff] flags 0x0 arm_memblock_init+0x154/0x1b0
memblock_reserve: [0x00000088000000-0x00000088014303] flags 0x0 early_init_fdt_reserve_self+0x3c/0x44
memblock_reserve: [0x0000008c000000-0x0000009fffffff] flags 0x0 memblock_alloc_range_nid+0x70/0x88

3. 记录系统预留内存
对于系统预留内存,包括静态内存(内核image,ramdisk,fdt等占用空间),以及camera,display等作系统预留的大量连续内存。另外像手机平台,也需要为多核留一些空间,比如为通信核预留的modem取悦,这部分都是永久分配出去。因此正式因为这部分有特殊的用途或者正在使用,不会进入伙伴系统或被memblock分配器再次分配。对于32位的系统,采用arm_memblock_init进行初始化,而对于64采用arm64_memblock_init,我们来看一下这个做了些什么

void __init arm_memblock_init(const struct machine_desc *mdesc)
{
    /* Register the kernel text, kernel data and initrd with memblock. */
#ifdef CONFIG_XIP_KERNEL                                            ----------------------(1)
    memblock_reserve(__pa(_sdata), _end - _sdata);
#else
    memblock_reserve(__pa(_stext), _end - _stext);
#endif
#ifdef CONFIG_BLK_DEV_INITRD                                        ----------------------(2)
    /* FDT scan will populate initrd_start */
    if (initrd_start && !phys_initrd_size) {
        phys_initrd_start = __virt_to_phys(initrd_start);
        phys_initrd_size = initrd_end - initrd_start;
    }
    initrd_start = initrd_end = 0;
    if (phys_initrd_size &&
        !memblock_is_region_memory(phys_initrd_start, phys_initrd_size)) {
        pr_err("INITRD: 0x%08llx+0x%08lx is not a memory region - disabling initrd\n",
               (u64)phys_initrd_start, phys_initrd_size);
        phys_initrd_start = phys_initrd_size = 0;
    }
    if (phys_initrd_size &&
        memblock_is_region_reserved(phys_initrd_start, phys_initrd_size)) {
        pr_err("INITRD: 0x%08llx+0x%08lx overlaps in-use memory region - disabling initrd\n",
               (u64)phys_initrd_start, phys_initrd_size);
        phys_initrd_start = phys_initrd_size = 0;
    }
    if (phys_initrd_size) {
        memblock_reserve(phys_initrd_start, phys_initrd_size);

        /* Now convert initrd to virtual addresses */
        initrd_start = __phys_to_virt(phys_initrd_start);
        initrd_end = initrd_start + phys_initrd_size;
    }
#endif

    arm_mm_memblock_reserve();                                   ----------------------(3)

    /* reserve any platform specific memblock areas */
    if (mdesc->reserve)
        mdesc->reserve();

    early_init_fdt_reserve_self();                              ----------------------(4)
    early_init_fdt_scan_reserved_mem();                         ----------------------(5)

    /* reserve memory for DMA contiguous allocations */
    dma_contiguous_reserve(arm_dma_limit);                      ----------------------(6)

    arm_memblock_steal_permitted = false;                       ----------------------(7)
    memblock_dump_all();
}

将内核代码段设置为reserved类型memblock,其中的init段会在free_initmem中返回给内核
将内核的initrd段设置为reserved类型memblock
将swapper_pg_dir页目录的16K地址空间也设置为reserved类型memblock
将DTB本身区域设置为reserved类型memblock
将dtb中的reserved-memory区域设置为reserved类型memblock,根据dtb中的memreserve信息, 调用memblock_reserve
会预留内存并准备给CMA使用。在有些体系架构下(例如,ARM),需要完成一些体系相关的工作
下面我们对重点看一下early_init_fdt_scan_reserved_mem这个这个函数怎么将dtb的节点属性来完成reserved。

void __init early_init_fdt_scan_reserved_mem(void)
{
    int n;
    u64 base, size;

    if (!initial_boot_params)                                       -------------------(1)
        return;

    /* Process header /memreserve/ fields */
    for (n = 0; ; n++) {                                           
        fdt_get_mem_rsv(initial_boot_params, n, &base, &size);      -------------------(2)
        if (!size)
            break;
        early_init_dt_reserve_memory_arch(base, size, 0);           -------------------(3)
    }

    of_scan_flat_dt(__fdt_scan_reserved_mem, NULL);                  -------------------(4)
    fdt_init_reserved_mem();                                         


initial_boot_params实际上就是fdt对应的虚拟地址。在early_init_dt_verify中设定的。如果系统中都没有有效的fdt,那么没有什么可以scan的
分析fdt中的 /memreserve/ fields ,进行内存的保留
保留每一个/memreserve/ fields定义的memory region,底层是通过memblock_reserve接口函数实现的
对fdt中的每一个节点调用__fdt_scan_reserved_mem函数,进行reserved-memory节点的扫描,之后调用fdt_init_reserved_mem函数进行内存预留的动作
设备树中标记为reserved的内存保留区域可以通过以下两种方式来配置

dts开头的位置通过/memreserve字段标记,由于它不属于任何节点,例如这种方式

/dts-v1/;

/memreserve/ 0x00000000 0x00400000;

#include "axm55xx.dtsi"
#include "axm5516-cpus.dtsi"

reserved-memory节点保留内存,以下为某dts中保留内存节点的一段。由dts可见它可以定义多个保留内存块属性,每个属性包含一块要保留的内存,在初始化reserved region时,可以通过解析dtb中该节点的各个属性,然后填充reserved region。在这个例子中,保留内存属性还包含了no-map标志,它表示这段内存不要放入线性映射区,因此需要从memory region中移除它们,例如这种方式

        reserved-memory {
                #address-cells = <1>;
                #size-cells = <1>;
                ranges;

                nss@40000000 {
                        reg = <0x40000000 0x1000000>;
                        no-map;
                };

                smem@41000000 {
                        reg = <0x41000000 0x200000>;
                        no-map;
                };
        };

no-map”属性决定向reserved region添加内存区,还是从memory region移除内存区,二者差别在于内核不会给”no-map”属性的内存区建立内存映射,即该内存区不在动态内存管理范围

对于本开发板其memory和reserved如下

[    0.000000]  memory[0x0]     [0x00000080000000-0x0000009fffffff], 0x20000000 bytes flags: 0x0
[    0.000000]  reserved[0x0]   [0x00000080003000-0x00000080007fff], 0x5000 bytes flags: 0x0
[    0.000000]  reserved[0x1]   [0x00000080200000-0x000000810e8eeb], 0xee8eec bytes flags: 0x0
[    0.000000]  reserved[0x2]   [0x00000088000000-0x00000088014303], 0x14304 bytes flags: 0x0
[    0.000000]  reserved[0x3]   [0x0000008c000000-0x0000009fffffff], 0x14000000 bytes flags: 0x0
1
2
3
4
5
4. memblock API
几乎所有的 memblock 相关的 APIs 都在文件:mm/memblock.c 中进行了实现,最为常见的有

/*
* 1. 基本接口
*/
// 向memory区中添加内存区域.
memblock_add(phys_addr_t base, phys_addr_t size)
 
// 向memory区中删除区域.
memblock_remove(phys_addr_t base, phys_addr_t size)
 
// 申请内存
memblock_alloc(phys_addr_t size, phys_addr_t align)

// 释放内存
memblock_free(phys_addr_t base, phys_addr_t size)
  
/*
* 2. 查找 & 遍历
*/
// 在给定的范围内找到未使用的内存
phys_addr_t memblock_find_in_range(phys_addr_t start, phys_addr_t end, phys_addr_t size, phys_addr_t align)
 
// 反复迭代 memblock
for_each_mem_range(i, type_a, type_b, nid, flags, p_start, p_end, p_nid)
 
/*
* 3. 获取信息
*/
//  获取内存区域信息
phys_addr_t get_allocated_memblock_memory_regions_info(phys_addr_t *addr);
//  获取预留内存区域信息
phys_addr_t get_allocated_memblock_reserved_regions_info(phys_addr_t *addr);
 
/*
* 4. 打印
*/
#define memblock_dbg(fmt, ...) \
    if (memblock_debug) printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__)

5. memblock调试
如果需要了解memblock的详细分配流程,可以通过在bootargs中加入“memblock=debug”。

在内核启动后,通过/proc/kmsg查看调试信息。

查看内存地址范围和reserved区域可以通过:

/sys/kernel/debug/memblock/memory

/sys/kernel/debug/memblock/reserved

6. 总结
此时memblock的初始化工作已经基本完成了,memblock管理算法将可用可分配的内存在memblock.memory进行管理,已分配的内存在memblock.reserved进行管理,只要内存加入到reserved里面就表示内存已经被申请了。所以,内存申请的时候,仅把被申请到的内存加入到reserved中,并不会在memblock.memory里面有关的删去操作,对于申请和释放的操作都集中在memblock.reserved中。

start_kernel()
    |---->page_address_init()
    |     考虑支持高端内存
    |     业务:初始化page_address_pool链表;
    |          将page_address_maps数组元素按索
    |          引降序插入page_address_pool链表;
    |          初始化page_address_htable数组
    |
    |---->setup_arch(&command_line);
    |
void setup_arch(char **cmdline_p)
    |---->parse_tags(tags);
          |---->parse_tag_mem32(tag)
                |---->arm_add_memory(tag->u.mem.start, 
                |           tag->u.mem.size);
                      |---->为meminfo添加内存信息
                      |     meminfo.bank[meminfo.
                      |         nr_banks].start = start;
                      |     meminfo.bank[meminfo.
                      |         nr_banks].size = size;
                      |     meminfo.bank[meminfo.
                      |         nr_banks].node = 0;
                      |     meminfo.nr_banks++;
                      |
    |----init_mm.start_code = (unsigned long)_text;
    |    init_mm.end_code = (unsigned long)_etext;
    |    init_mm.end_data = (unsigned long)_edata;
    |    init_mm.brk = (unsigned long)_end;
    |
    |---->parse_early_param()
    |     注意,这里也会根据boot传入的command_line中信息来修
    |     正meminfo的内存信息,此处忽略(假定command_line不含内存信息)。
    |---->early_initrd(char *p)
          |     ramdisk
                |---->phys_initrd_start = start;
                |---->phys_initrd_size = size;
    |
    |---->paging_init(mdesc);
    |     bootmem位图分配器初始化,I/O空间、中断向量空间映射,
    |     PKMAP空间映射初始化,"0"页面建立.
    |---->request_standart_resources(&meminfo, mdesc);
    |
    |---->smp_init_cpus()
    |     对于2.6.34的ARM,我能说这个函数有问题么,这时做了ioremap?
    |     获取核的个数,并在cpu_possible_bits上标注核的存在性
    |
    |---->cpu_init()
    |     为每个核的irq、abt、und状态设置栈,每个状态只有12字节
    |     栈空间(static struct stack stacks[NR_CPUS]),因为
    |     基本所有的事情都在svc状态即被处理
    |
    |---->tcm_init()//tightly coupled memory, tks gaohao
    ||
    |---->early_trap_init()
          |---->memcpy(vectors, __vectors_start,
          |           __vectors_end - __vectors_start);
          |     memcpy(vectors + 0x200, __stubs_start,
          |           __stubs_end - __stubs_start);
          |     拷贝中断向量
          |---->memcpy(vectors + 0x1000 - kuser_sz, 
          |           __kuser_helper_start, kuser_sz);
          |     ARM的特殊之处,为用户态进行原子操作提供接口,
          |     即用户态直接进入该部分(3G~4G),中断处将做
          |     特别检查和相应的处理.见__kuser_helper_version
          | 
          |---->memcpy(KERN_SIGRETURN_CODE, sigreturn_codes,
          |            sizeof(sigreturn_codes));
          |---->memcpy(KERN_RESTART_CODE, syscall_restart_code,
          |            sizeof(syscall_restart_code));
          |
          |---->flush_icache_range(vectors, vectors + PAGE_SIZE);
          |---->modify_domain(DOMAIN_USER, DOMAIN_CLIENT);

//paging_init 非常重要:

void pagint_init(struct machine_desc *mdesc)
    |---->build_mem_type_table()
    |     此处没有深入查看ARM的页表项,
    |     ARM的页表项和unicore不同,我的疑问在于:
    |     ARM页表项中没有提供Dirty、Accessed位,那么kswap线程进行页面回收时,
    |     它是怎样判定该操作哪些页?关于页表项就按unicore的理解,比较简单. 
    ||---->sanity_check_meminfo();
    |     以一块2G DRAM为例,前期meminfo.nr_banks = 1;
    |     开启高端内处支持,则需将meminfo分成两个bank,
    |     (为什么以bank作为变量名,DRAM的物理组成就有bank的概念,
    |      此处需要作出区分)
         |---->struct membank *bank = &meminfo.bank[0];
         |     memove(bank + 1, bank, sizef(*bank))
         |     meminfo.nr_banks++;
         |     bank[1].size -= VMALLOC_MIN - __va(bank->start);
         |     bank[1].start = __pa(VMALLOC_MIN - 1) + 1;
         |     bank[1].highmem = 1;
         |     bank->size = VMALLOC_MIN - __va(bank->start);
         |
    |---->prepare_page_table();
    |     将swapper_pg_dir处的页表清除(部分页表项已缓存在TLB中,在
    |     bootmem_init中会间接调用create_mapping(&map),其中会再次建立)
    |
    |---->bootmem_init();
    |     bootmem分配器初始化.
    |
    |---->devicemaps_init(mdesc);
    |     为中断向量和I/O空间的虚拟与物理地址建立映射关系
    |
    |---->kmap_init()
    |     永久映射区域保留,对于ARM,该区域位于3G-4M ~ 3G
    |
    |---->top_pmd = pmd_off_k(0xffff0000);
    |     记录0xffff0000相应的一级页表项地址.
    |---->zero_page = alloc_bootmem_low_pages(PAGE_SIZE);
    |     分配一个“0”页面.
    |---->empty_zero_page = virt_to_page(zero_page);
    |     管理"0"页面所对应的struct page虚拟地址.
    |---->__flush_dache_page(NULL, empty_zero_page);

//bootmem_init 完成位图分配器的建立,bootmem_init也使用了位图分配器进行内存分配

void bootmem_init(void)
    |---->struct meminfo *mi = &meminfo;
    |     sort(&mi->bank, mi->nr_banks, sizeof(mi->bank[0]),
    |          meminfo_cmp, NULL);
    |     将meminfo中的bank数组元素按其start地址升序排序
    |
    |---->int initrd_node = 0;
    |     initrd_node = check_initrd(mi);
    |     ramdisk在meminfo下的哪个bank
    |     check_initrd(mi)
          |-->struct membank *bank = &mi->bank[i];
          |   if (bank_phys_start(bank) <= phys_initrd_start &&
          |         end <= bank_phys_end(bank))
          |             initrd_node = bank->node;
          |     
          |     return initrd_node
          |
    |---->for_each_node(node)
    |     UMA体系,只有一个node, 仅循环一次
    |     |---->find_node_limits(node, mi, &min, &node_low, &node_high);
    |     |     此处两个bank(高、低)
    |     |     min:物理内存的最小页帧号(pfn)
    |     |     node_low:物理内存中低端内存的最大页帧号
    |     |     node_high:物理内存中高端内存的最大页帧号
    |     |    
    |     |     max_low:物理内存中低端内存的最大页帧号
    |     |     max_high:物理内存中高端内存的最大页帧号
    |     |
    |     |---->bootmem_init_node(node, mi, min, node_low);
    |     |     详见后文标注;
    |     |     业务在于:将低端内存部分与虚拟空间做固定偏移映射,而且采用一级页表完成;
    |     |             采集位图分配器信息,并存放在contig_page_data.bdata
    |     |             内,而且将位图分配器自身所占用的物理内存在位图分配器内标记为
    |     |             占用,此位图分配器暂时只管理低端内存(依据meminfo.bank[0],
    |     |             未使用meminfo.bank[1]).
    |     |
    |     |           
    |     |---->reserve_node_zero(&contig_page_data)
    |     |     |---->reserve_bootmem_node(pgdat, __pa(_stext), 
    |     |     |           _end - _stext, BOOTMEM_DEFAULT);
    |     |     |     把内核中内核所占物理内存在位图分配器中标记为被占用
    |     |     |
    |     |     |---->reserve_bootmem_node(pgdat, __pa(swapper_pg_dir),
    |     |     |      PTRS_PER_PGD * sizeof(pgd_t), BOOTMEM_DEFAULT);
    |     |     |     把0进程的一级页表所占用的物理内存标记为被占用,
    |     |     |     该一级页表是我们迄今为止惟一没有在内核编译时所占用的空间
    |     |     |
    |     |---->bootem_reserve_initrd(node)
    |     |     |---->res = reserve_bootmem_node(pgdat, 
    |     |     |                               phys_initrd_start, 
    |     |     |                phys_initrd_size, BOOTMEM_EXCLUSIVE);
    |     |     |     这里有个疑问:为什么是BOOTMEM_EXCLUSIVE
    |     |     |---->initrd_start = __phys_to_virt(phys_initrd_start);
    |     |     |     initrd_end = initrd_start + phys_initrd_size;
    |     |     |     文件系统的虚拟起始地址和结束地址
    |     |     |
    |---->for_each_node(node)
    |     UMA体系,只有一个node, 仅循环一次
    |     |---->find_node_limits(node, mi, &min, &max_low, &max_high);
    |     |     此处两个bank(高、低)
    |     |     min:物理内存的最小页帧号(pfn)
    |     |     max_low:物理内存中低端内存的最大页帧号
    |     |     max_high:物理内存中高端内存的最大页帧号
    |     |
    |     |---->unsigned long zone_size[MAX_NR_ZONES], 
    |     |         zhole_size[MAX_NR_ZONES];
    |     |     memset(zone_size, 0, sizeof(zone_size));
    |     |
    |     |     zone_size[0] = max_low - min;
    |     |     ZONE_NORMAL区的页帧数
    |     |
    |     |     zone_size[ZONE_HIGHMEM] = max_high - max_low;
    |     |     ZONE_HIGHMEM的页帧
    |     |
    |     |     memcpy(zhole_size, zone_size, sizeof(zhole_size));
    |     |     从zhole_size的各个区中减去各个zone_size,
    |     |     结果是zhole_size数组元素都为0
    |     |
    |     |---->free_area_init_node(node, zone_size, min, zhole_size);
    |     |     完善contig_page_data,并调用重量级函数:
    |     |        free_area_init_core
    |     |
    |---->high_memory = __va((max_low << PAGE_SHIFT) - 1) + 1;
    |     获取高端内存的起始虚拟地址
    |
    |---->max_low_pfn = max_low - PHYS_PFN_OFFSET;
    |     低端内存所对应的页帧数
    |
    |---->max_pfn = max_high - PHYS_PFN_OFFSET;
    |     总共的物理内存页帧数

void free_area_init_node(int nid, unsigned long *zones_size,
        unsigned long node_start_pfn, unsigned long *zholes_size)
    |---->pg_data_t *pgdat = &contig_page_data;
    |     pgdat->node_id = nid; (即0)
    |     pgdat->node_start_pfn = node_start_pfn;
    |         物理内存起始地址的页帧号
    |
    |---->calculate_node_totalpages(pgdat, zones_size, zholes_size);
    |     |---->totalpages = 该pgdata下的各个区(zone)所含页的页数
    |     |---->pgdat->node_spanned_pages = totalpages;
    |     |---->realtotalpages = totalpages;
    |     |---->realtotalpages -= 该pgdata下各个区(zone)所含的洞的页数
    |     |     对于连续型,实际上不存在“洞”
    |     |---->pgdat->node_present_pages = realtotalpages;
    |     |
    |---->alloc_node_mem_map(pgdat);
    |     为pglist_data建立mem_map(struct page数组)
    |     |---->start = pgdat->node_start_pfn & 
    |     |               ~(MAX_ORDER_NR_PAGES - 1);
    |     |     因为最后要迁移到伙伴系统,因此做了调整
    |     |
    |     |---->end = pgdat->node_start_pfn + pgdat->node_spanned_pages;
    |     |     end = ALIGN(end, MAX_ORDER_NR_PAGES);
    |     |
    |     |---->size = (end - start) * sizeof(struct page);
    |     |     为了管理pglist所跨越的总的页数目,首先获得需要申请的
    |     |     struct page实例的内存大小.
    |     |
    |     |---->struct page *map = NULL;
    |     |     map = alloc_bootmem_node(pgdat, size);
    |     |     依bootmem位图分配器申请内存
    |     |__alloc_bootmem_node(pgdat, size, SMP_CACHE_BYTES,
    |     |                    __pa(MAX_DAM_ADDRESS))
    |     |  |---->ptr = alloc_bootmem_core(pgdat->bdata, size, 
    |     |  |                   align, goal, 0);
    |     |  |     若位图中出现连续的未被占用的页数满足size的要求,则将在位图中
    |     |  |     找到的相应bit位置1(标记被占用),并将对应物理页清0,返回对应
    |     |  |     物理页的虚拟起始地址.
    |     |  |     return ptr;
    |     |  |
    |     |---->pgdat->node_mem_map = map + (pgdat->node_start_pfn
    |     |                                  - start);
    |     |     终于为pglist_data的node_mem_map域建立好了空间,所有的
    |     |     struct page 实例均存于该空间内.
    |     |---->mem_map = (&contig_page_data)->node_mem_map
    |     |
    |---->free_area_init_core(pgdat, zones_size, zholes_size)
    |     |详见下文
    |     | 初始化pgdat下的各个zone及相关信息

void free_area_init_core(struct pglist_data *pgdat,
        unsigned long *zones_size, 
        unsigned long *zholes_size)
    |---->init_waitqueue_head(&pgdat->kswapd_wait);
    |     pgdat->kswapd_max_order = 0;
    |
    |---->pgdat->nr_zones = 0;
    |
    |---->for(j = 0; j < MAX_NR_ZONES; j++)
    |     依次建立pglist_data下的每个zone.
    |
    |     struct zone *zone = pgdat->node_zones + j;
    |     unsigned long size, realsize, memmap_pages;
    |     enum lru_list l;  
    |     |
    |     |---->size = zone_spanned_pages_in_node(nid, j, zones_size);
    |     |     获取该区所跨越的页的总数
    |     |     
    |     |     realsize = size - zone_absent_pages_in_node(nid, j,
    |     |                zholes_size);
    |     |     获取该区实际可用的物理页的总数(除去“洞”)
    |     |
    |     |---->memmap_pages = PAGE_ALIGN(size * sizeof(struct page))
    |     |                    >> PAGE_SHIFT;
    |     |     获取因管理该区所使用的struct page实例的内存大小
    |     |
    |     |---->realisze -= memmap_pages;
    |     |     获取该区实际可用的物理页的总数(除去管理结构所占用页数)
    |     |
    |     |---->if(!is_highmem_idx(j)) nr_kernel_pages += realsize;
    |     |     将非高端内存区中,还未被所占用的页数计入nr_kernel_pages
    |     |
    |     |---->nr_all_pages += realsize;
    |     |     将所有还未被占用的页数计入nr_all_pages
    |     |
    |     |
    |     |开始为pglist_data下的各个区建立信息
    |     |---->zone->spanned_pages = size;
    |     |     将该区跨越的页数存入pglist_data下相应的
    |     |     zone->spanned_pages.
    |     |---->zone->present_pages = realsize;
    |     |     将该区可以使用的实际页数存入pglist_data下相应的
    |     |     zone->present_pages.
    |     |---->zone->name = zones_names[j];
    |     |     为pglist_data下相应的zone添加名称
    |     |---->spin_lock_init(&zone->lock);
    |     |     spin_lock_init(&zone->lru_lock);
    |     |---->zone->zone_pgdat = pgdat;
    |     |     记录zone所在的pglist_data
    |     |---->zone->pre_priority = DEF_PRIORITY
    |     |---->zone_pcp_init(zone);
    |     |     WHAT:????????????????????
    |     |---->for_each_lru(l)
    |     |     {INIT_LIST_HEAD(&zone->lru[l].list);
    |     |      zone->reclaim_stat.nr_saved_scan[l] = 0;}
    |     |---->zone->reclaim_stat.recent_rotated[0] = 0;
    |     |     zone->reclaim_stat.recent_rotated[1] = 0;
    |     |     zone->reclaim_stat.recent_scanned[0] = 0;
    |     |     zone->reclaim_stat.recent_scanned[1] = 0;
    |     |---->memset(zone->vm_stat, 0, sizeof(zone->vm_stat);     
    |     |---->zone->flags = 0;
    |     |
    |     |---->setup_usemap(pgdat, zone, size);
    |     |     将管理该zone中的pageblock的比特位图的起始地址
    |     |     存入zone->pageblock_flags.
    |     |
    |     |---->init_currently_empty_zone(zone, zone_start_pfn,
    |     |         size, MEMMAP_EARLY);
    |     |     详见下文
    |     |     分配zone的hash资源(用于进程请求页时阻塞);
    |     |     初始化zone的free_area,以及free_area元素下
    |     |     的各类free_list.
    |     |
    |     |---->memmap_init(size, nid, j, zone_start_pfn)
    |     |     即:memmap_init_zone(size, nid, j, 
    |     |                   zone_start_pfn, MEMMAP_EARLY);
    |     |     详细见下文
    |     |     该函数的业务: 
    |     |        修正最高的页帧数highest_memap_pfn;
    |     |        获取zone所管理的页对应的struct page实例,
    |     |        在struct page中的flags中标注各种标志;
    |     |        将页所隶属的pageblock的位图标记为MIGRATE_MOVABLE;
    |     |
    |     |---->zone_start_pfn += size;

void memmap_init_zone(unsigned long size, int nid, 
    unsigned long zone,                                             
    unsigned long start_pfn, enum memmap_context context)   
    |---->struct page *page = NULL;
    |     unsigned long end_pfn = start_pfn + size;
    |     unsigned long pfn = 0;
    |     struct zone *z = NULL;
    |
    |---->if(highest_memmap_pfn < end_pfn - 1)
    |        highest_memap_pfn = end_pfn - 1;
    |      修正最高的页帧数
    |
    |---->z = &NODE_DATA(nid)->node_zones[zone];
    |     获取需要操作的zone
    |
    |-->for(pfn = start_pfn; pfn < end_pfn; pfn++)
        |-->page = pfn_to_page(pfn);
        |   获取页帧号所对应的struct page实例地址
        |
        |-->set_page_links(page, zone, nid, pfn);
        |   |-->set_page_zone(page, zone);
        |   |   在struct page->flags中记录该页是属于哪个zone
        |   |-->set_page_node(page, node);
        |   |   set_page_section(page, pfn_to_section_nr(pfn);
        |   |   对于单个node,实际上无需在page->flags中
        |   |   存储node,section信息.
        |   |
        |-->init_page_count(page)
        |   |-->atomic_set(&page->_count, 1);
        |   |   page的访问计数,当为0时,说明page是空闲的,当大于0的时
        |   |   候,说明page被一个或多个进程正在使用该页或者有进程在等待该页.
        |   |   .
        |   |
        |-->reset_page_mapcount(page)
        |   |-->atomic_set(&(page)->_mapcount, -1);
        |   |
        |-->SetPageReserved(page);
        |   | 关于SetPageReserved请参阅:page-flags.h
        |   | 定义了许多宏以及page->flags各位的意义.
        |   |
        |-->INIT_LIST_HEAD(&page->lru)
        |   |
        |--->set_pageblock_migratetype(page, MIGRATE_MOVABLE);
        |    实际上此处是先测试,若满足条件再执行,一般直接执行也没问题。
        |    我们已经知道,内存中的一些页隶属于同一个pageblock,
        |    而且内存所对应的zone中,已存储了管理pageblock的位图
        |    pageblock_flags的起始地址。此函数的任务在于将每个page
        |    所属于的pageblock标记为MIGRATE_MOVABLE(即:属于该
        |    pageblock 中的页均MIGRATE_MOVABLE)

void setup_usemap(struct pglist_data *pgdat,
                  struct zone *zone, unsigne long zonesize)
    |---->unsigned long usemapsize = usemap_size(zonesize);
    |     每个zone中的页按pageblock被分成几个block,一个
    |     pageblock所含页数为(1 << (MAX_ORDER - 1)),每个
    |     pageblock需要几个bit位来存储信息(这几个bit位的
    |     作用,暂时不知道),usemap_size的作用就在于计算
    |     该zone中的pageblock数所对应的bit位数,并转化成字节数.
    |
    |---->zone->pageblock_flags = alloc_bootmem_node(pgdat, usemapsize);
    |     将管理该zone中的pageblock的比特位图的起始地址
    |     存入zone->pageblock_flags.

当对一个page做I/O操作的时候,I/O操作需要被锁住,防止不正确的数据被访问。进程在访问page前,调用wait_on_page()函数,使进程加入一个等待队列。访问完后,UnlockPage()函数解锁其他进程对page的访问。其他正在等待队列中的进程被唤 醒。每个page都可以有一个等待队列,但是太多的分离的等待队列使得花费太多的内存访问周期。替代的解决方法,就是将所有的队列放在struct zone数据结构中。    
                                                                                               
如果struct zone中只有一个队列,则当一个page unlock的时候,访问这个zone里内存page的所有休眠的进程将都被唤醒,这样就会出现拥堵(thundering herd)的问题。建立一个哈希表管理多个等待队列,能解解决这个问题,zone->wait_table就是这个哈希表。哈希表的方法可能还是会造成一些进程不必要的唤醒。
int init_currently_empty_zone(struct zone *zone,
        unsigned long zone_start_pfn,
        unsigned long size,
        enum memmap_context_context)
    |---->zone_wait_table_init(zone, size);
    |     初始化zone下的hash表(用于进程等待页资源时使用,
    |     我们可以将等待对列存放在各个struct page内,但是
    |     这样会使struct page结构体空间太大,造成浪费,
    |     因此放在了zone中,并用hash表实现).
    |     |---->zone->wait_table_hash_nr_entries = 
    |     |       wait_table_hash_nr_entries(size);
    |     |     获取所需的hash表的数组元素个数
    |     |
    |     |---->zone->wait_table_bits = 
    |     |       wait_table_bits(zone->wait_table_hash_nr_entries);
    |     |     获取值wait_table_hash_nr_entries中首个bit位值为1的序号
    |     |     (从最低位0开始记起,例如1,则获取值为0)
    |     |
    |     |---->alloc_size = zone->wait_table_hash_nr_entries *
    |     |          sizeof(wait_queue_head_t);
    |     |     获取所需的hash表的数组所需空间大小
    |     |
    |     |---->zone->wait_table = (wait_queue_head_t *)
    |     |       alloc_bootmem_node(pgdat, alloc_size);
    |     |     分配hash表数组空间
    |     |
    |     |---->init_waitqueue_head(
    |     |     zone->wait_table[0...wait_table_hash_nr_entries]);
    |     |     初始化各个队列头
    |     |
    |---->pgdat->nr_zones = zone_idx(zone) + 1;
    |     更新pgdat下的zone的数目   
    |
    |---->zone->zone_start_pfn = zone_start_pfn;
    |
    |---->zone_init_free_lists(zone);
          |-->for(order = 0; order < MAX_ORDER; order++)
          |     for(type = 0; type < MIGRATE_TYPES; type++)
          |     {INIT_LIST_HEAD(&zone->free_area[order].free_list[type]);
          |      zone->free_area[order].nr_free = 0;}
          |   可以看出,每个zone除了被分为pageblock外,
          |   还被分为数个free_area, 每个free_area又被
          |   分为不同类型的free_list,各个free_area下
          |   的各自的free_list所含的页数是下同的.
          |

static void bootmem_init_node(int node, struct meminfo *mi,
        unsigned int start_pfn, unsigned long end_pfn)
    |---->unsigned long boot_pfn;
    |     unsigned int boot_pages;
    |     pg_data_t *pgdat;
    |     int i;
    |
    |---->for_each_nodebank(i, mi, node)
    |     i依次取得meminfo中的bank索引
    |     struct membank *bank = &mi->bank[i];
    |     if(!bank->highmem) map_memory_bank(bank);
    |     对于低端内存所在的bank,需执行map_memory_bank(bank);
    |    
    |     map_memory_bank(bank)
          |---->struct map_desc map;
          |     map.pfn = bank_pfn_start(bank);
          |     map.virtual = __phys_to_virt(bank_phys_start(bank));
          |     map.length = banks_phys_size(bank);
          |     map.type = MT_MEMORY;
          |     create_mapping(&map);
          |     |---->此处以超页映射(低端内存,一级页表即可完成映射,
          |     |     ,减少TLB刷新)
          |
    |---->boot_pages = bootmem_bootmap_pages(end_pfn - start_pfn)
    |     对于低端内存,先用位图进行管理,获取bit位所需的页数
    |
    |---->boot_pfn = find_bootmap_pfn(node, mi, boot_pages);
    |     获取内核结束地址的页号,作为寻找位图页的起始页
    |
    |---->pg_data_t *pgdat = NODE_DATA(node);
    |---->init_bootmem_node(pgdat, boot_pfn, start_pfn, end_pfn);
          |---->init_bootmem_core(pgdat->bdata, boot_pfn, 
          |                       start_pfn, end_pfn);
          |     见后文对此函数的标注
          |
    |---->for_each_nodebank(i, mi, node)
    |     i依次取得meminfo中的bank索引
    |     struct membank *bank = &mi->bank[i];
    |     if(!bank->highmem)
    |        free_bootmem_node(pgdat, bank_phys_start(bank),
    |                          bank_phys_size(bank));                                
    |     对于低端内存所在的bank,需执行free_bootmem_node
    |
    |     free_bootmem_node---->
    |     mark_bootmem_node(pgdat->bdata, start, end, 0, 0)
    |           start为低端内存起始物理页帧号,
    |           end为低端内存终止页帧号
          |---->__free(bdata, sidx, eidx);
          |     sidx:低端内存起始页号(需减去bdata->node_min_pfn);
          |     eidx:低端内存终止页号(需减去bdata->node_min_pfn);
          |     业务:将bdata中的页图标注为未被占用
          |
    |---->reserve_bootmem_node(pgdat, boot_pfn << PAGE_SHIFT,
    |          boot_pages << PAGE_SHIFT, BOOTMEM_DEFAULT);  
    |     业务:即将位图分配器自身所占用的内存标记为被占用
          |---->mark_bootmem_node(pgdata->bdata, start, end, 1, 0);
          |     start:低端内存中,位图所占用的物理内存起始页帧号
          |     end:低端内存中,位图所占用的物理内存终止页帧号
                |---->sidx = start - bdata->node_min_pfn;
                |     eidx = end - bdata->node_min_pfn;
                |---->__reserve(bdata, sidx, eidx, flags)
                |     sidx:低端内存起始页号(需减去bdata->node_min_pfn);
                |     eidx:低端内存终止页号(需减去bdata->node_min_pfn);
                |     业务:将bdata中的页图标注为被占用
                |

unsigned long init_bootmem_core(bootmem_data_t *bdata,
        unsigned long mapstart, unsigned long start, unsigned long end)
    |---->bdata->node_bootmem_map = phys_to_virt(PFN_PHY(mapstart));
    |     bdata->node_bootmem_map存放位图页的虚拟地址
    |---->bdata->node_min_pfn = start;
    |     存放低端内存的起始物理页号
    |---->bdata->node_low_pfn = end;
    |     存放低端内存的结束物理页号
    |
    |---->link_bootmem(bdata);
    |     将bdata按照node_min_pfn值的升序顺序插入到bdata_list链表中
    |---->unsigned long mapsize = bootmap_bytes(end - sart);
    |     获取位图所需的字节数  
    |     memset(bdata->node_bootmem_map, 0XFF, mapsize);
    |     将位图全部标记为已被占用(后期会再做修改, 注意文件系统位置)

static void devicemaps_init(struct machine_desc *mdesc)
    |---->void *vectors = NULL;
    |     vectors = alloc_bootmem_low_pages(PAGE_SIZE);
    |     为中断向量申请内存空间,
    |     实际上仍是通过alloc_bootmem_core函数完成内存分配.
    |
    |---->for(addr = VMALLOC_END; addr; addr += PGDIR_SIZE)
    |          pmd_clear(pmd_off_k(addr))
    |     将VMALLOC_END ~ 4G的页表映射全部清除
    |---->map.pfn = __phys_to_pfn(virt_to_phys(vectors));
    |     map.virtual = CONFIG_VECTORS_BASE;
    |     map.length = PAGE_SIZE;
    |     map.type = MT_HIGH_VECTORS;
    |     create_mapping(&map);
    |     将物理地址和虚拟地址建立映射关系,此处即为:
    |     为中断向量虚拟地址寻找一个物理页面,并且建立映射关系.
    |    
    |---->mdesc->map_io()
    |     为I/O空间建立映射,注意页表中cache的属性,
    |     这部分完全和SOC设计相关,将需要建立的映射关系
    |     存放于一个struct map_desc实例数组中,调用
    |     create_mapping完成I/O空间映射.
    |---->local_flush_tlb_all();
    |     flush_cache_all();
    |     同步硬件缓存与物理内存.
void kmap_init(void)
    |---->pmd_t *pmd = pmd_off_k(PKMAP_BASE);
    |     获取PKMAP_BASE虚拟地址所对应的一级页表项的的地址
    |     关于PKMAP_BASE,网上有很多都说是接近4G,但是我只在
    |     X86上看到是这样,而ARM或者unicore都是:
    |     PAGE_OFFSET - PMD_SIZE 暂时不知作出改动的原因.
    |---->pte_t *pte = alloc_bootmem_low_pages(
    |          PTRS_PER_PTR * sizeof(pte_t);
    |     PKMAP空间需做二级页表映射,此处获得二级页表的起始
    |     地址.
    |---->__pmd_populate(pmd, __pa(pte) | 
    |                    _PAGE_KERNEL_TABLE);
    |     在相应的一级页表项中计入二级页表项的物理地址,并设置好
    |     一级页表项的属性.
    |---->pkmap_page_table = pte + PTRS_PER_PTE
    |     记录PKMAP虚拟空间的二级页表项的物理末尾地址.

//request_stanard_resources描述地有些不准确

void request_standard_resources(struct meminfo *mi,
        struct machine_desc *mdesc)
    |---->kernel_code.start = virt_to_phys(_text);
    |     kernel_code.end = virt_to_phys(_etext - 1);
    |     kernel_data.strt = virt_to_phys(_data);
    |     kernel_data.end = virt_to_phys(_end - 1);
    |
    |---->for(i = 0; i < mi->nr_banks; i++)
    |     此处是将meminfo的资源放入iomem_resource树中,
    |     同时将内核镜像资源也放入iomem_resource树中.
    |     注意内核镜像资源如何放入.
    |
    |     struct *res = NULL;
    |     res = alloc_bootmem_low(sizeof(*res));
    |     res->name = "System RAM"
    |     res->start = mi->bank[i].start;
    |     res->end = mi->bank[i].start + mi->bank[i].size - 1;
    |     res->flags = IORESOURCE_MEM | IORESOURCE_BUSY;
    |
    |     request_resource(&iomem_resource, res)
    |     将内存资源放入iomem_resource树中.
    |     
    |     if(kernel_code.start >= res->start &&
    |        kernel_code.end <= res->end)
    |            request_resource(res, &kernel_code);
    |     if(kernel_data.start >= res->start &&
    |       kernel_data.end <= res->end)
    |            request_resource(res, &kernel_data);
    |     将内核镜像资源放入iomem_resource树中.

bootmem_init
    |---->bootmem_init_node
        |---->map_memory_bank(bank)        对内存0x3000_0000,64M空间进行映射
        |
        |
        |---->bootmem_bootmap_pages
        |---->find_bootmap_pfn
        |
        |
        |---->init_bootmem_node
        |
        |---->free_bootmem_node
        |
        |---->reserve_bootmem_node
        |---->reserve_node_zero
        |
        |
        |---->zone_size[0] = end_pfn - start_pfn
        |
        |
        |---->arch_adjust_zones
        |
        |
        |---->free_area_init_node

1. map_memory_bank

	for_each_nodebank(i, mi, node) {
		struct membank *bank = &mi->bank[i];
		unsigned long start, end;

		start = bank->start >> PAGE_SHIFT;
		end = (bank->start + bank->size) >> PAGE_SHIFT;

		if (start_pfn > start)
			start_pfn = start;
		if (end_pfn < end)
			end_pfn = end;

		map_memory_bank(bank);
	}

static inline void map_memory_bank(struct membank *bank)
{
#ifdef CONFIG_MMU
	struct map_desc map;
	map.pfn = __phys_to_pfn(bank->start);
	map.virtual = __phys_to_virt(bank->start);
	map.length = bank->size;
	map.type = MT_MEMORY;
	
	/* 完成虚拟地址到物理地址的映射,直接填在初始化最开始的时候section位段处,映射大小是64M空间
	map.pfn = 0x30000
	map.virtual = 0xc0000000
	map.length = 0x1000000
	*/
	create_mapping(&map);
#endif
}

这部分代码会调用底层的create_mapping函数对物理内存进行一个映射。映射的实际情况是:

map_memory_bank pfn=0x30000, vitual=0xc0000000, length=0x4000000, type=0x8
creatmap pgd=0xc0007000, addr=0xc0000000, next=0xc0200000, phys=0x30000000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x600
pmd=0xc0007000, pmdcontext=0x3000041e, addr=0xc0000000, end=0xc0200000, phys=0x30000000
pmd=0xc0007004, pmdcontext=0x3010041e, addr=0xc0100000, end=0xc0200000, phys=0x30100000
creatmap pgd=0xc0007008, addr=0xc0200000, next=0xc0400000, phys=0x30200000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x601
pmd=0xc0007008, pmdcontext=0x3020041e, addr=0xc0200000, end=0xc0400000, phys=0x30200000
pmd=0xc000700c, pmdcontext=0x3030041e, addr=0xc0300000, end=0xc0400000, phys=0x30300000
creatmap pgd=0xc0007010, addr=0xc0400000, next=0xc0600000, phys=0x30400000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x602
pmd=0xc0007010, pmdcontext=0x3040041e, addr=0xc0400000, end=0xc0600000, phys=0x30400000
pmd=0xc0007014, pmdcontext=0x3050041e, addr=0xc0500000, end=0xc0600000, phys=0x30500000
creatmap pgd=0xc0007018, addr=0xc0600000, next=0xc0800000, phys=0x30600000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x603
pmd=0xc0007018, pmdcontext=0x3060041e, addr=0xc0600000, end=0xc0800000, phys=0x30600000
pmd=0xc000701c, pmdcontext=0x3070041e, addr=0xc0700000, end=0xc0800000, phys=0x30700000
creatmap pgd=0xc0007020, addr=0xc0800000, next=0xc0a00000, phys=0x30800000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x604
pmd=0xc0007020, pmdcontext=0x3080041e, addr=0xc0800000, end=0xc0a00000, phys=0x30800000
pmd=0xc0007024, pmdcontext=0x3090041e, addr=0xc0900000, end=0xc0a00000, phys=0x30900000
creatmap pgd=0xc0007028, addr=0xc0a00000, next=0xc0c00000, phys=0x30a00000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x605
pmd=0xc0007028, pmdcontext=0x30a0041e, addr=0xc0a00000, end=0xc0c00000, phys=0x30a00000
pmd=0xc000702c, pmdcontext=0x30b0041e, addr=0xc0b00000, end=0xc0c00000, phys=0x30b00000
creatmap pgd=0xc0007030, addr=0xc0c00000, next=0xc0e00000, phys=0x30c00000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x606
pmd=0xc0007030, pmdcontext=0x30c0041e, addr=0xc0c00000, end=0xc0e00000, phys=0x30c00000
pmd=0xc0007034, pmdcontext=0x30d0041e, addr=0xc0d00000, end=0xc0e00000, phys=0x30d00000
creatmap pgd=0xc0007038, addr=0xc0e00000, next=0xc1000000, phys=0x30e00000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x607
pmd=0xc0007038, pmdcontext=0x30e0041e, addr=0xc0e00000, end=0xc1000000, phys=0x30e00000
pmd=0xc000703c, pmdcontext=0x30f0041e, addr=0xc0f00000, end=0xc1000000, phys=0x30f00000
creatmap pgd=0xc0007040, addr=0xc1000000, next=0xc1200000, phys=0x31000000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x608
pmd=0xc0007040, pmdcontext=0x3100041e, addr=0xc1000000, end=0xc1200000, phys=0x31000000
pmd=0xc0007044, pmdcontext=0x3110041e, addr=0xc1100000, end=0xc1200000, phys=0x31100000
creatmap pgd=0xc0007048, addr=0xc1200000, next=0xc1400000, phys=0x31200000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x609
pmd=0xc0007048, pmdcontext=0x3120041e, addr=0xc1200000, end=0xc1400000, phys=0x31200000
pmd=0xc000704c, pmdcontext=0x3130041e, addr=0xc1300000, end=0xc1400000, phys=0x31300000
creatmap pgd=0xc0007050, addr=0xc1400000, next=0xc1600000, phys=0x31400000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x60a
pmd=0xc0007050, pmdcontext=0x3140041e, addr=0xc1400000, end=0xc1600000, phys=0x31400000
pmd=0xc0007054, pmdcontext=0x3150041e, addr=0xc1500000, end=0xc1600000, phys=0x31500000
creatmap pgd=0xc0007058, addr=0xc1600000, next=0xc1800000, phys=0x31600000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x60b
pmd=0xc0007058, pmdcontext=0x3160041e, addr=0xc1600000, end=0xc1800000, phys=0x31600000
pmd=0xc000705c, pmdcontext=0x3170041e, addr=0xc1700000, end=0xc1800000, phys=0x31700000
creatmap pgd=0xc0007060, addr=0xc1800000, next=0xc1a00000, phys=0x31800000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x60c
pmd=0xc0007060, pmdcontext=0x3180041e, addr=0xc1800000, end=0xc1a00000, phys=0x31800000
pmd=0xc0007064, pmdcontext=0x3190041e, addr=0xc1900000, end=0xc1a00000, phys=0x31900000
creatmap pgd=0xc0007068, addr=0xc1a00000, next=0xc1c00000, phys=0x31a00000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x60d
pmd=0xc0007068, pmdcontext=0x31a0041e, addr=0xc1a00000, end=0xc1c00000, phys=0x31a00000
pmd=0xc000706c, pmdcontext=0x31b0041e, addr=0xc1b00000, end=0xc1c00000, phys=0x31b00000
creatmap pgd=0xc0007070, addr=0xc1c00000, next=0xc1e00000, phys=0x31c00000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x60e
pmd=0xc0007070, pmdcontext=0x31c0041e, addr=0xc1c00000, end=0xc1e00000, phys=0x31c00000
pmd=0xc0007074, pmdcontext=0x31d0041e, addr=0xc1d00000, end=0xc1e00000, phys=0x31d00000
creatmap pgd=0xc0007078, addr=0xc1e00000, next=0xc2000000, phys=0x31e00000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x60f
pmd=0xc0007078, pmdcontext=0x31e0041e, addr=0xc1e00000, end=0xc2000000, phys=0x31e00000
pmd=0xc000707c, pmdcontext=0x31f0041e, addr=0xc1f00000, end=0xc2000000, phys=0x31f00000
creatmap pgd=0xc0007080, addr=0xc2000000, next=0xc2200000, phys=0x32000000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x610
pmd=0xc0007080, pmdcontext=0x3200041e, addr=0xc2000000, end=0xc2200000, phys=0x32000000
pmd=0xc0007084, pmdcontext=0x3210041e, addr=0xc2100000, end=0xc2200000, phys=0x32100000
creatmap pgd=0xc0007088, addr=0xc2200000, next=0xc2400000, phys=0x32200000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x611
pmd=0xc0007088, pmdcontext=0x3220041e, addr=0xc2200000, end=0xc2400000, phys=0x32200000
pmd=0xc000708c, pmdcontext=0x3230041e, addr=0xc2300000, end=0xc2400000, phys=0x32300000
creatmap pgd=0xc0007090, addr=0xc2400000, next=0xc2600000, phys=0x32400000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x612
pmd=0xc0007090, pmdcontext=0x3240041e, addr=0xc2400000, end=0xc2600000, phys=0x32400000
pmd=0xc0007094, pmdcontext=0x3250041e, addr=0xc2500000, end=0xc2600000, phys=0x32500000
creatmap pgd=0xc0007098, addr=0xc2600000, next=0xc2800000, phys=0x32600000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x613
pmd=0xc0007098, pmdcontext=0x3260041e, addr=0xc2600000, end=0xc2800000, phys=0x32600000
pmd=0xc000709c, pmdcontext=0x3270041e, addr=0xc2700000, end=0xc2800000, phys=0x32700000
creatmap pgd=0xc00070a0, addr=0xc2800000, next=0xc2a00000, phys=0x32800000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x614
pmd=0xc00070a0, pmdcontext=0x3280041e, addr=0xc2800000, end=0xc2a00000, phys=0x32800000
pmd=0xc00070a4, pmdcontext=0x3290041e, addr=0xc2900000, end=0xc2a00000, phys=0x32900000
creatmap pgd=0xc00070a8, addr=0xc2a00000, next=0xc2c00000, phys=0x32a00000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x615
pmd=0xc00070a8, pmdcontext=0x32a0041e, addr=0xc2a00000, end=0xc2c00000, phys=0x32a00000
pmd=0xc00070ac, pmdcontext=0x32b0041e, addr=0xc2b00000, end=0xc2c00000, phys=0x32b00000
creatmap pgd=0xc00070b0, addr=0xc2c00000, next=0xc2e00000, phys=0x32c00000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x616
pmd=0xc00070b0, pmdcontext=0x32c0041e, addr=0xc2c00000, end=0xc2e00000, phys=0x32c00000
pmd=0xc00070b4, pmdcontext=0x32d0041e, addr=0xc2d00000, end=0xc2e00000, phys=0x32d00000
creatmap pgd=0xc00070b8, addr=0xc2e00000, next=0xc3000000, phys=0x32e00000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x617
pmd=0xc00070b8, pmdcontext=0x32e0041e, addr=0xc2e00000, end=0xc3000000, phys=0x32e00000
pmd=0xc00070bc, pmdcontext=0x32f0041e, addr=0xc2f00000, end=0xc3000000, phys=0x32f00000
creatmap pgd=0xc00070c0, addr=0xc3000000, next=0xc3200000, phys=0x33000000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x618
pmd=0xc00070c0, pmdcontext=0x3300041e, addr=0xc3000000, end=0xc3200000, phys=0x33000000
pmd=0xc00070c4, pmdcontext=0x3310041e, addr=0xc3100000, end=0xc3200000, phys=0x33100000
creatmap pgd=0xc00070c8, addr=0xc3200000, next=0xc3400000, phys=0x33200000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x619
pmd=0xc00070c8, pmdcontext=0x3320041e, addr=0xc3200000, end=0xc3400000, phys=0x33200000
pmd=0xc00070cc, pmdcontext=0x3330041e, addr=0xc3300000, end=0xc3400000, phys=0x33300000
creatmap pgd=0xc00070d0, addr=0xc3400000, next=0xc3600000, phys=0x33400000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x61a
pmd=0xc00070d0, pmdcontext=0x3340041e, addr=0xc3400000, end=0xc3600000, phys=0x33400000
pmd=0xc00070d4, pmdcontext=0x3350041e, addr=0xc3500000, end=0xc3600000, phys=0x33500000
creatmap pgd=0xc00070d8, addr=0xc3600000, next=0xc3800000, phys=0x33600000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x61b
pmd=0xc00070d8, pmdcontext=0x3360041e, addr=0xc3600000, end=0xc3800000, phys=0x33600000
pmd=0xc00070dc, pmdcontext=0x3370041e, addr=0xc3700000, end=0xc3800000, phys=0x33700000
creatmap pgd=0xc00070e0, addr=0xc3800000, next=0xc3a00000, phys=0x33800000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x61c
pmd=0xc00070e0, pmdcontext=0x3380041e, addr=0xc3800000, end=0xc3a00000, phys=0x33800000
pmd=0xc00070e4, pmdcontext=0x3390041e, addr=0xc3900000, end=0xc3a00000, phys=0x33900000
creatmap pgd=0xc00070e8, addr=0xc3a00000, next=0xc3c00000, phys=0x33a00000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x61d
pmd=0xc00070e8, pmdcontext=0x33a0041e, addr=0xc3a00000, end=0xc3c00000, phys=0x33a00000
pmd=0xc00070ec, pmdcontext=0x33b0041e, addr=0xc3b00000, end=0xc3c00000, phys=0x33b00000
creatmap pgd=0xc00070f0, addr=0xc3c00000, next=0xc3e00000, phys=0x33c00000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x61e
pmd=0xc00070f0, pmdcontext=0x33c0041e, addr=0xc3c00000, end=0xc3e00000, phys=0x33c00000
pmd=0xc00070f4, pmdcontext=0x33d0041e, addr=0xc3d00000, end=0xc3e00000, phys=0x33d00000
creatmap pgd=0xc00070f8, addr=0xc3e00000, next=0xc4000000, phys=0x33e00000, type=0xc0360390, intit_mm_pgd=0xc0004000, pgd_index=0x61f
pmd=0xc00070f8, pmdcontext=0x33e0041e, addr=0xc3e00000, end=0xc4000000, phys=0x33e00000
pmd=0xc00070fc, pmdcontext=0x33f0041e, addr=0xc3f00000, end=0xc4000000, phys=0x33f00000

unsigned long __init init_bootmem_node(pg_data_t *pgdat, unsigned long freepfn,
                unsigned long startpfn, unsigned long endpfn)
{
    return init_bootmem_core(pgdat->bdata, freepfn, startpfn, endpfn);
}
该函数作用:
调用init_bootmem_core函数,初始化pgdat变量中的成员


先看一下pgdat变量的结构

typedef struct bootmem_data {
    unsigned long node_min_pfn;  /*表示内存起始页帧号*/
    unsigned long node_low_pfn;  /*表示内存结束页帧号*/
    void *node_bootmem_map;      /*表示bootmam_map的起始物理地址*/
    unsigned long last_end_off;  /*表示上一次分配截止地址的偏移。*/
    unsigned long hint_idx;      /*存放前一次分配的最后一个页面号*/
    struct list_head list;
} bootmem_data_t;

static unsigned long __init init_bootmem_core(bootmem_data_t *bdata,
    unsigned long mapstart, unsigned long start, unsigned long end)
{
    unsigned long mapsize;
 
    mminit_validate_memmodel_limits(&start, &end);//空函数
    bdata->node_bootmem_map = phys_to_virt(PFN_PHYS(mapstart));
    bdata->node_min_pfn = start;
    bdata->node_low_pfn = end;
    link_bootmem(bdata);
 
    /*
     * Initially all pages are reserved - setup_arch() has to
     * register free RAM areas explicitly.
     */
    mapsize = bootmap_bytes(end - start);
    memset(bdata->node_bootmem_map, 0xff, mapsize);
 
    bdebug("nid=%td start=%lx map=%lx end=%lx mapsize=%lx\n",
        bdata - bootmem_node_data, start, mapstart, end, mapsize);
 
    return mapsize;
}

假如调用函数传进来的参数值如下:
mapstart = 0x30431; start = 0x30000; end = 0x31000;
那么:
bdata->node_bootmem_map = phys_to_virt(PFN_PHYS(mapstart));
#define PFN_PHYS(x)  ((phys_addr_t)(x) << PAGE_SHIFT)


bdata->node_bootmem_map = phys_to_virt(0x30431000) = 0xc0431000
bdata->node_min_pfn = start = 0x30000
bdata->node_low_pfn = end = 0x31000


mapsize = bootmap_bytes(end - start) = 512
memset(bdata->node_bootmem_map, 0xff, mapsize)  也即
memset(bdata->node_bootmem_map, 0xff, 512)


到这里就可以看出该函数的主要作用:
1,初始化pgdat变量中的主要成员,这些成员在随后即将用到。
2,将用来管理内存状况的那512个字节的数据全部赋值为0xff,也即将4096位全部置1

10. Bootmem机制

10.1. 简介

Bootmem机制是内核在启动时对内存的一种简单的页面管理方式。 它为建立页表管理代码中的数据结构提供动态分配内存的支持,为了对页面管理机制作准备,Linux使用了一种叫bootmem分配器(bootmem allocator)的机制,这种机制仅仅用在系统引导时,它为整个物理内存建立起一个页面位图。这个位图建立在内核代码映象终点_end上方的地方。这个位图用来管理低区(可被直接一一映射的物理内存区,小于896Mb)。因为在0到896Mb的范围内,有些页面可能保留给内核代码,页目录,以及当前的位图使用,有些页面可能有空洞,因此,建立这个位图的目的就是要用一个比特位的两种状态标记物理页面的状态:已被保留;可被动态分配。Bootmem机制的核心是对Bitmap的操作,相关代码位于mm/bootmem.c和include/linux/bootmem.h中。

图 53. ARM上的Linux地址空间分布

在介绍Bootmem机制之前需要对内核的地址空间分布做一个深入的了解:

  • 32位操作系统只有4G的虚拟地址空间,通常Linux将最上的1G用于内核虚拟地址。ARM上将用户空间的3G最高处的16M用来给内核的模块使用。
  • Linux将物理内存完全一一映射到内核空间,这样很方便管理内存,任何页面的虚拟地址减去一个PAGE_OFFSET(0xc0000000)的偏移r然后加上物理地址的偏移PHYS_OFFSET就可以得到物理地址。
  • 内核还需要动态管理一些内存用于vmalloc或者设备临时映射等,因此不能将1G的虚拟空间完全一一映射物理内存,因此权衡了一个896M的大小,0xc0000000到0xc0000000 + 896M的虚拟地址空间一一映射物理内存,从0xc0000000+896M到0xffffffff的地址空间作为动态映射的需要。

对于大于896M的物理内存,是无法通过一一映射来访问的,通过vmalloc可以访问它们,但是对于大于4G的内存需要PAE的支持,否则无法访问。

10.2. bootmem_data

用来存放位图的数据结构为bootmem_data。

include/linux/bootmem.h

typedef struct bootmem_data {
        unsigned long node_min_pfn;
        unsigned long node_low_pfn;
        void *node_bootmem_map;
        unsigned long last_end_off;
        unsigned long hint_idx;
        struct list_head list;
} bootmem_data_t;
在RAM为256M的ARM板上的测试结果输出如下
.node_min_pfn:0x50000, node_low_pfn:0x60000
.node_bootmem_map:      c053c000
.last_end_off:  0x0
.hint_idx:      0x0
  • node_min_pfn/node_low_pfn表示最小和最大的物理内存页框,node_low_pfn-node_min_pfn代表物理内存的页框数,最大不能超过896Mb对应的页框数目0x38000。
  • node_bootmem_map表示存放bootmem位图的地址,即内核映象结束处的第一个页面的所在地址。
  • last_end_off记录的是上次申请的空间后的第一个相对于0偏移的物理地址。
  • hint_idx记录了最后一次申请的空间后的一个物理页框的地址。它方便下一次申请内存时使用。
  • list被用来连接到bdata_list。

10.3. UMA和NUMA

Linux对物理内存的描述机制有两种:UMA和NUMA。

在传统的计算机结构中,整个物理内存都是均匀一致的,CPU访问这个空间中的任何一个地址所需要的时间都相同,所以把这种内存称为“一致存储结构(Uniform Memory Architecture),简称UMA。可是,在一些新的系统结构中,特别是多CPU结构的系统中,物理存储空间在这方面的一致性却成了问题。这是因为,在多CPU结构中,系统中只有一条总线(例如,PCI总线),有多个CPU模块连接在系统总线上,每个CPU模块都有本地的物理内存,但是也可以通过系统总线访问其它CPU模块上的内存。另外,系统总线上还连接着一个公用的存储模块,所有的CPU模块都可以通过系统总线来访问它。因此,所有这些物理内存的地址可以互相连续而形成一个连续的物理地址空间。

显然,就某个特定的CPU而言,访问其本地的存储器速度是最快的,而穿过系统总线访问公用存储模块或其它CPU模块上的存储器就比较慢,而且还面临因可能的竞争而引起的不确定性。也就是说,在这样的系统中,其物理存储空间虽然地址连续,但因为所处“位置”不同而导致的存取速度不一致,所以称为“非一致存储结构(Non-Uniform Memory Architecture),简称NUMA。

事实上,严格意义上的UMA结构几乎不存在。就拿配置最简单的单CPU来说,其物理存储空间就包括了RAM、ROM(用于BIOS),还有图形卡上的静态RAM。但是,在UMA中,除主存RAM之外的存储器空间都很小,因此可以把它们放在特殊的地址上,在编程时加以特别注意就行,那么,可以认为以RAM为主体的主存是UMA结构。

由于NUMA的引入,就需要存储管理机制的支持,因此,Linux内核从2.4版本开始就提供了对NUMA的支持(作为一个编译可选项)。为了对NUMA进行描述,引入一个新的概念-“存储节点(或叫节点),把访问时间相同的存储空间就叫做一个“存储节点”。一般来说,连续的物理页面应该分配在相同的存储节点上。例如,如果CPU模块1要求分配5个页面,但是由于本模块上的存储空间已经不够,只能分配3个页面,那么此时,是把另外两个页面分配在其它CPU模块上呢,还是把5个页面干脆分配在一个模块上?显然,合理的分配方式因该是将这5个页面都分配在公用模块上。

mm/bootmem.c
bootmem_data_t bootmem_node_data[MAX_NUMNODES] __initdata;

Linux定义了一个大小为MAX_NUMNODES类型为bootmem_data_t的bootmem_node_data数组,数组的大小根据CONFIG_NODES_SHIFT的配置决定。对于UMA来说,NODES_SHIFT为0,所以MAX_NUMNODES的值为1。

Linux把物理内存划分为三个层次来管理:存储节点(Node)、管理区(Zone)和页面(Page)。为了支持NUMA模型,也即CPU对不同内存单元的访问时间可能不同,此时系统的物理内存被划分为几个节点(node)。在一个单独的节点内,任一给定CPU访问页面所需的时间都是相同的。然而,对不同的CPU,这个时间可能就不同。对每个CPU而言,内核都试图把耗时节点的访问次数减到最少这就要小心地选择CPU最常引用的内核数据结构的存放位置。

另外,linux内核在一些特殊的单处理器上使用NUMA,这些系统的物理地址空间中拥有巨大的"洞"。内核通过将有效物理地址的连续附属区域分配给不同的内存节点来处理这些体系结构。

每个节点中的物理内存又可以分为几个管理区(Zone)。每个节点都有一个类型为pg_data_t的描述符。对于UMA模式来说,系统中只需要描述符来定义一个节点,它被定义在mm/page_allloc.c中,名为contig_page_data。

#ifndef CONFIG_NEED_MULTIPLE_NODES
struct pglist_data __refdata contig_page_data = { .bdata = &bootmem_node_data[0] };
EXPORT_SYMBOL(contig_page_data);
#endif

对于NUMA来说,Linux在arch/arm/mm/discontig.c中定义了一个名为discontig_node_data的数组。contig_page_data和discontig_node_data均被EXPORT_SYMBOL出来,作为全局变量使用。另外注意到contig_page_data和discontig_node_data在被定义时都是指定了成员bdata的值。pg_data_t结构体中的struct bootmem_data类型成员bdata被用来在系统启动时通过bitmap管理该节点代表的内存。

pg_data_t discontig_node_data[MAX_NUMNODES] = {
  { .bdata = &bootmem_node_data[0] },
  { .bdata = &bootmem_node_data[1] },
  { .bdata = &bootmem_node_data[2] },
  { .bdata = &bootmem_node_data[3] },
  ......

10.4. Debug机制

bootmem.c中定义了bootmem_debug,将其置1,则可以查看Linux在使用bootmem机制时输出的信息。

mm/bootmem.c

static int bootmem_debug = 1;

bootmem.c中提供了一个名为bootmem_debug_setup的函数,它被用来在系统引导期间解析Bootloader传递来的参数行,如果提供了bootmem_debug=1,那么这里的bootmem_debug开关将被置为1。

static int __init bootmem_debug_setup(char *buf)
{
	bootmem_debug = 1;
	return 0;
}
early_param("bootmem_debug", bootmem_debug_setup);

如果开启bootmem_debug,Bootloader中的bootargs参数看起来应该如下所示:

bootargs=mem=64M console=ttyS1,115200n8 root=/dev/ram0 rw initrd=0xc1180000,4M bootmem_debug=1

10.5. 初始化函数

init_bootmem_core是bootmem机制中的核心函数,如果需要使用bootmem机制来管理内存,那么首先需要使用该函数来建立Bootmem allocator,并初始化位图。并且该函数只在初始化时使用。

static unsigned long __init init_bootmem_core(bootmem_data_t *bdata,
	unsigned long mapstart, unsigned long start, unsigned long end);
  • bdata存放初始化后的位图信息。
  • mapstart指明位图所要存放的物理页框。
  • start和end指明该分配器所要管理的物理内存的起止页框。

init_bootmem_core完成了以下工作:

  • 根据mapstart指定的物理页框,计算bdata中的node_bootmem_map所应该对应的虚拟地址。首先将mapstart物理页框通过PFN_PHYS转换为对应的物理地址,然后通过phys_to_virt转换为虚拟地址。
  • bdata中的node_min_pfn和node_low_pfn被分别赋值为start和end。它们记录了当前bdata可以管理的物理内存范围的起止页框。
  • 使用link_bootmem函数将bdata挂载到bdata_list全局变量中,Linux中所有的bdata都被链入bdata_list进行统一管理。链入时将根据node_min_pfn的值来确定挂载点。整个链表中的bdata节点内node_min_pfn总是从小到大排列的。
  • 最后物理页框对应的bitmap,也即node_bootmem_map指向的地址,大小为bitmap大小的区域全部置为0xff,也即全部保留。bitmap大小由bootmap_bytes计算出,其中会考虑到字节圆整,通常的公式为(pages + 7) / 8,另外要保证对齐到32。对于256M的内存来说,正好是0x2000。

对于一个使用UMA机制,物理内存为256Mb,且物理内存所在起始地址为0x50000000的系统,init_bootmem_core将打印以下信息:

bootmem::init_bootmem_core nid=0 start=50000 map=5053c end=60000 mapsize=2000

nid=0,指明当前操作的bdata对应到bootmem_node_data的数组索引,start=50000,即为0x50000000对应的物理页框。

图 54. 初始化后的位图

为了针对特定的内存节点应用Bootmem机制,bootmem.c中在init_bootmem_core的基础上封装了针对特定节点操作的init_bootmem_node函数。另外还有针对默认节点操作的init_bootmem函数,它们的调用关系如图所示:

图 55. 初始化函数调用

unsigned long __init init_bootmem_node(pg_data_t *pgdat, unsigned long freepfn,
				unsigned long startpfn, unsigned long endpfn)
{
	return init_bootmem_core(pgdat->bdata, freepfn, startpfn, endpfn);
}

注意到init_bootmem_node的第一个参数为pg_data_t类型。init_bootmem只需要两个参数,bitmap的物理页框地址start和物理页面数pages。NODE_DATA的作用就是取contig_page_data节点,而这里的所处理的物理页框起始地址永远为0。

include/linux/mmzone.h
#define NODE_DATA(nid)          (&contig_page_data)

unsigned long __init init_bootmem(unsigned long start, unsigned long pages)
{
	max_low_pfn = pages;
	min_low_pfn = start;
	return init_bootmem_core(NODE_DATA(0)->bdata, start, 0, pages);
}

通常在系统引导的时候调用init_bootmem_node来针对特定的节点初始化bootmem,而不是直接调用init_bootmem,这是因为很少有物理地址从0开始,它是由CPU的物理地址分配决定的,通常RAM占用的物理地址空间总是有一定的偏移,比如0x50000000。

10.6. __reserve和__free

不管是何种内存管理方式,最基本的功能就是内存的分发和回收,比如malloc和free。在bootmem机制中被称为__reserve和__free,分别对应bitmap中的比特位的状态1和0。所以__reserve的作用就是将对应的物理页框的比特位置为1,相当于malloc。

static int __init __reserve(bootmem_data_t *bdata, unsigned long sidx,
			unsigned long eidx, int flags);

  • bdata指明当前的保留操作作用在哪个bootmem_data区。
  • sidx和eidx代表的是需要预留的物理页框对应的bit位在bdata中的node_min_pfn的索引值。
  • flags标志,当前只支持BOOTMEM_DEFAULT和BOOTMEM_EXCLUSIVE。BOOTMEM_EXCLUSIVE可以保证在将要保留的整个页框中都是可以使用的,也即这些页框对应的bitmap连续为0,否则遇到已保留的页框,将释放已经获取的页框,并返回EBUSY。BOOTMEM_DEFAULT则不考虑这一情况。

__reserve调用test_and_set_bit来设置这一区域中的比特位,注意区域范围为[sidx + bdata->node_min_pfn, eidx + bdata->node_min_pfn)。

static void __init __free(bootmem_data_t *bdata,
			unsigned long sidx, unsigned long eidx);

__free函数在bootmeme机制中相当于通常使用的free函数。与__reserve相似,但是它调用test_and_clear_bit对需要释放的页框区对应的位图进行清零,如果在清零过程中发现该函数返回0,说明该区域中的比特位有异常翻转,调用BUG()抛出并将系统挂起。它的作用区域与__reserve一致。

注意:test_and_set_bit(0, start_addr)中,"0"不是要设置的值,而是表示start_addr中第0位需要被设置为"1"。此函数返回相应比特位上一次被设置的值。test_and_clear_bit与此相同。

10.7. alloc_bootmem_core

alloc_bootmem_core是使用bitmap分配内存空间的核心接口。

static void * __init alloc_bootmem_core(struct bootmem_data *bdata,
				unsigned long size, unsigned long align,
				unsigned long goal, unsigned long limit);

  • bdata指明当前的内存申请操作作用在哪个bootmem_data区。
  • size指明所要申请的内存空间大小,它的单位是bytes。
  • align指明申请的内存空间的边界大小,通常为32,PAGE_SIZE(4K)等。
  • goal代表从bdata中node_min_pfn搜索空闲内存的开始地址,如果为0,则默认从node_min_pfn开始。goal应该位于node_min_pfn和node_low_pfn所对应的页框所代表的地址之间,否则按0处理。
  • limit代表从bdata中node_min_pfn搜索空闲内存的终点地址,如果为0,则搜索到node_low_pfn结束。

alloc_bootmem_core尝试在goal和limit指定的虚拟地址范围[goal/node_min_pfn,limit/node_low_pfn]中分配size字节的内存,并且获取的内存与align对齐:

  • 首先检查参数的合法性。size不可为0;align必须为2的整数倍;如果limit不为0,那么需满足goal + size < limit。
  • 根据bdata中提供的node_min_pfn和node_low_pfn,以及goal和limit的值确定需要搜索的物理页框范围。最大页框max由node_low_pfn和limit共同确定,如果limit不为0,则取最小者。最小页框min取node_min_pfn和goal中最大值,并且对齐到align参数决定的step。
  • 根据公式step = max(align >> PAGE_SHIFT, 1UL)得到步进大小。右移PAGE_SHIFT用来计算align相对于PAGE_SIZE的倍数。step参数用来在搜索失败的情况下,搜索下一页框时增加的页框数目:步进大小为1,则当bitmap中bit i为占用状态时搜索bit i + 1的状态,步进大小为2则当bitmap中一个bit i为占用状态时搜索bit i + 2的状态。
  • 比较起点和上一次搜索结束的位置,从较小的位置开始搜索,如从上一次搜索结束的位置开始搜索,则设置回滚标识,如到终点还找不到合适的空间则从之前的起点开始进行搜索。
  • 根据搜索的区域和申请的内存大小,通过find_next_zero_bit函数来查找一个连续的并满足size要求的空闲内存块。如果有足够的空闲bit则将这些bit设为占用状态,并清空bit所对应的空间的数据,返回空闲空间的起始地址。
  • 更新last_end_off和hint_idx,以备下次申请内存时快速定位空闲的内存区。其中last_end_off记录了上次申请的空间后的第一个相对于0偏移的物理地址,所以本次申请的内存总是紧邻此后的并满足goal和align要求的内存。
  • 函数执行成功则通过__reserve及BOOTMEM_EXCLUSIVE标志预留对应的bitmap,memset置该区域为0,并返回申请到的内存的虚拟地址,否则返回NULL。

bootmem.c中对alloc_bootmem_core进行了一系列的扩展以完成丰富的功能。

图 56. alloc_bootmem_core调用

bootmem机制中提供的__alloc_bootmem_node和__alloc_bootmem_low_node函数被用来针对特定的节点进行内存管理。它们均通过调用___alloc_bootmem_node来实现。它们的第一个参数均为pg_data_t类型。

void * __init __alloc_bootmem_node(pg_data_t *pgdat, unsigned long size,
				   unsigned long align, unsigned long goal)
{
	return ___alloc_bootmem_node(pgdat->bdata, size, align, goal, 0);
}

__alloc_bootmem_low_node和__alloc_bootmem_node类似,唯一区别在于limit参数。ARCH_LOW_ADDRESS_LIMIT只在特定的体系架构上起作用,也即申请的内存被限制在ARCH_LOW_ADDRESS_LIMIT之下的内存中。通常它被定义为0xffffffffUL,也即是32位系统可以支持的最大虚拟地址,此时它的作用与0参数相同。

void * __init __alloc_bootmem_low_node(pg_data_t *pgdat, unsigned long size,
				       unsigned long align, unsigned long goal)
{
	return ___alloc_bootmem_node(pgdat->bdata, size, align,
				goal, ARCH_LOW_ADDRESS_LIMIT);
}

___alloc_bootmem_node在节点内存分配中是一个关键的函数。

  • 首先通过alloc_bootmem_core在给定的节点中进行内存分配。
  • 如果分配失败,则调用___alloc_bootmem尝试在bdata_list链表中的所有bdata中分配内存。

___alloc_bootmem_nopanic是一个通用的,一个用来尽力而为分配内存的函数,它通过list_for_each_entry在全局链表bdata_list中分配内存。___alloc_bootmem和___alloc_bootmem_nopanic类似,它首先通过___alloc_bootmem_nopanic函数分配内存,但是一旦内存分配失败,系统将通过panic("Out of memory")抛出信息,并停止运行。

___alloc_bootmem_nopanic尽管通过alloc_bootmem_core来实现,它和alloc_bootmem_core可以看作工作在同一层次上。alloc_bootmem_core工作于特定的bdata。而___alloc_bootmem_nopanic则是工作在bdata_list链表中的所有bdata。

10.8. Bootmem alloc宏

尽管bootmem.c中提供了一些了的内存分配函数,但是特定于某个体系架构的代码并没有直接调用它们,而是通过Linux提供的一系列的宏。

include/linux/bootmem.h

#define alloc_bootmem(x)         __alloc_bootmem(x, SMP_CACHE_BYTES, __pa(MAX_DMA_ADDRESS))
#define alloc_bootmem_nopanic(x)         __alloc_bootmem_nopanic(x, SMP_CACHE_BYTES, __pa(MAX_DMA_ADDRESS))
#define alloc_bootmem_low(x)         __alloc_bootmem_low(x, SMP_CACHE_BYTES, 0)
#define alloc_bootmem_pages(x)         __alloc_bootmem(x, PAGE_SIZE, __pa(MAX_DMA_ADDRESS))
#define alloc_bootmem_pages_nopanic(x)         __alloc_bootmem_nopanic(x, PAGE_SIZE, __pa(MAX_DMA_ADDRESS))
#define alloc_bootmem_low_pages(x)         __alloc_bootmem_low(x, PAGE_SIZE, 0)
#define alloc_bootmem_node(pgdat, x)         __alloc_bootmem_node(pgdat, x, SMP_CACHE_BYTES, __pa(MAX_DMA_ADDRESS))
#define alloc_bootmem_pages_node(pgdat, x)         __alloc_bootmem_node(pgdat, x, PAGE_SIZE, __pa(MAX_DMA_ADDRESS))
#define alloc_bootmem_low_pages_node(pgdat, x)         __alloc_bootmem_low_node(pgdat, x, PAGE_SIZE, 0)

一些系统引导时使用的临时结构体通常通过alloc_bootmem_low来分配内存,获取的内存相对于SMP_CACHE_BYTES对齐,通常为32。在创建系统页面机制时,将会用到alloc_bootmem_pages和alloc_bootmem_low_pages,它们分配的内存相对于PAGE_SIZE对齐。针对特定节点的内存分配使用alloc_bootmem_pages_node和alloc_bootmem_low_pages_node进行。

10.9. 标记函数

为了方便对bitmap的操作,bootmeme.c中对__reserve和__free函数又进行了进一步的封装。

图 57. bootmem标记函数


从函数的调用关系看,mark_bootmem_node处于标记函数的核心,所有的预留和释放均是通过它来实现。它的声明如下所示:

static int __init mark_bootmem_node(bootmem_data_t *bdata,
				unsigned long start, unsigned long end,
				int reserve, int flags);

  • bdata指明当前标记操作作用在哪个bootmem_data区。
  • start和end指明了需要标记的区域的起止物理页框。
  • reserve的值可以取1和0,分别对应到__reserve和__free操作。
  • flags当前只支持BOOTMEM_DEFAULT和BOOTMEM_EXCLUSIVE,参考对__reserve和__free函数的分析。

mark_bootmem_node完成了以下工作:

  • 判断start和end的合法性,它们应该位于参数bdata成员node_min_pfn和node_low_pfn之间。
  • 根据start和end计算bitmap的中的起止索引。
  • 根据reserve和索引值调用__reserve或__free函数进行标记操作。
  • 操作成功返回0,否则返回错误号。

mark_bootmem与mark_bootmem_node的关系,类似于___alloc_bootmem_nopanic和alloc_bootmem_core的关系。它通过list_for_each_entry遍历bdata_list,根据start和end查找所在的bdata,然后调用mark_bootmem_node在特定的bdata上完成标记操作。如果start和end指明的区域跨越多个bdata,那么通过mark_bootmem操作是非常方便的。

reserve_bootmem和free_bootmem与reserve_bootmem_node和free_bootmem_node的关系与mark_bootmem和mark_bootmem_node的关系类似。reserve_bootmem和free_bootmem调用mark_bootmem,只是reserve参数分别为1和0。reserve_bootmem_node和free_bootmem_node调用mark_bootmem_node,同样reserve参数分别为1和0。

10.10. Bootmem机制的应用

这里以ARM体系为例介绍Bootmem机制在系统引导时的应用。在系统初始化内存页管理功能之前会首先启用Bootmem,相关代码位于特定架构的kernel/setup.c中的setup_arch调用的paging_init之中。它被定义在特定于系统架构的代码中。

arch/arm/mm/mmu.c
void __init paging_init(struct meminfo *mi, struct machine_desc *mdesc)
{
	void *zero_page;

	build_mem_type_table();
	sanity_check_meminfo(mi);
	prepare_page_table(mi);
	
	bootmem_init(mi);
	devicemaps_init(mdesc);

	top_pmd = pmd_off_k(0xffff0000);

	zero_page = alloc_bootmem_low_pages(PAGE_SIZE);
	memzero(zero_page, PAGE_SIZE);
	empty_zero_page = virt_to_page(zero_page);
	flush_dcache_page(empty_zero_page);
}

注意到bootmem_init函数,它用来初始化bootmem,参数为struct meminfo类型。

arch/arm/mm/init.c

void __init bootmem_init(struct meminfo *mi);

struct meminfo这个结构体定义在特定架构中的include/asm/setup.h中,这是一个对物理内存区间描述的结构体,它将整个地址空间分为NR_BANKS(通常为8)个区间,通常一个区必须是连续的地址并且是同一类型的设备,而用于特殊目的的地址将划分为一个独立的区。首先定义nr_banks,它记录了当前系统的内存块的个数,然后是结构体bank[NR_BANKS]。struct membank结构中的start指明了RAM的起始物理地址,size指明了大小,node则指明了内存块号,对于UMA模式(未配置CONFIG_DISCONTIGMEM)来说,它永远被置为0。

arch/arm/include/asm/setup.h

struct membank {
        unsigned long start;
        unsigned long size;
        int           node;
};

struct meminfo {
        int nr_banks;
        struct membank bank[NR_BANKS];
};

10.10.1. mem参数的传递

内核在启动过程中通过一个全局变量"meminfo"来配置内存。它被定义为static,所以是一个局部的全局变量,仅限制于setup.c内部使用。__initdata限定meminfo被编译到data数据段。

arch/arm/kernel/setup.c

static struct meminfo meminfo __initdata = { 0, };

meminfo被初始化为0,那么其中的数据是合适填充的呢?Bootloader在引导时通过两种方式像内核提供参数的传递:

  • 通过tags数组来传递内存信息,在Bootloader中通过setup_memory_tags函数填充内存相关的信息,这些信息通过宏定义保存到类型为ATAG_MEM的tags中,然后通过寄存器传递给内核这些参数的地址。在内核的setup_arch中的parse_tags中不同类型的tags由不同的函数解析。其中ATAG_MEM类型的tags被parse_tag_mem32解析。
  • 通过内核配置中的CONFIG_CMDLINE指定mem参数,可以通过mem=size@addr(ex. mem=128M@0xb0000000)的方式同时指定大小和物理RAM的地址,如果只指定了大小,物理RAM的地址在内核中使用PHYS_OFFSET定义的值。CONFIG_CMDLINE在setup.c中首先被赋值给default_command_line,接着在setup_arch中的parse_cmdline中被解析。pase_cmdline调用一系列名为early_开始的函数解析对应的参数。mem参数由early_mem解析。

通常setup_arch会首先解析tags中的参数,然后解析CONFIG_CMDLIN中由eraly_开头的解析函数来解析的参数。无论是通过何种方式,最终都将调用统一的arm_add_memory函数将内存信息转换为struct membank类型并存放到meminfo中。

static void __init arm_add_memory(unsigned long start, unsigned long size)
{
	struct membank *bank;

	/*
	 * Ensure that start/size are aligned to a page boundary.
	 * Size is appropriately rounded down, start is rounded up.
	 */
	size -= start & ~PAGE_MASK;

	bank = &meminfo.bank[meminfo.nr_banks++];

	bank->start = PAGE_ALIGN(start);
	bank->size  = size & PAGE_MASK;
	
	/* if not define CONFIG_DISCONTIGMEM then #define PHYS_TO_NID(addr) (0) */
	bank->node  = PHYS_TO_NID(start);
}

注意当通过CONFIG_CMDLINE传递mem参数时,early_mem在一次处理mem参数时会将meminfo.nr_banks置为0,由于两种方式的解析先后顺序,导致CONFIG_CMDLINE传递mem参数时,由Bootloader通过tags方式传递的内存参数将失效。所以如果需要指明多个mem参数信息,那么通过CONFIG_CMDLINE传递是方便的。如果通过CONFIG_CMDLINE指定了mem=256M,PHYS_OFFSET被定义为0x50000000的系统,将得到如下的meminfo信息。

struct meminfo {
  .nr_banks = 1;
  bank[8] = 
  {
    {
      .start = 0x50000000;
      .size = 0x10000000;
      .node = 0;
    };
   ...
  }
};

10.10.2. bootmem_init

bootmem_init是一个举足轻重的函数,它是特定体系架构实现Bootmem机制的入口。参数mi传递系统中所有的RAM信息给该函数,函数中将针对所有的内存node(这里用bank来表示)做处理,并在每一个node中建立位图映射。

bootmem_init完成了以下功能,但不返回任何信息:

  • 首先通过check_initrd查找包含ramdisk的内存node,以备在bitmap中保留该node中的内存。
  • 通过for_each_node遍历当前系统中的MAX_NUMNODES个内存node,然后通过bootmem_init_node函数对每一个node进行初始化和预留的处理。
  • 通过for_each_node调用bootmem_free_node为每一个node通过Bootmem机制申请strcut page数据区,每一个物理页框都由一个对应的struct page结构管理。显然从此功能可以看到Bootmem在系统引导过程中是为了页面管理起到的准备作用。
  • 统计最大物理页帧memend_pfn,并据公式high_memory = __va(memend_pfn << PAGE_SHIFT)计算high_memory以及公式max_pfn = max_low_pfn = memend_pfn - PHYS_PFN_OFFSET计算当前系统中所有node的物理页框总数。

一个物理RAM为256Mb(0x10000000),起始物理地址为0x50000000的系统,将得到以下值,memend_pfn记录了最大的物理页框,PHYS_PFN_OFFSET则由PHYS_OFFSET取页框地址得到。

high_memory = 0xd0000000, max_pfn = 0x10000, memend_pfn = 0x60000 PHYS_PFN_OFFSET = 0x50000

对于Bootmem机制是如何通过bootmem_init实现,并在此基础上实现了内存页表管理,将在页表机制中详细说明。

10.10.3. bootmem_init_node

bootmem_init_node根据参数mi中的bank数据结构,为每一个node内存节点分配位图空间,并初始化相关信息。

arch/arm/mm/init.c

static unsigned long __init bootmem_init_node(int node, struct meminfo *mi);

 为什么要使用bootmem分配器,内存管理不是有buddy系统和slab分配器吗?由于在系统初始化的时候需要执行一些内存管理,内存分配的任务,这个时候buddy系统,slab分配器等并没有被初始化好,此时就引入了一种内存管理器bootmem分配器在系统初始化的时候进行内存管理与分配,当buddy系统和slab分配器初始化好后,在mem_init()中对bootmem分配器进行释放,内存管理与分配由buddy系统,slab分配器等进行接管。

     bootmem分配器使用一个bitmap来标记物理页是否被占用,分配的时候按照第一适应的原则,从bitmap中进行查找,如果这位为1,表示已经被占用,否则表示未被占用。为什么系统运行的时候不使用bootmem分配器呢?bootmem分配器每次在bitmap中进行线性搜索,效率非常低,而且在内存的起始端留下许多小的空闲碎片,在需要非常大的内存块的时候,检查位图这一过程就显得代价很高。bootmem分配器是用于在启动阶段分配内存的,对该分配器的需求集中于简单性方面,而不是性能和通用性。

 前面章节我们介绍了memblock,其作用内核启动初期,常用的内存分配器还未被初始化而不能使用,在此期间memblock是一种用于内存管理区域的方法。然后调用page_init来完成系统分页机制的初始化工作,建立页表,从而内核可以完成虚拟地址到物理地址的映射关系,本章主要是分析bootmem_init的流程。

1. bootm初始化
arm架构下, 在setup_arch中通过paging_init函数初始化内核分页机制之后, 内核通过bootmem_init()开始完成内存结点和内存区域的初始化工作,其函数定义在arch/arm/mm/init.c中,如下所示

void __init bootmem_init(void)
{
    unsigned long min, max_low, max_high;

    memblock_allow_resize();                                                           -------------(1)
    max_low = max_high = 0;

    find_limits(&min, &max_low, &max_high);

    early_memtest((phys_addr_t)min << PAGE_SHIFT,
              (phys_addr_t)max_low << PAGE_SHIFT);


    arm_memory_present();                                                              -------------(2)
    sparse_init();                                                                     -------------(3)
    zone_sizes_init(min, max_low, max_high);                                          -------------(4)
    min_low_pfn = min;
    max_low_pfn = max_low;
    max_pfn = max_high;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
通过memblock拿到limit,DRAM的起始地址的页面号,分别为min = 0x80000, max_low = 0xa0000,内存的结束地址max_high = 0xa0000,early_memtest做内存的Memtest使用,最终会赋值给min_low_pfn(内存块的起始帧号),max_low_pfn(normal结束帧号),max_pfn(内存块的结束帧号)。
arm_memory_present是通过CONFIG_SPARSEMEM来定义的,对于现在ARM32该宏没有定义,暂不分析,以后单独讨论。其主要是linux内核已经实现了内存热插的支持,当一个linux系统不管运行在 物理环境 或者虚拟环境 时只要宿主能提供内存热插拔机制,linux内核就能相应的增加或者减少内存。
启动并运行bootmem分配器,对于ARM32位系统,该功能不支持,几乎没有做什么
zone_sizes_init()来初始化节点和管理区的一些数据项, 其中关键的是初始化了系统中各个内存域的页帧边界,保存在max_zone_pfn数组,从min_low_pfn到max_low_pfn是ZONE_NORMAL,max_low_pfn到max_pfn是ZONE_HIGHMEM。
2.zone_sizes_init初始化
static void __init zone_sizes_init(unsigned long min, unsigned long max_low,
    unsigned long max_high)
{
    unsigned long zone_size[MAX_NR_ZONES], zhole_size[MAX_NR_ZONES];                   -------------(1)
    struct memblock_region *reg;
    memset(zone_size, 0, sizeof(zone_size));
    zone_size[0] = max_low - min;
#ifdef CONFIG_HIGHMEM
    zone_size[ZONE_HIGHMEM] = max_high - max_low;
#endif

    /*
     * Calculate the size of the holes.
     *  holes = node_size - sum(bank_sizes)
     */
    memcpy(zhole_size, zone_size, sizeof(zhole_size));                              -------------(2)
    for_each_memblock(memory, reg) {
        unsigned long start = memblock_region_memory_base_pfn(reg);
        unsigned long end = memblock_region_memory_end_pfn(reg);

        if (start < max_low) {
            unsigned long low_end = min(end, max_low);
            zhole_size[0] -= low_end - start;
        }
#ifdef CONFIG_HIGHMEM
        if (end > max_low) {
            unsigned long high_start = max(start, max_low);
            zhole_size[ZONE_HIGHMEM] -= end - high_start;
        }
#endif
    }

#ifdef CONFIG_ZONE_DMA                                                              -------------(3)
    /*
     * Adjust the sizes according to any special requirements for
     * this machine type.
     */
    if (arm_dma_zone_size)
        arm_adjust_dma_zone(zone_size, zhole_size,
            arm_dma_zone_size >> PAGE_SHIFT);
#endif

    free_area_init_node(0, zone_size, min, zhole_size);                             -------------(4)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
统计zone_size[0]和zone_size[ZONE_HIGHMEM]的大小,zone_size[0] = 0x20000,zone_size[ZONE_HIGHMEM] = 0

最终只是配置了zole_size[0],并且其值为0

如果定义了CONFIG_ZONE_DMA,通过arm_dma_zone_size来配置DMA的内存域,该区域的长度依于处理器类型。在IA-32计算机上,一般的限制为16MB,在我们现在使用的处理器上,CONFIG_ZONE_DMA没有定义,所以只有ZONE_NORMAL和ZONE_HIGHMEM两种。

进入到最关键的地方,free_area_init_node用来针对特定的节点进行初始化。

zone_size[]数据用于保持不同ZONE类型具有的页表,zhole_size数组用于保持不同的ZONE类型具有的空洞的页数,如下图所示

在这里插入图片描述

接下来看看free_area_init_node的实现接口

void __paginginit free_area_init_node(int nid, unsigned long *zones_size,
        unsigned long node_start_pfn, unsigned long *zholes_size)
{
    pg_data_t *pgdat = NODE_DATA(nid);                                            -----------------(1)
    unsigned long start_pfn = 0;
    unsigned long end_pfn = 0;

    /* pg_data_t should be reset to zero when it's allocated */
    WARN_ON(pgdat->nr_zones || pgdat->kswapd_classzone_idx);

    pgdat->node_id = nid;
    pgdat->node_start_pfn = node_start_pfn;
    pgdat->per_cpu_nodestats = NULL;
#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP
    get_pfn_range_for_nid(nid, &start_pfn, &end_pfn);
    pr_info("Initmem setup node %d [mem %#018Lx-%#018Lx]\n", nid,
        (u64)start_pfn << PAGE_SHIFT,
        end_pfn ? ((u64)end_pfn << PAGE_SHIFT) - 1 : 0);
#else
    start_pfn = node_start_pfn;
#endif
    calculate_node_totalpages(pgdat, start_pfn, end_pfn,                        -----------------(2)
                  zones_size, zholes_size);

    alloc_node_mem_map(pgdat);                                                  -----------------(3)
#ifdef CONFIG_FLAT_NODE_MEM_MAP
    printk(KERN_DEBUG "free_area_init_node: node %d, pgdat %08lx, node_mem_map %08lx\n",
        nid, (unsigned long)pgdat,
        (unsigned long)pgdat->node_mem_map);
#endif

    reset_deferred_meminit(pgdat);                                              -----------------(4)
    free_area_init_core(pgdat);                                                 -----------------(5)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
在NUMA有多个节点,而每个节点内,访问内存的时间是相同的,不同的节点,访问内存的时间可以不同。而对于UMA,只有一个节点,取得该node的pg_data_t数据结构变量,每个node都有一个pg_data_t变量描述,进行初始化工作。node_id=0,node_start_fn = 0x80000, start_fn = 0x80000
对节点长度和节点总可用页面数进行初始化。calculate_node_totalpages函数是通过调用zone_spanned_pages_in_node和zone_absent_pages_in_node函数实现的,主要是为pgdat的成员变量(包括空洞在内的总页数(node_spanned_pages))和除空洞外的页数(node_present_pages)设置值
alloc_node_mem_map() 初始化节点的局部映射地址,即pg_data_t->node_mem_map。在NUMA中,全局mem_map指向系统第一个节点的地址,系统中每个节点的起始地址,都对应在全局mem_map的某个位置。在UMA中,全局mem_map就是节点的node_mem_map
由于CONFIG_DEFERRED_STRUCT_PAGE_INIT未定义,该函数为空
调用free_area_init_core()来真正初始化每个struct zone中的成员,填充pgdat的ZONE结构体。
2.1 calculate_node_totalpages初始化
对于该函数主要是用来计算每一个zone的总页数和实际页数(不包含空洞),以及内存节点的总页数和实际页数(不包含空洞),其代码实现如下

static void __meminit calculate_node_totalpages(struct pglist_data *pgdat,
                        unsigned long node_start_pfn,
                        unsigned long node_end_pfn,
                        unsigned long *zones_size,
                        unsigned long *zholes_size)
{
    unsigned long realtotalpages = 0, totalpages = 0;
    enum zone_type i;

    for (i = 0; i < MAX_NR_ZONES; i++) {
        struct zone *zone = pgdat->node_zones + i;
        unsigned long zone_start_pfn, zone_end_pfn;
        unsigned long size, real_size;

        size = zone_spanned_pages_in_node(pgdat->node_id, i,
                          node_start_pfn,
                          node_end_pfn,
                          &zone_start_pfn,
                          &zone_end_pfn,
                          zones_size);
        real_size = size - zone_absent_pages_in_node(pgdat->node_id, i,
                          node_start_pfn, node_end_pfn,
                          zholes_size);
        if (size)
            zone->zone_start_pfn = zone_start_pfn;
        else
            zone->zone_start_pfn = 0;
        zone->spanned_pages = size;
        zone->present_pages = real_size;

        totalpages += size;
        realtotalpages += real_size;
    }

    pgdat->node_spanned_pages = totalpages;
    pgdat->node_present_pages = realtotalpages;
    printk(KERN_DEBUG "On node %d totalpages: %lu\n", pgdat->node_id,
                            realtotalpages);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
该函数主要计算各个ZONE区的page数目,对于ZONE区,其主要有以下3个

ZONE_DMA,该管理区是一些设备无法使用DMA访问所有地址的范围,因此特意划分出来的一块内存,专门用于特殊DMA访问分配使用的区域。比如x86架构此区域为0-16M。本处理器该区域不存在

ZONE_NORMAL:直接映射区,含有的页面数为0x20000

ZONE_HIGHMEM:高端内存管理区,申请的内存,需要内核进行map后才能访问

ZONE_MOVABLE:这个区域是一个特殊的存在,主要是为了支持memory hotplug功能,所以MOVABLE表示可移除,其实它也表示可迁移。本架构CPU不支持该功能。

简单来说,可迁移的页面不一定都在ZONE_MOVABLE中,但是ZONE_MOVABLE中的也页面必须都是可迁移的,我们通过查看/proc/pagetypeinfo来看下实例:

在这里插入图片描述

ZONE_MOVABLE这个管理区,主要是和memory hotplug功能有关,为什么要设计内存热插拔功能,主要是为了如下两点考虑:
1.逻辑内存热插拔,对于虚拟机的支持,对于虚拟机按照需求来分配可用内存
2.物理内存热插拔,对于NUMA服务器的支持,不需要的内存就设置为offline,以降低功耗
3.优化内存碎片问题

2.2 alloc_node_mem_map
在linux内核中,所有的物理内存都用struct page结构来描述,这些对象以数组形式存放,而这个数组的地址就是mem_map。内核以节点node为单位,每个node下的物理内存统一管理,也就是说在表示内存node的描述类型struct pglist_data中,有node_mem_map这个成员,其针对平坦型内存进行描述(CONFIG_FLAT_NODE_MEM_MAP)。如果系统只有一个pglist_data对象,那么此对象下的node_mem_map即为全局对象mem_map。函数alloc_remap()就是针对节点node的node_mem_map处理

static void __ref alloc_node_mem_map(struct pglist_data *pgdat)
{
    unsigned long __maybe_unused start = 0;
    unsigned long __maybe_unused offset = 0;

    /* Skip empty nodes */
    if (!pgdat->node_spanned_pages)                                                ------------------(1)
        return;

#ifdef CONFIG_FLAT_NODE_MEM_MAP
    start = pgdat->node_start_pfn & ~(MAX_ORDER_NR_PAGES - 1);                     ------------------(2)
    offset = pgdat->node_start_pfn - start;
    /* ia64 gets its own node_mem_map, before this, without bootmem */
    if (!pgdat->node_mem_map) {
        unsigned long size, end;
        struct page *map;
        /*
         * The zone's endpoints aren't required to be MAX_ORDER
         * aligned but the node_mem_map endpoints must be in order
         * for the buddy allocator to function correctly.
         */
        end = pgdat_end_pfn(pgdat);                                                ------------------(3)
        end = ALIGN(end, MAX_ORDER_NR_PAGES);
        size =  (end - start) * sizeof(struct page);                               ------------------(4)
        map = alloc_remap(pgdat->node_id, size);
        if (!map)
            map = memblock_virt_alloc_node_nopanic(size,                          ------------------(5)
                                   pgdat->node_id);
        pgdat->node_mem_map = map + offset;
    }
#endif /* CONFIG_FLAT_NODE_MEM_MAP */
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
pgdat->node_spanned_pages此内存节点内无有效的内存,直接略过
起始地址必须对其,这个一般按照MB级别对齐即可,对齐后地址与真正开始地址之间的偏移大小,start = 0x80000,offset = 0
获取节点内结束页帧号pfn,然后对齐,end = 0xa0000,
计算需要的数组大小,需要注意end-start是页帧个数(0xa0000 - 0x80000 = 0x40000),每个页需要一个struct page对象,所以,这里是乘关系,这样得到整个node内所有以page为单位描述需要占据的内存
如果这里分配失败,则通过memblock管理算法分配内存。
2.3 free_area_init_core
static void __paginginit free_area_init_core(struct pglist_data *pgdat)
{
    enum zone_type j;
    int nid = pgdat->node_id;
    int ret;

    pgdat_resize_init(pgdat);                                                      ------------------(1)
#ifdef CONFIG_NUMA_BALANCING
    spin_lock_init(&pgdat->numabalancing_migrate_lock);
    pgdat->numabalancing_migrate_nr_pages = 0;
    pgdat->numabalancing_migrate_next_window = jiffies;
#endif
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
    spin_lock_init(&pgdat->split_queue_lock);
    INIT_LIST_HEAD(&pgdat->split_queue);
    pgdat->split_queue_len = 0;
#endif
    init_waitqueue_head(&pgdat->kswapd_wait);
    init_waitqueue_head(&pgdat->pfmemalloc_wait);
#ifdef CONFIG_COMPACTION
    init_waitqueue_head(&pgdat->kcompactd_wait);
#endif
    pgdat_page_ext_init(pgdat);
    spin_lock_init(&pgdat->lru_lock);
    lruvec_init(node_lruvec(pgdat));

    for (j = 0; j < MAX_NR_ZONES; j++) {                                        ------------------(2)
        struct zone *zone = pgdat->node_zones + j;
        unsigned long size, realsize, freesize, memmap_pages;
        unsigned long zone_start_pfn = zone->zone_start_pfn;

        size = zone->spanned_pages;
        realsize = freesize = zone->present_pages;

        /*
         * Adjust freesize so that it accounts for how much memory
         * is used by this zone for memmap. This affects the watermark
         * and per-cpu initialisations
         */
        memmap_pages = calc_memmap_size(size, realsize);
        if (!is_highmem_idx(j)) {
            if (freesize >= memmap_pages) {
                freesize -= memmap_pages;
                if (memmap_pages)
                    printk(KERN_DEBUG
                           "  %s zone: %lu pages used for memmap\n",
                           zone_names[j], memmap_pages);
            } else
                pr_warn("  %s zone: %lu pages exceeds freesize %lu\n",
                    zone_names[j], memmap_pages, freesize);
        }

        /* Account for reserved pages */
        if (j == 0 && freesize > dma_reserve) {
            freesize -= dma_reserve;
            printk(KERN_DEBUG "  %s zone: %lu pages reserved\n",
                    zone_names[0], dma_reserve);
        }

        if (!is_highmem_idx(j))
            nr_kernel_pages += freesize;
        /* Charge for highmem memmap if there are enough kernel pages */
        else if (nr_kernel_pages > memmap_pages * 2)
            nr_kernel_pages -= memmap_pages;
        nr_all_pages += freesize;

        /*
         * Set an approximate value for lowmem here, it will be adjusted
         * when the bootmem allocator frees pages into the buddy system.
         * And all highmem pages will be managed by the buddy system.
         */
        zone->managed_pages = is_highmem_idx(j) ? realsize : freesize;
#ifdef CONFIG_NUMA
        zone->node = nid;
#endif
        zone->name = zone_names[j];
        zone->zone_pgdat = pgdat;
        spin_lock_init(&zone->lock);
        zone_seqlock_init(zone);
        zone_pcp_init(zone);                                                    ------------------(3)

        if (!size)
            continue;

        set_pageblock_order();
        setup_usemap(pgdat, zone, zone_start_pfn, size);
        ret = init_currently_empty_zone(zone, zone_start_pfn, size);            ------------------(4)
        BUG_ON(ret);
        memmap_init(size, nid, j, zone_start_pfn);                              ------------------(5)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
主要是初始化struct pglist_data,首先初始化pgdat->node_size_lock自旋锁初始化,初始化pgdat->kswapd_wait等待队列,初始化页换出守护进程创建空闲块的大小

遍历各个zone区域,进行如下初始化:

根据spanned_pages和present_pages,调用calc_memmap_size计算管理该zone所需的struct page结构所占的页面数memmap_pages

zone中的freesize表示可用的区域,需要减去memmap_pages和dma_reserve的区域,如下开发板的Log打印所示:memmap使用1024页,DMA 保留0页

在这里插入图片描述

计算nr_kernel_pages和nr_all_pages的数量

初始化zone的其他变量和各种锁

初始化zone结构体的per_cpu_pageset结构体变量pageset,per_cpu_pageset按CPU进行管理。它不直接返回伙伴系统,为快速分配而按不同CPU持有页。

初始化zone结构体的free_area结构体。

memmap_init函数在与页帧具有1:1映射关系的页数组中,向相应的页帧的page结构体的flags成员设置PG_reserved位。

最后,当我们回顾bootmem_init函数时,发现它基本上完成了linux物理内存框架的初始化,包括Node, Zone, Page Frame,以及对应的数据结构等。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值