Linux内存初始化

start_kernel -> setup_arch 在这个函数中我们主要看这几个函数.
machine_specific_memory_setup
max_low_pfn = setup_memory();
paging_init
zone_sizes_init
然后我们还要看
build_all_zonelists();
mem_init();

//处理内存图,最后保存在e820中
char * __init machine_specific_memory_setup(void)
{
    ......
    who = "BIOS-e820";

    // #define E820_MAP_NR (*(char*) (PARAM+E820NR))
    // #define E820_MAP    ((struct e820entry *) (PARAM+E820MAP))
    // 删除内存构成图中任何重叠的部分,因为BIOS所报告的内存构成图可能有重叠
    sanitize_e820_map(E820_MAP, &E820_MAP_NR);

    //调用copy_e820_map()进行实际的拷贝。
    //如果操作失败,创建一个伪内存构成图,这个伪构成图有两部分:0到640K及1M到最大物理内存。
    if (copy_e820_map(E820_MAP, E820_MAP_NR) < 0) {
        ......
        e820.nr_map = 0;
        add_memory_region(0, LOWMEMSIZE(), E820_RAM);
        add_memory_region(HIGH_MEMORY, mem_size << 10, E820_RAM);
    }
    ......
}
这里要特别对宏E820_MAP进行说明。E820_MAP是个struct e820entry数据结构的指针.
这个数据结构定义在include/i386/e820.h中:
struct e820map {
    int nr_map;
    struct e820entry {
        unsigned long long addr;        /* start of memory segment */
        unsigned long long size;        /* size of memory segment */
        unsigned long type;             /* type of memory segment */
    } map[E820MAX];
};
其中,E820MAX被定义为128。从这个数据结构的定义可以看出,每个e820entry都是对一个物理区间的描述,并且一个物理区间必须是同一类型。如果有一片地址连续的物理内存空间,其一部分是RAM,而另一部分是ROM,那就要分成两个区间。即使同属RAM,如果其中一部分要保留用于特殊目的,那也属于不同的一个分区。在e820.h文件中定义了4种不同的类型:

#define E820_RAM                1
#define E820_RESERVED     2
#define E820_ACPI               3  /* usable as RAM once ACPI tables have been read */
#define E820_NVS                4

#define HIGH_MEMORY     (1024*1024)

其中E820_NVS表示“Non-Volatile Storage”,即“不挥发”存储器,包括ROM、EPROM、Flash存储器等。

在PC中,对于最初1MB存储空间的使用是特殊的。开头640KB(0x0~0x9FFFF为RAM,从0xA0000开始的空间则用于CGA、EGA、VGA等图形卡。现在已经很少使用这些图形卡,但是不管是什么图形卡,开机时总是工作于EGA或VGA模式。从0xF0000开始到0xFFFFF,即最高的4KB,就是在EPROM或Flash存储器中的BIOS。所以,只要有BIOS存在,就至少有两个区间,如果nr_map小于2,那就一定出错了。由于BIOS的存在,本来连续的RAM空间就不连续了。当然,现在已经不存在这样的存储结构了。1MB的边界早已被突破,但因为历史的原因,把1MB以上的空间定义为“HIGH­­_MEMORY”,这个称呼一直沿用到现在,于是代码中的常数HIGH­­_MEMORY就定义为“1024´1024”但是,为了保持兼容,就得留出最初1MB的空间。

这个阶段初始化后,物理内存中内核映像的分布如图所示:

 

 
  *


      图  内核映象在物理内存中的分布

符号_text对应物理地址0x00100000,表示内核代码的第一个字节的地址。内核代码的结束位置用另一个类似的符号_etext表示。内核数据被分为两组:初始化过的数据和未初始化过的数据。初始化过的数据在_etext后开始,在_edata处结束,紧接着是未初始化过的数据,其结束符号为_end,这也是整个内核映像的结束符号。

图中出现的符号是由编译程序在编译内核时产生的。你可以在System.map文件中找到这些符号的线性地址(或叫虚拟地址),System.map是编译内核以后所创建的。

调用copy_e820_map()进行实际的拷贝。

如果操作失败,创建一个伪内存构成图,这个伪构成图有两部分:0到640K及1M到最大物理内存。

 

//进行实际拷贝
int __init copy_e820_map(struct e820entry * biosmap, int nr_map)
{
    //如果物理内存区间小于2,那肯定出错。因为BIOS至少和RAM属于不同的物理区间。
    if (nr_map < 2)
        return -1;

    //从BIOS构成图中读出一项
    do {
        unsigned long long start = biosmap->addr;
        unsigned long long size = biosmap->size;
        unsigned long long end = start + size;
        unsigned long type = biosmap->type;

        if (start > end)
            return -1;

        //一些BIOS把640K~1MB之间的区间作为RAM来用,这是不符合常规的。因为从0xA0000开始的空间用于图形卡,因此,在内存构成图中要进行修正。      
        ////如果一个区的起点在0xA0000 (640K)以下,而终点在1MB之上,就要将这个区间拆开成两个区间,中间跳过从0xA0000到1MB边界之间的那一部分。
        if (type == E820_RAM) {
            if (start < 0x100000ULL && end > 0xA0000ULL) {
                if (start < 0xA0000ULL)
                    add_memory_region(start, 0xA0000ULL-start, type);

                if (end <= 0x100000ULL)
                    continue;

                start = 0x100000ULL;
                size = end - start;
            }
        }
        //这个函数的功能就是在e820中增加一项
        add_memory_region(start, size, type);
    } while (biosmap++, --nr_map);
    return 0;
}
下面来看
#define PFN_UP(x)       (((x) + PAGE_SIZE-1) >> PAGE_SHIFT)
#define PFN_DOWN(x)     ((x) >> PAGE_SHIFT)
#define PFN_PHYS(x)     ((x) << PAGE_SHIFT)
PFN_UP() 和PFN_DOWN()都是将地址x转换为页面号(PFN即Page Frame Number的缩写),二者之间的区别为:PFN_UP()返回大于x的第一个页面号,而PFN_DOWN()返回小于x的第一个页面号。宏PFN_PHYS()返回页面号x的物理地址。
static unsigned long __init setup_memory(void)
{
    //arch/i386/kernel/head.S 中有
    //movl %edi,(init_pg_tables_end - __PAGE_OFFSET) 将实际映射到的最后页表的内存地址放到变量init_pg_tables_end的物理地址
    //init_pg_tables_end = 内核保护模式代码启始地址(0x100000) + 内核保护模式代码尺寸 + pg0的1024个4字节页面描述符号 + 保证第一次分页设置的页表尺寸
    //(一般还需要若干1024个4字节的页面描述符号,由内核尺寸决定) + 描述1G内存的位图尺寸128K字节 + 描述1G内存的页表空间(1024*4096字节) +
    //间隔空间(4*4096字节)
    //初始化最小可用pfn
    min_low_pfn = PFN_UP(init_pg_tables_end);
    //在e820 结构中查找最大pfn. 初始化max_pfn
    find_max_pfn();
    //我们要好好看看这个
    max_low_pfn = find_max_low_pfn();
#ifdef CONFIG_HIGHMEM
    highstart_pfn = highend_pfn = max_pfn;
    if (max_pfn > max_low_pfn) {
        highstart_pfn = max_low_pfn; //初始化高端内存开始pfn
    }
    printk(KERN_NOTICE "%ldMB HIGHMEM available.\n", pages_to_mb(highend_pfn - highstart_pfn));
#endif
    printk(KERN_NOTICE "%ldMB LOWMEM available.\n", pages_to_mb(max_low_pfn));
    //初始化max_low_pfn的低端内存映射图bitmap,并对系统启动时使用一些特殊占用内存进行保留设置。
    setup_bootmem_allocator();

    return max_low_pfn;
}

首先介绍一些宏定义:
#define VMALLOC_RESERVE (unsigned long)(128 << 20)
#define MAXMEM (unsigned long)(-PAGE_OFFSET-VMALLOC_RESERVE)
#define MAXMEM_PFN PFN_DOWN(MAXMEM)
#define MAX_NONPAE_PFN (1 << 20)

VMALLOC_RESERVE :为vmalloc()函数访问内核空间所保留的内存区,大小为128MB。
MAXMEM :内核能够直接映射的最大RAM容量,为1GB-128MB=896MB(-PAGE_OFFSET就等于1GB)
MAXMEM_PFN :返回由内核能直接映射的最大物理页面数。
MAX_NONPAE_PFN :给出在4GB之上第一个页面的页面号。当页面扩充(PAE)功能启用时,才能访问4GB以上的内存。
unsigned long __init find_max_low_pfn(void)
{
    unsigned long max_low_pfn;
    max_low_pfn = max_pfn;

    if (max_low_pfn > MAXMEM_PFN) { //内存大于896M
        if (highmem_pages == -1)
            highmem_pages = max_pfn - MAXMEM_PFN; //初始化高端内存页数量为 max_pfn - 896

        if (highmem_pages + MAXMEM_PFN < max_pfn) //在调整max_pfn
            max_pfn = MAXMEM_PFN + highmem_pages;

        if (highmem_pages + MAXMEM_PFN > max_pfn) { //说明有错误发生
            printk("only %luMB highmem pages available, ignoring highmem size of %uMB. n", pages_to_mb(max_pfn - MAXMEM_PFN), ages_to_mb(highmem_pages));
            highmem_pages = 0; //忽略
        }
        max_low_pfn = MAXMEM_PFN; //初始化为896M内存的pfn
#ifndef CONFIG_HIGHMEM //没有定义使用高端内存
        printk(KERN_WARNING "Warning only %ldMB will be used.\n", MAXMEM>>20);
        if (max_pfn > MAX_NONPAE_PFN) //内存超过4G
            printk(KERN_WARNING "Use a PAE enabled kernel.\n");
        else
            printk(KERN_WARNING "Use a HIGHMEM enabled kernel.\n");
        max_pfn = MAXMEM_PFN; //最多使用到896M
#else //定义
#ifndef CONFIG_X86_PAE //没有开启PAE
        if (max_pfn > MAX_NONPAE_PFN) {
            max_pfn = MAX_NONPAE_PFN; //只能使用4G内存
            printk(KERN_WARNING "Warning only 4GB will be used.\n");
            printk(KERN_WARNING "Use a PAE enabled kernel.\n");
        }
#endif /* !CONFIG_X86_PAE */
#endif /* !CONFIG_HIGHMEM */
        //下面说明内存小于896M
    } else {
        if (highmem_pages == -1)
            highmem_pages = 0; //没有高端内存
#ifdef CONFIG_HIGHMEM
        if (highmem_pages >= max_pfn) {
            printk(KERN_ERR "highmem size specified (%uMB) is bigger than pages available (%luMB)!.\n", pages_to_mb(highmem_pages), pages_to_mb(max_pfn));
            highmem_pages = 0;

        }
        if (highmem_pages) { //有高端内存
            if (max_low_pfn-highmem_pages < 64*1024*1024/PAGE_SIZE) { //高端内存的使用导致了常规内存小于64M
                printk(KERN_ERR "highmem size %uMB results in smaller than 64MB lowmem, ignoring it.\n", pages_to_mb(highmem_pages));
                highmem_pages = 0; //忽略
            }
            max_low_pfn -= highmem_pages; //缩小常规内存大小
        }
#else
        if (highmem_pages)
            printk(KERN_ERR "ignoring highmem size on non-highmem kernel!\n");
#endif
    }
    return max_low_pfn;
}


typedef struct bootmem_data {
    unsigned long node_boot_start; //表示存放bootmem位图的第一个页面(即内核映象结束处的第一个页面)
    unsigned long node_low_pfn;   //表示物理内存的顶点,最高不超过896MB
    void *node_bootmem_map;    //指向bootmem位图
    unsigned long last_offset;     //用来存放在前一次分配中所分配的最后一个字节相对于last_pos的位移量
    unsigned long last_pos;       //用来存放前一次分配的最后一个页面的页面号,
    //这个域用在__alloc_bootmem_core()函数中,通过合并相邻的内存来减少内部碎片
} bootmem_data_t;
为了对页面管理机制作出初步准备,Linux使用了一种叫bootmem分配器(bootmem allocator)的机制,这种机制仅仅用在系统引导时,
它为整个物理内存建立起一个页面位图。这个位图建立在从min_low_pfn开始的地方,也就是说,内核映象终点_end上方的地方。
这个位图用来管理低区(例如小于896MB),因为在0到896MB的范围内,有些页面可能保留,有些页面可能有空洞,因此,建立这个
位图的目的就是要搞清楚哪一些物理页面是可以动态分配的。
void __init setup_bootmem_allocator(void)
{
    ......
    /*
     通过调用init_bootmem()函数,为物理内存页面管理机制的建立做初步准备,为整个物理内存建立起一个页面位图。这个位图建立在从min_low_pfn开始的地方,
     也就是说,把内核映像终点_end上方的若干页面用作物理页面位图。在前面的代码中已经搞清楚了物理内存顶点所在的页面号为max_low_pfn,所以物理内存的页面号一定在0~max_low_pfn之间。可是,在这个范围内可能有空洞(hole),另一方面,并不是所有的物理内存页面都可以动态分配。建立这个位图的目的就是要搞清楚哪一些物理内存页面可以动态分配的。
    */
    bootmap_size = init_bootmem(min_low_pfn, max_low_pfn);
    //根据e820表将未使用的内存在bdata->node_bootmem_map中对应的位置清0,很简单
    register_bootmem_low_pages(max_low_pfn);

    //启始地址是内核启始指针,尺寸为从内核开始到内存映射表结束,保留从内核开始到内存映射表结束的内存
    //通过使用test_and_set_bit,将保留空间设置为1
    reserve_bootmem(__pa_symbol(_text), (PFN_PHYS(min_low_pfn) + bootmap_size + PAGE_SIZE-1) - __pa_symbol(_text));
    reserve_bootmem(0, PAGE_SIZE); //保留启始4K内存
    reserve_ebda_region();保留EBDA开始4K内存
    ......
}
在这里NODE_DATA(0)是全局变量contig_page_data的地址,start是min_low_pfn,pages是max_low_pfn
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), start, 0, pages));
}
static unsigned long __init init_bootmem_core (pg_data_t *pgdat, unsigned long mapstart, unsigned long start, unsigned long end)
{
    bootmem_data_t *bdata = pgdat->bdata;
    unsigned long mapsize = ((end - start)+7)/8; //低端页面表所占用尺寸

    mapsize = ALIGN(mapsize, sizeof(long));
    bdata->node_bootmem_map = phys_to_virt(mapstart << PAGE_SHIFT); //获得min_low_pfn指向的虚拟地址
    bdata->node_boot_start = (start << PAGE_SHIFT); // 0
    bdata->node_low_pfn = end; //低端内存页面数目
    link_bootmem(bdata); //将这结构插入bdata_list连表

    //设置min_low_pfn起内存供低端页面表占用的空间初始化为0xFF
    memset(bdata->node_bootmem_map, 0xff, mapsize);

    return mapsize;
}
    下面我们看一个非常重要的函数
void __init paging_init(void)
{
    ......
    //建立内核页表
    pagetable_init(); //这个最重要,我们下面看

    load_cr3(swapper_pg_dir); //__asm__( "movl %%ecx,%%cr3\n" ::"c"(__pa(swapper_pg_dir))); 大约是这样
    //这一句是个宏,它使得转换旁路缓冲区(TLB)无效。TLB总是要维持几个最新的虚地址到物理地址的转换。每当页目录改变时,TLB就需要被刷新。
    __flush_tlb_all();
    //临时内核映射初始化 kmap_atomic()使用
    kmap_init();
}
static void __init pagetable_init (void)
{
    unsigned long vaddr;
    //让pgd_base (页目录基地址) 指向 swapper_pg_dir
    pgd_t *pgd_base = swapper_pg_dir;
#ifdef CONFIG_X86_PAE
    int i;
    //如果PAE 被激活, PTRS_PER_PGD就为4,且变量 swapper_pg_dir 用作页目录指针表
    for (i = 0; i < PTRS_PER_PGD; i++)
    set_pgd(pgd_base + i, __pgd(__pa(empty_zero_page) | _PAGE_PRESENT));
#endif
    ......
    kernel_physical_mapping_init(pgd_base); //映射核心页表
    ......
    //在内存的最高端(4GB - 128MB),有些虚地址直接用在内核资源的某些部分中,这些地址的映射定义在/include/asm-i386/fixmap.h中,
    //枚举类型__end_of_fixed_addresses用作索引,宏__fix_to_virt()返回给定索引的虚地址。
    //#define __fix_to_virt(x)        (FIXADDR_TOP - ((x) << PAGE_SHIFT))
    // FIXADDR_TOP  0xfffff000  (4G)
    vaddr = __fix_to_virt(__end_of_fixed_addresses - 1) & PMD_MASK;
    //进行保留映射页的初始化
    page_table_range_init(vaddr, 0, pgd_base);
    //永久映射页的初始化
    permanent_kmaps_init(pgd_base);
    ......
}
//i386体系结构正常使用两级页表,如果开启物理地址扩展PAE那么使用三级页表
static void __init kernel_physical_mapping_init(pgd_t *pgd_base)
{
    unsigned long pfn;
    pgd_t *pgd;
    pmd_t *pmd;
    pte_t *pte;
    int pgd_idx, pmd_idx, pte_ofs;

    //在给定地址的页目录中检索相应的下标。因此返回0x300 (或 十进制768 ),即内核地址空间开始处的下标。
    //因此,pgd现在指向页目录表的第768项。
    /*
#define PGDIR_SHIFT     22
#define PTRS_PER_PGD    1024
开启PAE
#define PGDIR_SHIFT     30
#define PTRS_PER_PGD    4
*/
    pgd_idx = pgd_index(PAGE_OFFSET);
    pgd = pgd_base + pgd_idx; //指向768项
    pfn = 0;

    for (; pgd_idx < PTRS_PER_PGD; pgd++, pgd_idx++) {
        pmd = one_md_table_init(pgd);
        if (pfn >= max_low_pfn)
            continue;

        //如果是两级分页 PTRS_PER_PMD = 1,三级分页 = 512 启用了PAE
        for (pmd_idx = 0; pmd_idx < PTRS_PER_PMD && pfn < max_low_pfn; pmd++, pmd_idx++) {
            //计算第pfn个页框的虚拟地址
            unsigned int address = pfn * PAGE_SIZE + PAGE_OFFSET; //0xC0000000
            if (cpu_has_pse) { //开启了PSE 4M页
                //计算第pfn个4M页框结束边界的虚拟地址
                unsigned int address2 = (pfn + PTRS_PER_PTE - 1) * PAGE_SIZE + PAGE_OFFSET + PAGE_SIZE-1;
                if (is_kernel_text(address) || is_kernel_text(address2))
                    set_pmd(pmd, pfn_pmd(pfn, PAGE_KERNEL_LARGE_EXEC));
                else
                    set_pmd(pmd, pfn_pmd(pfn, PAGE_KERNEL_LARGE));

                pfn += PTRS_PER_PTE; // pfn += 1024
            } else {
                //分配一个页表
                pte = one_page_table_init(pmd);
                //两级 1024 三级 512   PTRS_PER_PTE
                for (pte_ofs = 0; pte_ofs < PTRS_PER_PTE && pfn < max_low_pfn; pte++, pfn++, pte_ofs++) {
                    if (is_kernel_text(address)) //addr >= PAGE_OFFSET && addr <= (unsigned long)__init_end 核心正使用
                        //PAGE_KERNEL_EXEC .. include/asm-i386/pgtable.h
                        //#define __pte(x) ((pte_t) { (x) } )
                        //#define pfn_pte(pfn, prot)      __pte(((pfn) << PAGE_SHIFT) | pgprot_val(prot))
                        set_pte(pte, pfn_pte(pfn, PAGE_KERNEL_EXEC));
                    else
                        set_pte(pte, pfn_pte(pfn, PAGE_KERNEL));
                }
            }
        }
    }
}
static pmd_t * __init one_md_table_init(pgd_t *pgd)
{
    pud_t *pud;
    pmd_t *pmd_table;

    //如果使用了CONFIG_X86_PAE选项,则分配一页(4K)的内存给页中间目录,并在全局目录中设置它的地址。
    //#define pud_offset(pgd, start)            (pgd)
    //#define pmd_offset(pud, address)         (pud) //正常两级分页,类似这样
#ifdef CONFIG_X86_PAE
    pmd_table = (pmd_t *) alloc_bootmem_low_pages(PAGE_SIZE);
    set_pgd(pgd, __pgd(__pa(pmd_table) | _PAGE_PRESENT));
    pud = pud_offset(pgd, 0);
    if (pmd_table != pmd_offset(pud, 0)) //从上面的宏可以看出肯定是一样的
        BUG();
#else //否则,没有页上级目录,就把页上级目录直接映射到全局页目录
    pud = pud_offset(pgd, 0); //pud = pgd
    pmd_table = pmd_offset(pud, 0); //   三级页表时 pmd_table 为什么是 pgd,pmd_offset 与两级页表时不同
#endif

    return pmd_table;
}
static pte_t * __init one_page_table_init(pmd_t *pmd)
{
    if (pmd_none(*pmd)) { //如果pmd项中为空,分配一页用作页表
        pte_t *page_table = (pte_t *) alloc_bootmem_low_pages(PAGE_SIZE);
        set_pmd(pmd, __pmd(__pa(page_table) | _PAGE_TABLE));
        if (page_table != pte_offset_kernel(pmd, 0))
            BUG();

        return page_table;
    }
    //返回已经存在的页表
    return pte_offset_kernel(pmd, 0);
}
static void __init page_table_range_init (unsigned long start, unsigned long end, pgd_t *pgd_base)
{
    pgd_t *pgd;
    pud_t *pud;
    pmd_t *pmd;
    int pgd_idx, pmd_idx;
    unsigned long vaddr;

    vaddr = start;
    pgd_idx = pgd_index(vaddr); //pgd 索引
    pmd_idx = pmd_index(vaddr);
    pgd = pgd_base + pgd_idx; //指向所在pgd项

    for ( ; (pgd_idx < PTRS_PER_PGD) && (vaddr != end); pgd++, pgd_idx++) {
        if (pgd_none(*pgd))
            one_md_table_init(pgd); //已经看过

        pud = pud_offset(pgd, vaddr); //取出相应的索引
        pmd = pmd_offset(pud, vaddr);
        for (; (pmd_idx < PTRS_PER_PMD) && (vaddr != end); pmd++, pmd_idx++) {
            if (pmd_none(*pmd))
                one_page_table_init(pmd); //已经看过

            //两级页表 PMD_SIZE (1 << 22)  三级页表 (1 << 30)
            vaddr += PMD_SIZE;
        }
        pmd_idx = 0;
    }
}
static void __init permanent_kmaps_init(pgd_t *pgd_base)
{
    pgd_t *pgd;
    pud_t *pud;
    pmd_t *pmd;
    pte_t *pte;
    unsigned long vaddr;

    //#define FIXADDR_BOOT_START      (FIXADDR_TOP - __FIXADDR_BOOT_SIZE)
    //#define __FIXADDR_BOOT_SIZE     (__end_of_fixed_addresses << PAGE_SHIFT)
    //#define PKMAP_BASE ( (FIXADDR_BOOT_START - PAGE_SIZE*(LAST_PKMAP + 1)) & PMD_MASK )
    vaddr = PKMAP_BASE;

    //LAST_PKMAP 1024     PAE enable 512
    page_table_range_init(vaddr, vaddr + PAGE_SIZE*LAST_PKMAP, pgd_base);

    pgd = swapper_pg_dir + pgd_index(vaddr);
    pud = pud_offset(pgd, vaddr);
    pmd = pmd_offset(pud, vaddr);
    pte = pte_offset_kernel(pmd, vaddr);
    pkmap_page_table = pte; //永久内核映射的开始  kmap 函数使用
}
//为kmap_atomic函数初始化一些参数
static void __init kmap_init(void)
{
    unsigned long kmap_vstart;
    //临时内核映射线性地址开始
    kmap_vstart = __fix_to_virt(FIX_KMAP_BEGIN);

    //#define kmap_get_fixmap_pte(vaddr)  pte_offset_kernel(pmd_offset(pud_offset(pgd_offset_k(vaddr), vaddr), (vaddr)), (vaddr))
    //#define pgd_offset(mm, address) ((mm)->pgd+pgd_index(address))
    //#define pgd_offset_k(address) pgd_offset(&init_mm, address) 
    //init_mm->pgd = swapper_pg_dir  :)
    kmap_pte = kmap_get_fixmap_pte(kmap_vstart); //指向开始的页表项
    kmap_prot = PAGE_KERNEL;
}
下面我们看内存页是怎样组织的.
物理内存被划分为三个区来管理,它们是ZONE_DMA、ZONE_NORMAL 和ZONE_HIGHMEM。每个区都用struct zone结构来表示,
定义于include/linux/mmzone.h:
struct zone {
    unsigned long           free_pages; //在这个区中现有空闲页的个数
    unsigned long           pages_min, pages_low, pages_high; //对这个区最少、较少及最多页面个数的描述
    ......
    struct free_area        free_area[MAX_ORDER]; //在伙伴分配系统中的位图数组和页面链表
    ......
    struct pglist_data       *zone_pgdat;            //本管理区所在的存储节点
    ......
    unsigned long           zone_start_pfn;      //该管理区的起始pfn
    unsigned long           spanned_pages;      //总计页数量,包含洞
    unsigned long           present_pages;      //总计页数量,除了洞
    char                    *name;              //管理区名字
};
#define ZONE_DMA                0
#define ZONE_DMA32              1
#define ZONE_NORMAL             2
#define ZONE_HIGHMEM            3
#define MAX_NR_ZONES            4

struct free_area {
    struct list_head        free_list;
    unsigned long           nr_free;
};
zone­­­结构中的free_area[MAX_ORDER]是一组“空闲区间”链表。为什么要定义一组而不是一个空闲队列呢?
这是因为常常需要成块地在物理空间分配连续的多个页面,所以要按块的大小分别加以管理。
因此,在管理区数据结构中既要有一个队列来保持一些离散(连续长度为1)的物理页面,还要有一个队列来保持一些
连续长度为2的页面块以及连续长度为4、816、…、直至2 MAX_ORDER(即4M字节)的队列。

void __init zone_sizes_init(void)
{
    unsigned long zones_size[MAX_NR_ZONES] = {0, 0, 0};
    ......
    //define MAX_DMA_ADDRESS (PAGE_OFFSET+0x1000000)       (PAGE_OFFSET + 16M)
    max_dma = virt_to_phys((char *)MAX_DMA_ADDRESS) >> PAGE_SHIFT;
    low = max_low_pfn;
    if (low < max_dma)
        zones_size[ZONE_DMA] = low;
    else { //建立域
        zones_size[ZONE_DMA] = max_dma;
        zones_size[ZONE_NORMAL] = low - max_dma;
#ifdef CONFIG_HIGHMEM
        zones_size[ZONE_HIGHMEM] = highend_pfn - low;
#endif
    }
    //这个函数用来初始化内存管理区并创建内存映射表,定义于mm/page_alloc.c中
    //NODE_DATA(0) 就是struct pglist_data contig_page_data 变量的宏定义,一般在NUMA中有多个节点,其他就只有这一个变量.
    free_area_init(zones_size); //这个函数是 free_area_init_node(0, NODE_DATA(0), zones_size, __pa(PAGE_OFFSET) >> PAGE_SHIFT, NULL);的封装
}
//我们一个一个看
void __meminit free_area_init_node(int nid, struct pglist_data *pgdat, unsigned long *zones_size, unsigned long node_start_pfn, unsigned long *zholes_size)
{
    pgdat->node_id = nid; // 0
    pgdat->node_start_pfn = node_start_pfn; // 也是0
    calculate_zone_totalpages(pgdat, zones_size, zholes_size);

    alloc_node_mem_map(pgdat);

    free_area_init_core(pgdat, zones_size, zholes_size);
}
static void __init calculate_zone_totalpages(struct pglist_data *pgdat, unsigned long *zones_size, unsigned long *zholes_size)
{
    unsigned long realtotalpages, totalpages = 0;
    int i;
    //总计页数量
    for (i = 0; i < MAX_NR_ZONES; i++)
        totalpages += zones_size[i];
    pgdat->node_spanned_pages = totalpages; //记录全部页数量包含洞

    realtotalpages = totalpages;
    if (zholes_size)
        for (i = 0; i < MAX_NR_ZONES; i++)
            realtotalpages -= zholes_size[i];
    pgdat->node_present_pages = realtotalpages;  //现在这是和上面的totalpages一样,因为这时zholes_size = NULL.
}
static void __init alloc_node_mem_map(struct pglist_data *pgdat)
{
    ......
    if (!pgdat->node_mem_map) {
        unsigned long size, start, end;
        struct page *map;
        //计算页数量,之所以要这样是为了适应NUMA体系结构,如果不是NUMA那么直接使用pgdat->node_spanned_pages就可以了
        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);

        map = alloc_remap(pgdat->node_id, size);  // ?
        if (!map)
            map = alloc_bootmem_node(pgdat, size); //分配size 数量的struct page 内存

        pgdat->node_mem_map = map + (pgdat->node_start_pfn - start); //指向位图的相应位置
    }
#ifdef CONFIG_FLATMEM
    if (pgdat == NODE_DATA(0))
        mem_map = NODE_DATA(0)->node_mem_map; //mem_map是一个全局变量
#endif
    ......
}
static void __meminit free_area_init_core(struct pglist_data *pgdat, unsigned long *zones_size, unsigned long *zholes_size)
{
    unsigned long j;
    int nid = pgdat->node_id;
    unsigned long zone_start_pfn = pgdat->node_start_pfn;
    int ret;

    pgdat_resize_init(pgdat); // 调用 spin_lock_init(&pgdat->node_size_lock);
    pgdat->nr_zones = 0;
    init_waitqueue_head(&pgdat->kswapd_wait);
    pgdat->kswapd_max_order = 0;

    for (j = 0; j < MAX_NR_ZONES; j++) {
        struct zone *zone = pgdat->node_zones + j;
        unsigned long size, realsize;
        realsize = size = zones_size[j];
        if (zholes_size)
            realsize -= zholes_size[j]; //实际大小为去掉洞的大小

        if (j < ZONE_HIGHMEM)
            nr_kernel_pages += realsize; //全局变量,记录kernel可以使用的页数量

        nr_all_pages += realsize; //全局变量,记录总共可以使用的页数量,去掉了洞

        //下面初始化zone
        zone->spanned_pages = size;
        zone->present_pages = realsize;
        ...... //略过关于 NUMA
        //static char *zone_names[MAX_NR_ZONES] = { "DMA", "DMA32", "Normal", "HighMem" };
        zone->name = zone_names[j];
        spin_lock_init(&zone->lock);
        spin_lock_init(&zone->lru_lock);
        zone_seqlock_init(zone);
        zone->zone_pgdat = pgdat;
        zone->free_pages = 0;

        zone->prev_priority = DEF_PRIORITY;
        //初始化zone结构体中的pageset成员,这个成员为zone中的每cpu变量,其中又分cold和hot下标1和0的页集合
        zone_pcp_init(zone);
        INIT_LIST_HEAD(&zone->active_list);
        INIT_LIST_HEAD(&zone->inactive_list);
        zone->nr_scan_active = 0;
        zone->nr_scan_inactive = 0;
        zone->nr_active = 0;
        zone->nr_inactive = 0;
        zap_zone_vm_stats(zone); // 实现为memset(zone->vm_stat, 0, sizeof(zone->vm_stat));
        atomic_set(&zone->reclaim_in_progress, 0);

        if (!size)
            continue;

        //初始化 struct zone *zone_table[1 << ZONETABLE_SHIFT] __read_mostly;
        zonetable_add(zone, nid, j, zone_start_pfn, size);
        ret = init_currently_empty_zone(zone, zone_start_pfn, size, MEMMAP_EARLY);

        zone_start_pfn += size; //下一个zone的zone_start_pfn

    }
}
__meminit int init_currently_empty_zone(struct zone *zone, unsigned long zone_start_pfn, unsigned long size, enum memmap_context context)
{
    struct pglist_data *pgdat = zone->zone_pgdat;
    int ret;
    /*初始化zone中的
      wait_queue_head_t       * wait_table;
      unsigned long           wait_table_hash_nr_entries;
      unsigned long           wait_table_bits;
      */
    ret = zone_wait_table_init(zone, size);
    if (ret)
        return ret;
    pgdat->nr_zones = zone_idx(zone) + 1;
    zone->zone_start_pfn = zone_start_pfn;
    //#define memmap_init(size, nid, zone, start_pfn) memmap_init_zone((size), (nid), (zone), (start_pfn), MEMMAP_EARLY)
    //#define zone_idx(zone)          ((zone) - (zone)->zone_pgdat->node_zones)  //计算zone的索引是DMA 0 或其他 1,2,3等
    memmap_init(size, pgdat->node_id, zone_idx(zone), zone_start_pfn);
    //初始化zone中的free_area
    zone_init_free_lists(pgdat, zone, zone->spanned_pages);
    return 0;
}
    下面我们看
void __meminit memmap_init_zone(unsigned long size, int nid, unsigned long zone, unsigned long start_pfn, enum memmap_context context)
{
    struct page *page;
    unsigned long end_pfn = start_pfn + size;
    unsigned long pfn;

    for (pfn = start_pfn; pfn < end_pfn; pfn++) {
        if (context == MEMMAP_EARLY) {
            //#define early_pfn_valid(pfn) pfn_valid(pfn)     
            if (!early_pfn_valid(pfn))
                continue;

            if (!early_pfn_in_nid(pfn, nid))
                continue;
        }
        page = pfn_to_page(pfn); //在 CONFIG_FLATMEM 内存模式 基本上为 (mem_map + pfn)
        set_page_links(page, zone, nid, pfn); //用来设置page所属的节点,zone, node, section, 设置在struct page->flags中.
        init_page_count(page); //atomic_set(&page->_count, 1);
        reset_page_mapcount(page); //atomic_set(&(page)->_mapcount, -1);
        SetPageReserved(page); //设置页为保留
        INIT_LIST_HEAD(&page->lru);
#ifdef WANT_PAGE_VIRTUAL
        if (!is_highmem_idx(zone)) //不是高端内存
            set_page_address(page, __va(pfn << PAGE_SHIFT)); //设置页的虚拟地址
#endif
    }
}
//进一步初始化内存域连表
void __meminit build_all_zonelists(void)
{
    if (system_state == SYSTEM_BOOTING) { //启动时
        //这个函数循环在每个节点上调用 build_zonelists(NODE_DATA(nid)); 对于无NUMA就一个节点
        __build_all_zonelists(0);
        cpuset_init_current_mems_allowed();
    } else {
        /* we have to stop all cpus to guaranntee there is no user of zonelist */
        stop_machine_run(__build_all_zonelists, NULL, NR_CPUS);
        /* cpuset refresh routine should be here */
    }
    //计算出全部空闲的页
    vm_total_pages = nr_free_pagecache_pages(); //实现为 return nr_free_zone_pages(gfp_zone(GFP_HIGHUSER));
    printk("Built %i zonelists.  Total pages: %ld\n", num_online_nodes(), vm_total_pages);
}
//我们看无NUMA的
struct zonelist {
    struct zone *zones[MAX_NUMNODES * MAX_NR_ZONES + 1]; // NULL delimited               
};
static void __meminit build_zonelists(pg_data_t *pgdat)
{
    int i, j, k, node, local_node;
    local_node = pgdat->node_id; // 0

    for (i = 0; i < GFP_ZONETYPES; i++) { //GPF_ZONETYPES 是 5
        struct zonelist *zonelist;
        //获得节点中指向管理区链表的域
        zonelist = pgdat->node_zonelists + i;

        j = 0;
        //仔细看一下当 i=2 时,连表管理区有全部的zone
        k = highest_zone(i); //获得一个域类型 例如 ZONE_NORMAL 或 .. 等
        j = build_zonelists_node(pgdat, zonelist, j, k); //返回设置了几个zone
        ...... //这有几个循环在不是NUMA的情况下不执行

        zonelist->zones[j] = NULL; //最后一个为NULL表示结束
    }
}
static int __meminit build_zonelists_node(pg_data_t *pgdat, struct zonelist *zonelist, int nr_zones, int zone_type)
{
    struct zone *zone;

    BUG_ON(zone_type > ZONE_HIGHMEM);

    do {
        zone = pgdat->node_zones + zone_type; //指向相应类型的域
        if (populated_zone(zone)) { //zone有页存在
#ifndef CONFIG_HIGHMEM
            BUG_ON(zone_type > ZONE_NORMAL);
#endif
            //如果zone_type为ZONE_DMA,管理区链表zonelist将仅仅包含DMA管理区,如果为ZONE_HIGHMEM,
            //则管理区链表中就会依次有ZONE_HIGHMEM、 ZONE_NORMAL和ZONE_DMA
            zonelist->zones[nr_zones++] = zone; //安排好优先顺序
            check_highest_zone(zone_type);
        }
        zone_type--;
    } while (zone_type >= 0);
    return nr_zones;
}
static unsigned int nr_free_zone_pages(int offset)
{
    pg_data_t *pgdat = NODE_DATA(numa_node_id());
    unsigned int sum = 0;

    struct zonelist *zonelist = pgdat->node_zonelists + offset;
    struct zone **zonep = zonelist->zones;
    struct zone *zone;
    //计算出全部的空闲页
    for (zone = *zonep++; zone; zone = *zonep++) {
        unsigned long size = zone->present_pages;
        unsigned long high = zone->pages_high;
        if (size > high)
            sum += size - high;
    }
    return sum;
}
    最后内存初始化,释放前边标志为保留的所有页面
void __init mem_init(void)
{
    extern int ppro_with_ram_bug(void); // 检测pentium是否是有bug的cpu
    int codesize, reservedpages, datasize, initsize;
    int tmp;
    int bad_ppro;
#ifdef CONFIG_FLATMEM
    if (!mem_map)
        BUG();
#endif
    bad_ppro = ppro_with_ram_bug();

#ifdef CONFIG_HIGHMEM
    //确认fixmap和kmap映射范围没有重叠
    if (PKMAP_BASE+LAST_PKMAP*PAGE_SIZE >= FIXADDR_START) {

        printk(KERN_ERR "fixmap and kmap areas overlap - this will crash\n");
        printk(KERN_ERR "pkstart: %lxh pkend: %lxh fixstart %lxh\n",
                PKMAP_BASE, PKMAP_BASE+LAST_PKMAP*PAGE_SIZE, FIXADDR_START);
        BUG();
    }
#endif
    set_max_mapnr_init();   //设置num_physpages 和 (也许有)max_mapnr 全局变量
#ifdef CONFIG_HIGHMEM
    high_memory = (void *) __va(highstart_pfn * PAGE_SIZE - 1) + 1; //设置高端内存开始虚拟地址
#else
    high_memory = (void *) __va(max_low_pfn * PAGE_SIZE - 1) + 1;
#endif
    //实现为 return(free_all_bootmem_core(NODE_DATA(0)));
    totalram_pages += free_all_bootmem(); //根据页面位图释放内存中所有可供动态分配的页面

    reservedpages = 0;
    for (tmp = 0; tmp < max_low_pfn; tmp++)
        //在e820中寻找
        if (page_is_ram(tmp) && PageReserved(pfn_to_page(tmp)))
            reservedpages++;  //计算保留页
    //我认为下面这行应该和上面的循环颠倒一下,然后 max_low_pfn 应该为num_physpages. 这样可以统计所有的保留页
    set_highmem_pages_init(bad_ppro);//初始化高端页面

    //计算内核各个部分的大小
    codesize =  (unsigned long) &_etext - (unsigned long) &_text;
    datasize =  (unsigned long) &_edata - (unsigned long) &_etext;
    initsize =  (unsigned long) &__init_end - (unsigned long) &__init_begin;

    kclist_add(&kcore_mem, __va(0), max_low_pfn << PAGE_SHIFT);
    //#define VMALLOC_OFFSET  (8*1024*1024)    // 8M gap
    //#define VMALLOC_START   (((unsigned long) high_memory + vmalloc_earlyreserve + 2*VMALLOC_OFFSET-1) & ~(VMALLOC_OFFSET-1))
    //vmalloc_earlyreserve 在非discontig内存模式下为0
    kclist_add(&kcore_vmalloc, (void *)VMALLOC_START, VMALLOC_END-VMALLOC_START);

    //打印一些统计信息
    //nr_free_pages 函数核心就是
    // for_each_zone(zone) 循环所有zone
    //       sum += zone->free_pages; //统计所有空闲页数量,前面释放页调用过free_page()函数
    printk(KERN_INFO "Memory: %luk/%luk available (%dk kernel code, %dk reserved, %dk data, %dk init, %ldk highmem)\n",
            (unsigned long) nr_free_pages() << (PAGE_SHIFT-10),
            num_physpages << (PAGE_SHIFT-10), codesize >> 10,
            reservedpages << (PAGE_SHIFT-10), datasize >> 10,
            initsize >> 10, (unsigned long) (totalhigh_pages << (PAGE_SHIFT-10))
          );
    ......
    if (boot_cpu_data.wp_works_ok < 0)
        test_wp_bit(); //检查cpu是否支持写保护位,不支持就提示用户重新编译内核.

#ifndef CONFIG_SMP
    //由startup_32( )函数创建的物理内存前8MB的恒等映射用来完成内核的初始化阶段.
    //当这种映射不再必要时,内核调用这函数清除对应的页表项
    zap_low_mappings();
#endif
}
static void __init set_max_mapnr_init(void)
{
#ifdef CONFIG_HIGHMEM
    num_physpages = highend_pfn;
#else
    num_physpages = max_low_pfn;
#endif
#ifdef CONFIG_FLATMEM
    max_mapnr = num_physpages;
#endif
}
static unsigned long __init free_all_bootmem_core(pg_data_t *pgdat)
{
    struct page *page;
    unsigned long pfn;
    bootmem_data_t *bdata = pgdat->bdata;
    unsigned long i, count, total = 0;
    unsigned long idx;
    unsigned long *map;
    int gofast = 0;

    pfn = bdata->node_boot_start >> PAGE_SHIFT; //存放bootmem位图的第一个页面
    idx = bdata->node_low_pfn - (bdata->node_boot_start >> PAGE_SHIFT); //总计需要释放的内存页数量,  node_low_pfn 最高到896M的pfn
    map = bdata->node_bootmem_map; //节点内存位图

    if (bdata->node_boot_start == 0 || ffs(bdata->node_boot_start) - PAGE_SHIFT > ffs(BITS_PER_LONG))

        gofast = 1;
    for (i = 0; i < idx; ) {
        //没有使用页相应位是0,取反后为1
        unsigned long v = ~map[i / BITS_PER_LONG]; //#define BITS_PER_LONG 32
        if (gofast && v == ~0UL) { //v为0xFFFFFFFF,即连续32个页面都没有使用
            int order;
            page = pfn_to_page(pfn);
            count += BITS_PER_LONG;
            order = ffs(BITS_PER_LONG) - 1;
            __free_pages_bootmem(page, order);
            i += BITS_PER_LONG;
            page += BITS_PER_LONG;
        } else if (v) {
            unsigned long m;
            page = pfn_to_page(pfn);
            for (m = 1; m && i < idx; m<<=1, page++, i++) {
                if (v & m) { //相应位为1,没有使用
                    count++;
                    __free_pages_bootmem(page, 0); //释放页
                }
            }
        } else {
            i+=BITS_PER_LONG;
        }
        pfn += BITS_PER_LONG;
    }
    total += count;

    page = virt_to_page(bdata->node_bootmem_map); //释放位图页面
    count = 0;
    //一个字节8位,每一位代表一个pfn,下面 / 8 在 / PAGE_SIZE 就是换算使用了多少个页面
    for (i = 0; i < ((bdata->node_low_pfn-(bdata->node_boot_start >> PAGE_SHIFT))/8 + PAGE_SIZE-1)/PAGE_SIZE; i++,page++) {
        count++;
        __free_pages_bootmem(page, 0);
    }
    total += count;
    bdata->node_bootmem_map = NULL;
    return total;
}
void fastcall __init __free_pages_bootmem(struct page *page, unsigned int order)
{
    if (order == 0) {
        __ClearPageReserved(page); //清除页的保留标志
        set_page_count(page, 0);  //atomic_set(&page->_count, 0);
        set_page_refcounted(page); //set_page_count(page, 1);
        __free_page(page); //释放这页
    } else {
        int loop;
        prefetchw(page); //预取
        for (loop = 0; loop < BITS_PER_LONG; loop++) {
            struct page *p = &page[loop];
            if (loop + 1 < BITS_PER_LONG)
                prefetchw(p + 1);

            __ClearPageReserved(p);
            set_page_count(p, 0);
        }
        set_page_refcounted(page);
        __free_pages(page, order);
    }
}
    mem_init->
static void __init set_highmem_pages_init(int bad_ppro)
{
    int pfn;
    for (pfn = highstart_pfn; pfn < highend_pfn; pfn++)
        add_one_highpage_init(pfn_to_page(pfn), pfn, bad_ppro);
    totalram_pages += totalhigh_pages;
}
void __init add_one_highpage_init(struct page *page, int pfn, int bad_ppro)
{
    if (page_is_ram(pfn) && !(bad_ppro && page_kills_ppro(pfn))) {
        ClearPageReserved(page); //清除保留标志
        free_new_highpage(page);
    } else
        SetPageReserved(page); //设置保留标志,此页不能使用
}
static void __meminit free_new_highpage(struct page *page)
{
    init_page_count(page); //atomic_set(&page->_count, 1);
    __free_page(page);
    totalhigh_pages++; //统计高端内存页数量
}
    mem_init->
void kclist_add(struct kcore_list *new, void *addr, size_t size)
{
    new->addr = (unsigned long)addr;
    new->size = size;

    write_lock(&kclist_lock);
    new->next = kclist;
    kclist = new;
    write_unlock(&kclist_lock);
}

 

posted on 2013-08-30 11:46  SuperKing  阅读(1784)  评论(0编辑  收藏  举报

导航