Linux内存管理

来源:互联网 发布:文本相似算法 编辑:程序博客网 时间:2024/06/06 03:46

Linux内存管理(2):内存描述

 linux内存管理建立在基本的分页机制基础上,在linux内核中RAM的某些部分将会永久的分配给内核,并用来存放内核代码以及静态内核数据结构。RAM的其余部分称为动态内存,这不仅是进程所需的宝贵资源,也是内核本身所需的宝贵资源。实际上,整个系统的性能取决于如何有效地管理动态内存。因此,现在所有多任务操作系统都在经历优化对动态内存的使用,也就是说,尽可能做到当要时分配,不需要时释放。
    内存管理是os中最复杂的管理机制之一。linux中采用了很多有效的管理方法,包括页表管理、高端内存(临时映射区、固定映射区、永久映射区、非连续内存区)管理、为减小外部碎片的伙伴系统、为减小内部碎片的slab机制、伙伴系统未建立之前的页面分配制度以及紧急内存管理等等。

    linux使用于广泛的体系结构,因此需要用一种与体系结构无关的方式来描述内存。linux用VM描述和管理内存。在VM中使用的普遍概念就是非一致内存访问。对于大型机器而言,内存会分成许多簇,依据簇与处理器“距离”的不同,访问不同的簇会有不同的代价。每个簇都被认为是一个节点(pg_data_t),每个节点被分成很多的称为管理区(zone)的块,用于表示内存中的某个范围。zone的类型除了ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM以外,从linux2.6.32开始引入了ZONE_MOVABLE,用于适应大块连续内存的分配。每个物理页面由一个page结构体描述,所有的页结构都存储在一个全局的mem_map数组中(非平板模式),该数组通常存放在ZONE_NORMAL内存区域的首部,或者就在内存系统中为装入内核映像而预留的区域之后。内存描述的层次结构为pg_data_t--->zone--->mem_map数组(ZONE_XXX类型)--->page,如下图。下面的以2.6.32.45的内核代码为参考来介绍。

图1 内存描述的层次结构

    1、节点:pg_data_t

    内存的每个节点都有pg_data_t描述,在分配一个页面时,linux采用节点局部分配的策略,从最靠近运行中的CPU的节点分配内存。由于进程往往是在同一个CPU上运行,因此从当前节点得到的内存很可能被用到。pg_data_t在include/linux/mmzone.h中,如下:

[cpp] view plain copy
  1. /* 
  2.  * pg_data_t结构用在带有CONFIG_DISCONTIGMEM编译选项的机器中(最新的NUMA机器), 
  3.  * 以表示比zone结构更高一层次的内存区域。 
  4.  * 在NUMA机器上,每个NUMA节点由一个pg_data_t来描述它的内存布局。内存使用统计和 
  5.  * 页面交换数据结构由每个zone区域来维护 
  6.  */  
  7. struct bootmem_data;  
  8. typedef struct pglist_data {  
  9.     /* 该节点内的内存区。可能的区域类型用zone_type表示 */  
  10.     struct zone node_zones[MAX_NR_ZONES];  
  11.     /* 该节点的备用内存区。当节点没有可用内存时,就从备用区中分配内存 */  
  12.     struct zonelist node_zonelists[MAX_ZONELISTS];  
  13.     /* 可用内存区数目,即node_zones数据中保存的最后一个有效区域的索引 */  
  14.     int nr_zones;  
  15. #ifdef CONFIG_FLAT_NODE_MEM_MAP /* means !SPARSEMEM */  
  16.     /* 在平坦型的内存模型中,它指向本节点第一个页面的描述符 */  
  17.     struct page *node_mem_map;  
  18. #ifdef CONFIG_CGROUP_MEM_RES_CTLR  
  19.     /* cgroup相关 */  
  20.     struct page_cgroup *node_page_cgroup;  
  21. #endif  
  22. #endif  
  23.     /* 在内存子系统初始化以前,即boot阶段也需要进行内存管理。  
  24.      * 此结构用于这个阶段的内存管理。  
  25.      */  
  26.     struct bootmem_data *bdata;  
  27. #ifdef CONFIG_MEMORY_HOTPLUG  
  28.     /* 当系统支持内存热插拨时,这个锁用于保护本结构中的与节点大小相关的字段。 
  29.      * 当你希望node_start_pfn,node_present_pages,node_spanned_pages仍保持常量时, 
  30.      * 需要持有该锁。 
  31.      */  
  32.     spinlock_t node_size_lock;  
  33. #endif  
  34.     unsigned long node_start_pfn; /*起始页面帧号,指出该节点在全局mem_map中的偏移*/  
  35.     unsigned long node_present_pages; /* 物理页的总数 */  
  36.     unsigned long node_spanned_pages; /* 物理页范围的跨度,包括holes */  
  37.     int node_id;  /* 节点编号 */  
  38.     /* 等待该节点内的交换守护进程的等待队列。将节点中的页帧换出时会用到 */  
  39.     wait_queue_head_t kswapd_wait;  
  40.     /* 负责该节点的交换守护进程 */  
  41.     struct task_struct *kswapd;  
  42.     /* 由页交换子系统使用,定义要释放的区域大小 */  
  43.     int kswapd_max_order;  
  44. } pg_data_t;  
    该结构的主要数据有内存区、备用内存区、可用内存区计数、锁、物理页总数、物理页范围跨度、所属交换守护进程等。一个节点通过node_zones数组有维护多个zone管理区。
    2、管理区:zone
    管理区用于跟踪诸如页面使用情况统计数,空闲区域信息和锁信息等。每个管理区由一个zone结构体描述,管理区的类型由zone_type描述,都在include/linux/mmzone.h中。如下:
[cpp] view plain copy
  1. enum zone_type {  
  2. #ifdef CONFIG_ZONE_DMA  
  3.     /* 
  4.      * ZONE_DMA is used when there are devices that are not able 
  5.      * to do DMA to all of addressable memory (ZONE_NORMAL). Then we 
  6.      * carve out the portion of memory that is needed for these devices. 
  7.      * The range is arch specific. 
  8.      * 
  9.      * Some examples 
  10.      * 
  11.      * Architecture     Limit 
  12.      * --------------------------- 
  13.      * parisc, ia64, sparc  <4G 
  14.      * s390         <2G 
  15.      * arm          Various 
  16.      * alpha        Unlimited or 0-16MB. 
  17.      * 
  18.      * i386, x86_64 and multiple other arches 
  19.      *          <16M. 
  20.      */  
  21.     ZONE_DMA,  
  22. #endif  
  23. #ifdef CONFIG_ZONE_DMA32  
  24.     /* 
  25.      * x86_64 needs two ZONE_DMAs because it supports devices that are 
  26.      * only able to do DMA to the lower 16M but also 32 bit devices that 
  27.      * can only do DMA areas below 4G. 
  28.      */  
  29.     ZONE_DMA32,  
  30. #endif  
  31.     /* 
  32.      * Normal addressable memory is in ZONE_NORMAL. DMA operations can be 
  33.      * performed on pages in ZONE_NORMAL if the DMA devices support 
  34.      * transfers to all addressable memory. 
  35.      */  
  36.     ZONE_NORMAL,  
  37. #ifdef CONFIG_HIGHMEM  
  38.     /* 
  39.      * A memory area that is only addressable by the kernel through 
  40.      * mapping portions into its own address space. This is for example 
  41.      * used by i386 to allow the kernel to address the memory beyond 
  42.      * 900MB. The kernel will set up special mappings (page 
  43.      * table entries on i386) for each page that the kernel needs to 
  44.      * access. 
  45.      */  
  46.     ZONE_HIGHMEM,  
  47. #endif  
  48.     ZONE_MOVABLE,  
  49.     __MAX_NR_ZONES  
  50. };  
    管理区类型介绍:
    (1)ZONE_DMA:用在当有设备不能通过DMA访问整个可寻址内存(ZONE_NORMAL)的情况下。这时我们就要为这些设备专门开辟出一段内存,通常是低端内存区域。ZONE_DMA的内存范围与体系结构有关,parisc、ia64以及sparc中是小于4G;s390是小于2G;arm中是可变的多种多样的;alpha中是无限或者0-16MB;i386、x86_64以及其他很多体系结构是小于16MB(0-16MB)。
    (2)ZONE_DMA32:注意x86_64需要两个ZONE_DMA区域,因为它既支持只能访问16MB以下DMA区域的设备,也支持只能访问4GB以下DMA区域的32位设备,ZONE_DMA32针对后一种情况。
    (3)ZONE_NORMAL:正常的可访问内存。如果DMA设备能支持传输数据到整个可访问内存,则DMA操作也能在ZONE_NORMAL类型的页面上进行。
    (4)ZONE_HIGHMEM:映射到内核代码本身的内核地址空间,一般是高端内存区域,它只能由内核访问,用户空间访问不到。所有的内核操作都只能使用这个内存区域来进行,因此这是对性能至关重要的区域。例如i386允许内核访问超过900MB的内存,对每个内核需要访问的页面,内核将设置特别的映射项(i386上的页表项)。
    (5)ZONE_MOVABLE:这是一个伪内存段。为了防止形成物理内存碎片,可以将虚拟地址对应的物理地址进行迁移,使多个碎片合并成一块连续的大内存。ZONE_MOVABLE类型用于适应大块连续内存的分配。
[cpp] view plain copy
  1. struct zone {  
  2.     /* 被页面分配器访问的通用域 */  
  3.   
  4.     /* 本管理区的三个水线值:高水线(比较充足)、低水线、MIN水线。会被*_wmark_pages(zone)宏访问 */  
  5.     unsigned long watermark[NR_WMARK];  
  6.   
  7.     /* 当可用页数在本水线值以下时,在读取可用页计数值时,需要增加额外的工作以避免每个CPU的计数器 
  8.      * 漂移导致水线值被打破    
  9.      */  
  10.     unsigned long percpu_drift_mark;  
  11.   
  12.     /* 我们不知道即将分配的内存是否可用,以及最终是否会被释放,因此为了避免浪费几GB的RAM,我们 
  13.      * 必须额外保留一些低端区域的内存(如DMA区域)供驱动使用。否则我们会面临在低端区域内出现 
  14.      * OOM(Out of Memory)的风险,尽管这时高端区域还有大量可用的RAM。本字段是指从上级内存区 
  15.      * 退到回内存区时,需要额外保留的内存数量。如果在运行时sysctl_lowmem_reserve_ratio控制 
  16.      * 改变,它会被重新计算 
  17.      */  
  18.     unsigned long       lowmem_reserve[MAX_NR_ZONES];  
  19.   
  20. #ifdef CONFIG_NUMA  
  21.     int node; /* 所属的NUMA节点 */  
  22.     /* 未映射的页(即可回收的页)超过此值,将进行页面回收 */  
  23.     unsigned long       min_unmapped_pages;  
  24.     /* 管理区中用于slab的可回收页大于此值时,将回收slab中的缓存页 */   
  25.     unsigned long       min_slab_pages;  
  26.      /*  
  27.       * 每CPU的页面缓存。  
  28.       * 当分配单个页面时,首先从该缓存中分配页面。这样可以:  
  29.       * 避免使用全局的锁  
  30.       * 避免同一个页面反复被不同的CPU分配,引起缓存页的失效。  
  31.       * 避免将管理区中的大块分割成碎片。  
  32.       */    
  33.     struct per_cpu_pageset  *pageset[NR_CPUS];  
  34. #else  
  35.     struct per_cpu_pageset  pageset[NR_CPUS];  
  36. #endif  
  37.     /* 该锁用于保护伙伴系统数据结构。即保护free_area相关数据 */   
  38.     spinlock_t      lock;  
  39. #ifdef CONFIG_MEMORY_HOTPLUG  
  40.     /* 用于保护spanned/present_pages等变量。这些变量几乎不会发生变化,除非发生了内存热插拨操作。  
  41.      * 这几个变量并不被lock字段保护。并且主要用于读,因此使用读写锁 */  
  42.     seqlock_t       span_seqlock;  
  43. #endif  
  44.     /* 伙伴系统的主要变量。这个数组定义了11个队列,每个队列中的元素都是大小为2^n的页面 */  
  45.     struct free_area    free_area[MAX_ORDER];  
  46.   
  47. #ifndef CONFIG_SPARSEMEM  
  48.     /* 本管理区里的pageblock_nr_pages块标志数组,参考pageblock-flags.h 
  49.      * 在SPARSEMEM中,本映射存储在结构mem_section中 */  
  50.     unsigned long       *pageblock_flags;  
  51. #endif /* CONFIG_SPARSEMEM */  
  52.   
  53.     /* 填充的未用字段,确保后面的字段是缓存行对齐的 */   
  54.     ZONE_PADDING(_pad1_)  
  55.   
  56.     /* 被页面回收扫描器访问的通用域 */  
  57.     /*  
  58.     * lru相关的字段用于内存回收。这个锁用于保护这几个回收相关的字段。  
  59.     * lru用于确定哪些字段是活跃的,哪些不是活跃的,并据此确定应当被写回到磁盘以释放内存。  
  60.      */    
  61.     spinlock_t      lru_lock;  
  62.     /* 匿名活动页、匿名不活动页、文件活动页、文件不活动页链表头 */  
  63.     struct zone_lru {  
  64.         struct list_head list;  
  65.     } lru[NR_LRU_LISTS];  
  66.   
  67.     struct zone_reclaim_stat reclaim_stat; /* 页面回收状态 */  
  68.     /* 自从最后一次回收页面以来,扫过的页面数 */  
  69.     unsigned long       pages_scanned;  
  70.     unsigned long       flags;         /* 管理区标志,参考下面 */  
  71.   
  72.     /* Zone statistics */  
  73.     atomic_long_t       vm_stat[NR_VM_ZONE_STAT_ITEMS];  
  74.   
  75.     /* 
  76.      * prev_priority holds the scanning priority for this zone.  It is 
  77.      * defined as the scanning priority at which we achieved our reclaim 
  78.      * target at the previous try_to_free_pages() or balance_pgdat() 
  79.      * invokation. 
  80.      * 
  81.      * We use prev_priority as a measure of how much stress page reclaim is 
  82.      * under - it drives the swappiness decision: whether to unmap mapped 
  83.      * pages. 
  84.      * 
  85.      * Access to both this field is quite racy even on uniprocessor.  But 
  86.      * it is expected to average out OK. 
  87.      */  
  88.     int prev_priority;  
  89.   
  90.     /* 
  91.      * The target ratio of ACTIVE_ANON to INACTIVE_ANON pages on 
  92.      * this zone's LRU.  Maintained by the pageout code. 
  93.      */  
  94.     unsigned int inactive_ratio;  
  95.   
  96.     /* 为cache对齐 */  
  97.     ZONE_PADDING(_pad2_)  
  98.     /* Rarely used or read-mostly fields */  
  99.   
  100.     /* 
  101.      * wait_table       -- the array holding the hash table 
  102.      * wait_table_hash_nr_entries   -- the size of the hash table array 
  103.      * wait_table_bits  -- wait_table_size == (1 << wait_table_bits) 
  104.      * 
  105.      * The purpose of all these is to keep track of the people 
  106.      * waiting for a page to become available and make them 
  107.      * runnable again when possible. The trouble is that this 
  108.      * consumes a lot of space, especially when so few things 
  109.      * wait on pages at a given time. So instead of using 
  110.      * per-page waitqueues, we use a waitqueue hash table. 
  111.      * 
  112.      * The bucket discipline is to sleep on the same queue when 
  113.      * colliding and wake all in that wait queue when removing. 
  114.      * When something wakes, it must check to be sure its page is 
  115.      * truly available, a la thundering herd. The cost of a 
  116.      * collision is great, but given the expected load of the 
  117.      * table, they should be so rare as to be outweighed by the 
  118.      * benefits from the saved space. 
  119.      * 
  120.      * __wait_on_page_locked() and unlock_page() in mm/filemap.c, are the 
  121.      * primary users of these fields, and in mm/page_alloc.c 
  122.      * free_area_init_core() performs the initialization of them. 
  123.      */  
  124.     wait_queue_head_t   * wait_table;  
  125.     unsigned long       wait_table_hash_nr_entries;  
  126.     unsigned long       wait_table_bits;  
  127.   
  128.     /* 
  129.      * Discontig memory support fields. 
  130.      */  
  131.     struct pglist_data  *zone_pgdat; /* 本管理区所属的节点 */  
  132.     /* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */  
  133.     unsigned long       zone_start_pfn; /* 管理区的页面在mem_map中的偏移 */  
  134.   
  135.     /* 
  136.      * zone_start_pfn, spanned_pages and present_pages are all 
  137.      * protected by span_seqlock.  It is a seqlock because it has 
  138.      * to be read outside of zone->lock, and it is done in the main 
  139.      * allocator path.  But, it is written quite infrequently. 
  140.      * 
  141.      * The lock is declared along with zone->lock because it is 
  142.      * frequently read in proximity to zone->lock.  It's good to 
  143.      * give them a chance of being in the same cacheline. 
  144.      */  
  145.     unsigned long       spanned_pages;  /* total size, including holes */  
  146.     unsigned long       present_pages;  /* amount of memory (excluding holes) */  
  147.   
  148.     const char      *name; /* 很少使用的域 */  
  149. } ____cacheline_internodealigned_in_smp;  
    zone结构中的字段主要分两大类,一类是被页面分配器访问的字段,有水线值、保留的DMA内存区域数量、所属NUMA节点、未映射页数、slab中缓存页数、每个CPU的缓存页面集、伙伴系统可用区域数组free_area、页面标志数组等。一类是被页面回收器访问的字段,有LRU链表(用于LRU页面回收算法)、页面回收统计信息、所属的pglist_data节点、页面在mem_map中的偏移等。
    3、物理页面:page
    系统中每个物理页面都有一个相关联的page用于记录该页面的状态。在include/linux/mm_types.h中,如下:
[cpp] view plain copy
  1. /* 
  2.  * 系统中每个物理页面有一个相关联的page结构,用于记录该页面的状态。注意虽然当该页面是 
  3.  * 一个缓存页时,rmap结构能告诉我们谁正在映射它,但我们并没有一般的方法来跟踪哪个进程正在使用该页面 
  4.  */  
  5. struct page {  
  6.     unsigned long flags;        /* 原子标志,一些可以会被异步更新 */  
  7.     atomic_t _count;        /* 使用计数,参考下面 */  
  8.     union {  
  9.         atomic_t _mapcount; /* 在mms中映射的ptes计数,用于表明页面什么时候被映射, 
  10.                       * 并且限制反向映射搜索 
  11.                       */  
  12.         struct {        /* SLUB */  
  13.             u16 inuse;  
  14.             u16 objects;  
  15.         };  
  16.     };  
  17.     union {  
  18.         struct {  
  19.         unsigned long private;      /* 映射时的私有非透明数据: 
  20.                          * 如果设置PagePrivate,则用作buffer_heads; 
  21.                          * 如果设置PageSwapCache,则用作swp_entry_t; 
  22.                          * 如果设置PG_buddy,则表示在伙伴系统中的顺序编号 
  23.                          */  
  24.         struct address_space *mapping;  /* 如果低端bit清除,则指向inode地址空间,或者为null. 
  25.                          * 如果页面被映射为匿名内存,低端bit设置,则指向 
  26.                          * anon_vma对象,参看PAGE_MAPPING_ANON 
  27.                          */  
  28.         };  
  29. #if USE_SPLIT_PTLOCKS  
  30.         spinlock_t ptl;  
  31. #endif  
  32.         struct kmem_cache *slab;    /* SLUB: 指向slab的指针 */  
  33.         /* 如果属于伙伴系统,并且不是伙伴系统中的第一个页则指向第一个页 */  
  34.         struct page *first_page;  
  35.     };  
  36.     union {  /* 如果是文件映射,那么表示本页面在文件中的位置(偏移) */  
  37.         pgoff_t index;      /* Our offset within mapping. */  
  38.         void *freelist;     /* SLUB: freelist req. slab lock */  
  39.     };  
  40.     struct list_head lru;       /* Pageout list, eg. active_list 
  41.                      * protected by zone->lru_lock ! 
  42.                      */  
  43.     /* 
  44.      * On machines where all RAM is mapped into kernel address space, 
  45.      * we can simply calculate the virtual address. On machines with 
  46.      * highmem some memory is mapped into kernel virtual memory 
  47.      * dynamically, so we need a place to store that address. 
  48.      * Note that this field could be 16 bits on x86 ... ;) 
  49.      * 
  50.      * Architectures with slow multiplication can define 
  51.      * WANT_PAGE_VIRTUAL in asm/page.h 
  52.      */  
  53. #if defined(WANT_PAGE_VIRTUAL)  
  54.     void *virtual;          /* 内核虚拟地址(如果没有被内核映射,则为NULL,例如高端内存hignmem) */  
  55. #endif /* WANT_PAGE_VIRTUAL */  
  56. #ifdef CONFIG_WANT_PAGE_DEBUG_FLAGS  
  57.     unsigned long debug_flags;  /* Use atomic bitops on this */  
  58. #endif  
  59.   
  60. #ifdef CONFIG_KMEMCHECK  
  61.     /* kmemcheck想跟踪一个page中的每个byte的状态,这是一个指向这种状态块的指针。 
  62.      * 如果没有被跟踪,则为NULL 
  63.      */  
  64.     void *shadow;  
  65. #endif  
  66. };  
    该结构主要包含原子标志、使用计数、指向的地址空间、指向slab的指针、文件中的位置(如果是文件映射)、状态跟踪指针等。
    4、全局的mem_map数组:定义在include/linux/mmzone.h中,如下:
[cpp] view plain copy
  1. #ifndef CONFIG_DISCONTIGMEM  
  2. /* 物理页数组,对discontigmem使用pgdat->lmem_map */  
  3. extern struct page *mem_map;  
  4. #endif  
    这个数组保存了所有的物理页page结构,它存储在ZONE_NORMAL内存区域的开头,用于跟踪所有的物理页面。

Linux内存管理(3):内存探测与初始化

  1、内存探测

    linux在被bootloader加载到内存后, cpu最初执行的内核代码是arch/x86/boot/header.S汇编文件中的_start例程,设置好头部header,其中包括大量的bootloader参数。接着是其中的start_of_setup例程,这个例程在做了一些准备工作后会通过call main跳转到arch/x86/boot/main.c:main()函数处执行,这就是众所周知的x86下的main函数,它们都工作在实模式下。在这个main函数中我们可以第一次看到与内存管理相关的代码,这段代码调用detect_memory()函数检测系统物理内存。如下:

[cpp] view plain copy
  1. void main(void)  
  2. {  
  3.     /* First, copy the boot header into the "zeropage" */  
  4.     copy_boot_params(); /* 把头部各参数复制到boot_params变量中 */  
  5.   
  6.     /* End of heap check */  
  7.     init_heap();  
  8.   
  9.     /* Make sure we have all the proper CPU support */  
  10.     if (validate_cpu()) {  
  11.         puts("Unable to boot - please use a kernel appropriate "  
  12.              "for your CPU.\n");  
  13.         die();  
  14.     }  
  15.   
  16.     /* Tell the BIOS what CPU mode we intend to run in. */  
  17.     set_bios_mode();  
  18.   
  19.     /* Detect memory layout */  
  20.     detect_memory(); /* 内存探测函数 */  
  21.   
  22.     /* Set keyboard repeat rate (why?) */  
  23.     keyboard_set_repeat();  
  24.   
  25.     /* Query MCA information */  
  26.     query_mca();  
  27.   
  28.     /* Query Intel SpeedStep (IST) information */  
  29.     query_ist();  
  30.   
  31.     /* Query APM information */  
  32. #if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE)  
  33.     query_apm_bios();  
  34. #endif  
  35.   
  36.     /* Query EDD information */  
  37. #if defined(CONFIG_EDD) || defined(CONFIG_EDD_MODULE)  
  38.     query_edd();  
  39. #endif  
  40.   
  41.     /* Set the video mode */  
  42.     set_video();  
  43.   
  44.     /* Parse command line for 'quiet' and pass it to decompressor. */  
  45.     if (cmdline_find_option_bool("quiet"))  
  46.         boot_params.hdr.loadflags |= QUIET_FLAG;  
  47.   
  48.     /* Do the last things and invoke protected mode */  
  49.     go_to_protected_mode();  
  50. }  
    内存探测的实现在arch/x86/boot/memory.c中,如下:

[cpp] view plain copy
  1. int detect_memory(void)  
  2. {  
  3.     int err = -1;  
  4.   
  5.     if (detect_memory_e820() > 0)  
  6.         err = 0;  
  7.   
  8.     if (!detect_memory_e801())  
  9.         err = 0;  
  10.   
  11.     if (!detect_memory_88())  
  12.         err = 0;  
  13.   
  14.     return err;  
  15. }  
    由上面的代码可知,linux内核会分别尝试调用detect_memory_e820()、detcct_memory_e801()、detect_memory_88()获得系统物理内存布局,这3个函数都在memory.c中实现,它们内部其实都会以内联汇编的形式调用bios中断以取得内存信息,该中断调用形式为int 0x15,同时调用前分别把AX寄存器设置为0xe820h、0xe801h、0x88h,关于0x15号中断有兴趣的可以去查询相关手册。下面分析detect_memory_e820()的代码,其它代码基本一样。

[cpp] view plain copy
  1. #define SMAP    0x534d4150  /* ASCII "SMAP" */  
  2.   
  3. static int detect_memory_e820(void)  
  4. {  
  5.     int count = 0; /* 用于记录已检测到的物理内存数目 */  
  6.     struct biosregs ireg, oreg;  
  7.     struct e820entry *desc = boot_params.e820_map;  
  8.     static struct e820entry buf; /* static so it is zeroed */  
  9.   
  10.     initregs(&ireg); /* 初始化ireg中的相关寄存器 */  
  11.     ireg.ax  = 0xe820;  
  12.     ireg.cx  = sizeof buf; /* e820entry数据结构大小 */  
  13.     ireg.edx = SMAP; /* 标识 */  
  14.     ireg.di  = (size_t)&buf; /* int15返回值的存放处 */  
  15.   
  16.     /* 
  17.      * Note: at least one BIOS is known which assumes that the 
  18.      * buffer pointed to by one e820 call is the same one as 
  19.      * the previous call, and only changes modified fields.  Therefore, 
  20.      * we use a temporary buffer and copy the results entry by entry. 
  21.      * 
  22.      * This routine deliberately does not try to account for 
  23.      * ACPI 3+ extended attributes.  This is because there are 
  24.      * BIOSes in the field which report zero for the valid bit for 
  25.      * all ranges, and we don't currently make any use of the 
  26.      * other attribute bits.  Revisit this if we see the extended 
  27.      * attribute bits deployed in a meaningful way in the future. 
  28.      */  
  29.   
  30.     do {  
  31.         /* 在执行这条内联汇编语句时输入的参数有:  
  32.         eax寄存器=0xe820  
  33.         dx寄存器=’SMAP’  
  34.         edi寄存器=desc  
  35.         ebx寄存器=next  
  36.         ecx寄存器=size  
  37.           
  38.          返回给c语言代码的参数有:  
  39.         id=eax寄存器 
  40.         rr=edx寄存器  
  41.         ext=ebx寄存器 
  42.         size=ecx寄存器  
  43.         desc指向的内存地址在执行0x15中断调用时被设置  
  44.         */    
  45.         intcall(0x15, &ireg, &oreg);  
  46.         ireg.ebx = oreg.ebx; /* 选择下一个 */  
  47.   
  48.         /* BIOSes which terminate the chain with CF = 1 as opposed 
  49.            to %ebx = 0 don't always report the SMAP signature on 
  50.            the final, failing, probe. */  
  51.         if (oreg.eflags & X86_EFLAGS_CF)  
  52.             break;  
  53.   
  54.         /* Some BIOSes stop returning SMAP in the middle of 
  55.            the search loop.  We don't know exactly how the BIOS 
  56.            screwed up the map at that point, we might have a 
  57.            partial map, the full map, or complete garbage, so 
  58.            just return failure. */  
  59.         if (oreg.eax != SMAP) {  
  60.             count = 0;  
  61.             break;  
  62.         }  
  63.   
  64.         *desc++ = buf; /* 将buf赋值给desc */  
  65.         count++; /* 探测数加一 */  
  66.     } while (ireg.ebx && count < ARRAY_SIZE(boot_params.e820_map));  
  67.     /* 将内存块数保持到变量中 */  
  68.     return boot_params.e820_entries = count;  
  69. }  
    由于历史原因,一些I/O设备也会占据一部分内存物理地址空间,因此系统可以使用的物理内存空间是不连续的,系统内存被分成了很多段,每个段的属性也是不一样的。int 0x15查询物理内存时每次返回一个内存段的信息,因此要想返回系统中所有的物理内存,我们必须以迭代的方式去查询。detect_memory_e820()函数把int 0x15放到一个do-while循环里,每次得到的一个内存段放到struct e820entry里,而struct e820entry的结构正是e820返回结果的结构。像其它启动时获得的结果一样,最终都会被放到boot_params里,探测到的各个内存段情况被放到了boot_params.e820_map。
    这里存放中断返回值的e820entry结构,以及表示内存图的e820map结构均位于arch/x86/include/asm/e820.h中,如下:
[cpp] view plain copy
  1. struct e820entry {  
  2.     __u64 addr; /* 内存段的开始 */  
  3.     __u64 size; /* 内存段的大小 */  
  4.     __u32 type; /* 内存段的类型 */  
  5. } __attribute__((packed));  
  6.   
  7. struct e820map {  
  8.     __u32 nr_map;  
  9.     struct e820entry map[E820_X_MAX];  
  10. };  
    内存探测用于检测出系统有多少个通常不连续的内存区块。之后要建立一个描述这些内存块的内存图数据结构,这就是上面的e820map结构,其中nr_map为检测到的系统中内存区块数,不能超过E820_X_MAX(定义为128),map数组描述各个内存块的情况,包括其开始地址、内存块大小、类型。

     对于32位的系统,通过调用链arch/x86/boot/main.c:main()--->arch/x86/boot/pm.c:go_to_protected_mode()--->arch/x86/boot/pmjump.S:protected_mode_jump()--->arch/i386/boot/compressed/head_32.S:startup_32()--->arch/x86/kernel/head_32.S:startup_32()--->arch/x86/kernel/head32.c:i386_start_kernel()--->init/main.c:start_kernel(),到达众所周知的Linux内核启动函数start_kernel(),这里会调用setup_arch()完成与体系结构相关的一系列初始化工作,其中就包括各种内存的初始化工作,如内存图的建立、管理区的初始化等等。对x86体系结构,setup_arch()函数在arch/x86/kernel/setup.c中,如下:

[cpp] view plain copy
  1. void __init setup_arch(char **cmdline_p)  
  2. {  
  3.     /* ...... */  
  4.   
  5.     x86_init.oem.arch_setup();  
  6.   
  7.     setup_memory_map(); /* 建立内存图 */  
  8.     parse_setup_data();  
  9.     /* update the e820_saved too */  
  10.     e820_reserve_setup_data();  
  11.   
  12.     /* ...... */  
  13.   
  14.     /* 
  15.      * partially used pages are not usable - thus 
  16.      * we are rounding upwards: 
  17.      */  
  18.     max_pfn = e820_end_of_ram_pfn(); /* 找出最大可用内存页面帧号 */  
  19.   
  20.     /* preallocate 4k for mptable mpc */  
  21.     early_reserve_e820_mpc_new();  
  22.     /* update e820 for memory not covered by WB MTRRs */  
  23.     mtrr_bp_init();  
  24.     if (mtrr_trim_uncached_memory(max_pfn))  
  25.         max_pfn = e820_end_of_ram_pfn();  
  26.   
  27. #ifdef CONFIG_X86_32  
  28.     /* max_low_pfn在这里更新 */  
  29.     find_low_pfn_range(); /* 找出低端内存的最大页帧号 */  
  30. #else  
  31.     num_physpages = max_pfn;  
  32.   
  33.     /* ...... */  
  34.   
  35.     /* max_pfn_mapped在这更新 */  
  36.     /* 初始化内存映射机制 */  
  37.     max_low_pfn_mapped = init_memory_mapping(0, max_low_pfn<<PAGE_SHIFT);  
  38.     max_pfn_mapped = max_low_pfn_mapped;  
  39.   
  40. #ifdef CONFIG_X86_64  
  41.     if (max_pfn > max_low_pfn) {  
  42.         max_pfn_mapped = init_memory_mapping(1UL<<32,  
  43.                              max_pfn<<PAGE_SHIFT);  
  44.         /* can we preseve max_low_pfn ?*/  
  45.         max_low_pfn = max_pfn;  
  46.     }  
  47. #endif  
  48.   
  49.     /* ...... */  
  50.   
  51.     initmem_init(0, max_pfn); /* 启动内存分配器 */  
  52.   
  53.     /* ...... */  
  54.   
  55.     x86_init.paging.pagetable_setup_start(swapper_pg_dir);  
  56.     paging_init(); /* 建立完整的页表 */  
  57.     x86_init.paging.pagetable_setup_done(swapper_pg_dir);  
  58.   
  59.     /* ...... */  
  60. }  
    几乎所有的内存初始化工作都是在setup_arch()中完成的,主要的工作包括:
    (1)建立内存图:setup_memory_map();
    (2)调用e820_end_of_ram_pfn()找出最大可用页帧号max_pfn,调用find_low_pfn_range()找出低端内存区的最大可用页帧号max_low_pfn。
    (2)初始化内存映射机制:init_memory_mapping();
    (3)初始化内存分配器:initmem_init();
    (4)建立完整的页表:paging_init()。
    2、建立内存图
    内存探测完之后,就要建立描述各内存块情况的全局内存图结构了。函数为setup_arch()--->arch/x86/kernel/e820.c:setup_memory_map(),如下:

[cpp] view plain copy
  1. void __init setup_memory_map(void)  
  2. {  
  3.     char *who;  
  4.     /* 调用x86体系下的memory_setup函数 */  
  5.     who = x86_init.resources.memory_setup();  
  6.     /* 保存到e820_saved中 */  
  7.     memcpy(&e820_saved, &e820, sizeof(struct e820map));  
  8.     printk(KERN_INFO "BIOS-provided physical RAM map:\n");  
  9.     /* 打印输出 */  
  10.     e820_print_map(who);  
  11. }  
    该函数调用x86_init.resources.memory_setup()实现对BIOS e820内存图的设置和优化,然后将全局e820中的值保存在e820_saved中,并打印内存图。Linux的内存图保存在一个全局的e820变量中,还有其备份e820_saved,这两个全局的e820map结构变量均定义在arch/x86/kernel/e820.c中。memory_setup()函数是建立e820内存图的核心函数,从arch/x86/kernel/x86_init.c中可知,x86_init.resources.memory_setup()就是e820.c中的default_machine_specific_memory_setup()函数,如下:

[cpp] view plain copy
  1. char *__init default_machine_specific_memory_setup(void)  
  2. {  
  3.     char *who = "BIOS-e820";  
  4.     u32 new_nr;  
  5.     /* 
  6.      * 复制BIOS提供的e820内存图,否则伪造一个内存图:一块为0-640k,接着的 
  7.      * 下一块为1mb到appropriate_mem_k的大小 
  8.      */  
  9.     new_nr = boot_params.e820_entries;  
  10.     /* 将重叠的去除 */  
  11.     sanitize_e820_map(boot_params.e820_map,  
  12.             ARRAY_SIZE(boot_params.e820_map),  
  13.             &new_nr);  
  14.     /* 去掉重叠的部分后得到的内存块个数 */  
  15.     boot_params.e820_entries = new_nr;   
  16.     /* 将其复制到全局变量e820中,小于0时,为出错处理 */  
  17.     if (append_e820_map(boot_params.e820_map, boot_params.e820_entries)  
  18.       < 0) {  
  19.         u64 mem_size;  
  20.   
  21.         /* compare results from other methods and take the greater */  
  22.         if (boot_params.alt_mem_k  
  23.             < boot_params.screen_info.ext_mem_k) {  
  24.             mem_size = boot_params.screen_info.ext_mem_k;  
  25.             who = "BIOS-88";  
  26.         } else {  
  27.             mem_size = boot_params.alt_mem_k;  
  28.             who = "BIOS-e801";  
  29.         }  
  30.   
  31.         e820.nr_map = 0;  
  32.         e820_add_region(0, LOWMEMSIZE(), E820_RAM);  
  33.         e820_add_region(HIGH_MEMORY, mem_size << 10, E820_RAM);  
  34.     }  
  35.   
  36.     /* In case someone cares... */  
  37.     return who;  
  38. }  
  39.   
  40. /* 
  41.  * 复制BIOS e820内存图到一个安全的地方。如果我们在里面,则要进行重叠检查 
  42.  * 如果我们用的是现代系统,则设置代码将给我们提供一个可以使用的内存图,以便 
  43.  * 用它来建立内存。如果不是现代系统,则将伪造一个内存图 
  44.  */  
  45. static int __init append_e820_map(struct e820entry *biosmap, int nr_map)  
  46. {  
  47.     /* Only one memory region (or negative)? Ignore it */  
  48.     if (nr_map < 2)  
  49.         return -1;  
  50.   
  51.     return __append_e820_map(biosmap, nr_map);  
  52. }  
  53.   
  54. static int __init __append_e820_map(struct e820entry *biosmap, int nr_map)  
  55. {  
  56.     while (nr_map) { /* 循环nr_map次调用,添加内存块到e820 */  
  57.         u64 start = biosmap->addr;  
  58.         u64 size = biosmap->size;  
  59.         u64 end = start + size;  
  60.         u32 type = biosmap->type;  
  61.   
  62.         /* Overflow in 64 bits? Ignore the memory map. */  
  63.         if (start > end)  
  64.             return -1;  
  65.         /* 添加函数 */  
  66.         e820_add_region(start, size, type);  
  67.   
  68.         biosmap++;  
  69.         nr_map--;  
  70.     }  
  71.     return 0;  
  72. }  
  73.   
  74. void __init e820_add_region(u64 start, u64 size, int type)  
  75. {  
  76.     __e820_add_region(&e820, start, size, type);  
  77. }  
  78.   
  79. /* 
  80.  * 添加一个内存块到内存e820内存图中 
  81.  */  
  82. static void __init __e820_add_region(struct e820map *e820x, u64 start, u64 size,  
  83.                      int type)  
  84. {  
  85.     int x = e820x->nr_map;  
  86.   
  87.     if (x >= ARRAY_SIZE(e820x->map)) {  
  88.         printk(KERN_ERR "Ooops! Too many entries in the memory map!\n");  
  89.         return;  
  90.     }  
  91.   
  92.     e820x->map[x].addr = start;  
  93.     e820x->map[x].size = size;  
  94.     e820x->map[x].type = type;  
  95.     e820x->nr_map++;  
  96. }  
    从以上代码可知,内存图设置函数memory_setup()    把从BIOS中探测到的内存块情况(保存在boot_params.e820_map中)做重叠检测,把重叠的内存块去除,然后调用append_e820_map()将它们添加到全局的e920变量中,具体完成添加工作的函数是__e820_add_region()。到这里,物理内存就已经从BIOS中读出来存放到全局变量e820中,e820是linux内核中用于建立内存管理框架的基础。例如建立初始化页表映射、管理区等都会用到它。

Linux内存管理(4):内存映射机制


现代意义上的操作系统都处于32位保护模式下。每个进程一般都能寻址4G的内存空间。但是我们的物理内存常常没有这么大,进程怎么能获得4G的内存空间呢?这就是使用了虚拟地址的好处。我们经常在程序的反汇编代码中看到一些类似0x32118965这样的地址,操作系统中称为线性地址,或虚拟地址。通常我们使用一种叫做虚拟内存的技术来实现,因为可以使用硬盘中的一部分来当作内存使用。另外,现在操作系统都划分为系统空间和用户空间,使用虚拟地址可以很好的保护内核空间不被用户空间破坏。Linux 2.6内核使用了许多技术来改进对大量虚拟内存空间的使用,以及对内存映射的优化,使得Linux比以往任何时候都更适用于企业。包括反向映射(reverse mapping)、使用更大的内存页、页表条目存储在高端内存中,以及更稳定的管理器。    对于虚拟地址如何转为物理地址,这个转换过程有操作系统和CPU共同完成。操作系统为CPU设置好页表。CPU通过MMU单元进行地址转换。CPU做出映射的前提是操作系统要为其准备好内核页表,而对于页表的设置,内核在系统启动的初期和系统初始化完成后都分别进行了设置。
    Linux简化了分段机制,使得虚拟地址与线性地址总是一致,因此Linux的虚拟地址空间也为0~4G。Linux内核将这4G字节的空间分为两部分。将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF)供内核使用,称为“内核空间”。而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF)供各个进程使用,称为“用户空间“。因为每个进程可以通过系统调用进入内核,因此Linux内核由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G字节的虚拟空间。
    Linux使用两级保护机制:0级供内核使用,3级供用户程序使用。每个进程有各自的私有用户空间(0~3G),这个空间对系统中的其他进程是不可见的。最高的1GB字节虚拟内核空间则为所有进程以及内核所共享。内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不管是内核空间还是用户空间,它们都处于虚拟空间中。虽然内核空间占据了每个虚拟空间中的最高1GB字节,但映射到物理内存却总是从最低地址(0x00000000)开始。对内核空间来说,其地址映射是很简单的线性映射,0xC0000000就是物理地址与线性地址之间的位移量,在Linux代码中就叫做PAGE_OFFSET。
    1、与内存映射相关的宏定义

    这些宏定义在include/asm-generic/page.h中,用于定义Linux三级分页模型中的页全局目录项pgd、页中间目录项pmd、页表项pte的数据类型,以及基本的地址转换,如下:

[cpp] view plain copy
  1. #ifndef __ASM_GENERIC_PAGE_H  
  2. #define __ASM_GENERIC_PAGE_H  
  3. /* 
  4.  * 针对NOMMU体系结构的通用page.h实现,为内存管理提供虚拟定义 
  5.  */  
  6.   
  7. #ifdef CONFIG_MMU  
  8. #error need to prove a real asm/page.h  
  9. #endif  
  10.   
  11.   
  12. /* PAGE_SHIFT决定页的大小 */  
  13.   
  14. #define PAGE_SHIFT  12  
  15. #ifdef __ASSEMBLY__  
  16. /* 页大小为4KB(不使用大内存页时) */  
  17. #define PAGE_SIZE   (1 << PAGE_SHIFT)  
  18. #else  
  19. #define PAGE_SIZE   (1UL << PAGE_SHIFT)  
  20. #endif  
  21. #define PAGE_MASK   (~(PAGE_SIZE-1))  
  22.   
  23. #include <asm/setup.h>  
  24.   
  25. #ifndef __ASSEMBLY__  
  26.   
  27. #define get_user_page(vaddr)        __get_free_page(GFP_KERNEL)  
  28. #define free_user_page(page, addr)  free_page(addr)  
  29.   
  30. #define clear_page(page)    memset((page), 0, PAGE_SIZE)  
  31. #define copy_page(to,from)  memcpy((to), (from), PAGE_SIZE)  
  32.   
  33. #define clear_user_page(page, vaddr, pg)    clear_page(page)  
  34. #define copy_user_page(to, from, vaddr, pg) copy_page(to, from)  
  35.   
  36. /* 
  37.  * 使用C的类型检查.. 
  38.  */  
  39. typedef struct {  
  40.     unsigned long pte;  
  41. } pte_t;  
  42. typedef struct {  
  43.     unsigned long pmd[16];  
  44. } pmd_t;  
  45. typedef struct {  
  46.     unsigned long pgd;  
  47. } pgd_t;  
  48. typedef struct {  
  49.     unsigned long pgprot;  
  50. } pgprot_t;  
  51. typedef struct page *pgtable_t;  
  52.   
  53. /* 把x转换成对应无符号整数 */  
  54. #define pte_val(x)  ((x).pte)  
  55. #define pmd_val(x)  ((&x)->pmd[0])  
  56. #define pgd_val(x)  ((x).pgd)  
  57. #define pgprot_val(x)   ((x).pgprot)  
  58.   
  59. /* 把无符号整数转换成对应的C类型 */  
  60. #define __pte(x)    ((pte_t) { (x) } )  
  61. #define __pmd(x)    ((pmd_t) { (x) } )  
  62. #define __pgd(x)    ((pgd_t) { (x) } )  
  63. #define __pgprot(x) ((pgprot_t) { (x) } )  
  64.   
  65. /* 物理内存的起始地址和结束地址 */  
  66. extern unsigned long memory_start;  
  67. extern unsigned long memory_end;  
  68.   
  69. #endif /* !__ASSEMBLY__ */  
  70.   
  71. /* 如果内核配置了RAM的基地址,则把页偏移设为这个值,否则为0 */  
  72. #ifdef CONFIG_KERNEL_RAM_BASE_ADDRESS  
  73. #define PAGE_OFFSET     (CONFIG_KERNEL_RAM_BASE_ADDRESS)  
  74. #else  
  75. #define PAGE_OFFSET     (0)  
  76. #endif  
  77.   
  78. #ifndef __ASSEMBLY__  
  79.   
  80. /* 把物理地址x转换为线性地址(即虚拟地址) */  
  81. #define __va(x) ((void *)((unsigned long)(x) + PAGE_OFFSET))  
  82. /* 把内核空间的线性地址x转换为物理地址 */  
  83. #define __pa(x) ((unsigned long) (x) - PAGE_OFFSET)  
  84.   
  85. /* 根据内核空间的线性地址得到其物理页框号(即第几页) */  
  86. #define virt_to_pfn(kaddr)  (__pa(kaddr) >> PAGE_SHIFT)  
  87. /* 根据物理页框号得到其线性地址 */  
  88. #define pfn_to_virt(pfn)    __va((pfn) << PAGE_SHIFT)  
  89.   
  90. /* 根据用户空间的线性地址得到其物理页号 */  
  91. #define virt_to_page(addr)  (mem_map + (((unsigned long)(addr)-PAGE_OFFSET) >> PAGE_SHIFT))  
  92. /* 根据物理页号得到其用户空间的线性地址 */  
  93. #define page_to_virt(page)  ((((page) - mem_map) << PAGE_SHIFT) + PAGE_OFFSET)  
  94.   
  95. #ifndef page_to_phys  
  96. #define page_to_phys(page)      ((dma_addr_t)page_to_pfn(page) << PAGE_SHIFT)  
  97. #endif  
  98.   
  99. #define pfn_valid(pfn)      ((pfn) < max_mapnr)  
  100.   
  101. #define virt_addr_valid(kaddr)  (((void *)(kaddr) >= (void *)PAGE_OFFSET) && \  
  102.                 ((void *)(kaddr) < (void *)memory_end))  
  103.   
  104. #endif /* __ASSEMBLY__ */  
  105.   
  106. #include <asm-generic/memory_model.h>  
  107. #include <asm-generic/getorder.h>  
  108.   
  109. #endif /* __ASM_GENERIC_PAGE_H */  
    主要的定义有页移位数PAGE_SHIFT为12;页大小PAGE_SIZE为4KB(不使用大内存页时);三级映射映射模型的表项数据类型pte, pmd和pgd;内核空间的物理地址与线性地址的转换__va(x), __pa(x);线性地址与物理页框号的转换virt_to_pfn(), pfn_to_virt(), virt_to_page(), page_to_virt()。
    2、临时页表的初始化
    linux页表映射机制的建立分为两个阶段,第一个阶段是内核进入保护模式之前要先建立一个临时内核页表并开启分页功能,因为在进入保护模式后,内核继续初始化直到建立完整的内存映射机制之前,仍然需要用到页表来映射相应的内存地址。对x86 32位内核,这个工作在保护模式下的内核入口函数arch/x86/kernel/head_32.S:startup_32()中完成。第二阶段是建立完整的内存映射机制,在在setup_arch()--->arch/x86/mm/init.c:init_memory_mapping()中完成。注意对于物理地址扩展(PAE)分页机制,Intel通过在她得处理器上把管脚数从32增加到36已经满足了这些需求,寻址能力可以达到64GB。不过,只有引入一种新的分页机制把32位线性地址转换为36位物理地址才能使用所增加的物理地址。linux为对多种体系的支持,选择了一套简单的通用实现机制。在这里只分析x86 32位下的实现。
    arch/x86/kernel/head_32.S中的startup_32()相关汇编代码如下:

[cpp] view plain copy
  1. __HEAD  
  2. ENTRY(startup_32)  
  3.     /* test KEEP_SEGMENTS flag to see if the bootloader is asking 
  4.         us to not reload segments */  
  5.     testb $(1<<6), BP_loadflags(%esi)  
  6.     jnz 2f  
  7.       
  8.     /* ...... */  
  9.   
  10. /* 
  11.  * 初始化页表。这会创建一个PDE和一个页表集,存放在__brk_base的上面。 
  12.  * 变量_brk_end会被设置成指向第一个“安全”的区域。在虚拟地址0(为标识映射) 
  13.  * 和PAGE_OFFSET处会创建映射。注意在这里栈还没有被设置 
  14.  */  
  15. default_entry:  
  16. #ifdef CONFIG_X86_PAE  
  17.   
  18.     /* 
  19.      * 在PAE模式下swapper_pg_dir被静态定义包括足够多的条目以包含VMSPLIT选项(即最高的1, 
  20.      * 2或3的条目)。标识映射通过把两个PGD条目指向第一个内核PMD条目来实现 
  21.      * 注意在这一阶段,每个PMD或PTE的上半部分总是为0 
  22.      */  
  23.   
  24. #define KPMDS (((-__PAGE_OFFSET) >> 30) & 3) /* 内核PMD的数量 */  
  25.   
  26.     xorl %ebx,%ebx              /* %ebx保持为0 */  
  27.   
  28.     movl $pa(__brk_base), %edi  
  29.     movl $pa(swapper_pg_pmd), %edx  
  30.     movl $PTE_IDENT_ATTR, %eax  
  31. 10:  
  32.     leal PDE_IDENT_ATTR(%edi),%ecx      /* 创建PMD条目 */  
  33.     movl %ecx,(%edx)            /* 保存PMD条目 */  
  34.                         /* 上半部分已经为0 */  
  35.     addl $8,%edx  
  36.     movl $512,%ecx  
  37. 11:  
  38.     stosl  
  39.     xchgl %eax,%ebx  
  40.     stosl  
  41.     xchgl %eax,%ebx  
  42.     addl $0x1000,%eax  
  43.     loop 11b  
  44.   
  45.     /* 
  46.      * 终止条件:我们必须映射到end + MAPPING_BEYOND_END. 
  47.      */  
  48.     movl $pa(_end) + MAPPING_BEYOND_END + PTE_IDENT_ATTR, %ebp  
  49.     cmpl %ebp,%eax  
  50.     jb 10b  
  51. 1:  
  52.     addl $__PAGE_OFFSET, %edi  
  53.     movl %edi, pa(_brk_end)  
  54.     shrl $12, %eax  
  55.     movl %eax, pa(max_pfn_mapped)  
  56.   
  57.     /* 对fixmap区域做初期的初始化 */  
  58.     movl $pa(swapper_pg_fixmap)+PDE_IDENT_ATTR,%eax  
  59.     movl %eax,pa(swapper_pg_pmd+0x1000*KPMDS-8)  
  60. #else   /* 非PAE */  
  61.   
  62. /* 得到开始目录项的索引 */  
  63. page_pde_offset = (__PAGE_OFFSET >> 20);  
  64.     /* 将基地址__brk_base转换成物理地址,传给edi */  
  65.     movl $pa(__brk_base), %edi  
  66.     /* 将全局页目录表地址传给edx */  
  67.     movl $pa(swapper_pg_dir), %edx  
  68.     movl $PTE_IDENT_ATTR, %eax  
  69. 10:  
  70.     leal PDE_IDENT_ATTR(%edi),%ecx      /* 创建PDE条目 */  
  71.     movl %ecx,(%edx)            /* 保存标识PDE条目 */  
  72.     movl %ecx,page_pde_offset(%edx)     /* 保存内核PDE条目 */  
  73.     addl $4,%edx  
  74.     movl $1024, %ecx  
  75. 11:  
  76.     stosl  
  77.     addl $0x1000,%eax  
  78.     loop 11b  
  79.     /* 
  80.      * 终止条件:我们必须映射到end + MAPPING_BEYOND_END. 
  81.      */  
  82.     movl $pa(_end) + MAPPING_BEYOND_END + PTE_IDENT_ATTR, %ebp  
  83.     cmpl %ebp,%eax  
  84.     jb 10b  
  85.     addl $__PAGE_OFFSET, %edi  
  86.     movl %edi, pa(_brk_end)  
  87.     shrl $12, %eax  
  88.     movl %eax, pa(max_pfn_mapped)  
  89.   
  90.     /* 对fixmap区域做初期的初始化 */  
  91.     movl $pa(swapper_pg_fixmap)+PDE_IDENT_ATTR,%eax  
  92.     movl %eax,pa(swapper_pg_dir+0xffc)  
  93. #endif  
  94.     jmp 3f  
  95. /* 
  96.  * Non-boot CPU entry point; entered from trampoline.S 
  97.  * We can't lgdt here, because lgdt itself uses a data segment, but 
  98.  * we know the trampoline has already loaded the boot_gdt for us. 
  99.  * 
  100.  * If cpu hotplug is not supported then this code can go in init section 
  101.  * which will be freed later 
  102.  */  
  103.   
  104. __CPUINIT  
  105.   
  106. #ifdef CONFIG_SMP  
  107. ENTRY(startup_32_smp)  
  108.     cld  
  109.     movl $(__BOOT_DS),%eax  
  110.     movl %eax,%ds  
  111.     movl %eax,%es  
  112.     movl %eax,%fs  
  113.     movl %eax,%gs  
  114. #endif /* CONFIG_SMP */  
  115. 3:  
  116.   
  117. /* 
  118.  *  New page tables may be in 4Mbyte page mode and may 
  119.  *  be using the global pages.  
  120.  * 
  121.  *  NOTE! If we are on a 486 we may have no cr4 at all! 
  122.  *  So we do not try to touch it unless we really have 
  123.  *  some bits in it to set.  This won't work if the BSP 
  124.  *  implements cr4 but this AP does not -- very unlikely 
  125.  *  but be warned!  The same applies to the pse feature 
  126.  *  if not equally supported. --macro 
  127.  * 
  128.  *  NOTE! We have to correct for the fact that we're 
  129.  *  not yet offset PAGE_OFFSET.. 
  130.  */  
  131. #define cr4_bits pa(mmu_cr4_features)  
  132.     movl cr4_bits,%edx  
  133.     andl %edx,%edx  
  134.     jz 6f  
  135.     movl %cr4,%eax      # 打开分页选项(PSE,PAE,...)  
  136.     orl %edx,%eax  
  137.     movl %eax,%cr4  
  138.   
  139.     btl $5, %eax        # 检查PAE是否开启  
  140.     jnc 6f  
  141.   
  142.     /* 检查扩展函数功能是否实现 */  
  143.     movl $0x80000000, %eax  
  144.     cpuid  
  145.     cmpl $0x80000000, %eax  
  146.     jbe 6f  
  147.     mov $0x80000001, %eax  
  148.     cpuid  
  149.     /* Execute Disable bit supported? */  
  150.     btl $20, %edx  
  151.     jnc 6f  
  152.   
  153.     /* 设置EFER (Extended Feature Enable Register) */  
  154.     movl $0xc0000080, %ecx  
  155.     rdmsr  
  156.   
  157.     btsl $11, %eax  
  158.     /* 使更改生效 */  
  159.     wrmsr  
  160.   
  161. 6:  
  162.   
  163. /* 
  164.  * 开启分页功能 
  165.  */  
  166.     movl pa(initial_page_table), %eax  
  167.     movl %eax,%cr3      /* 设置页表指针:cr3控制寄存器保存的是目录表地址 */  
  168.     movl %cr0,%eax  
  169.     orl  $X86_CR0_PG,%eax  
  170.     movl %eax,%cr0      /* ..同时设置分页(PG)标识位 */  
  171.     ljmp $__BOOT_CS,$1f /* 清除预读取和规格化%eip */  
  172. 1:  
  173.     /* 设置栈指针 */  
  174.     lss stack_start,%esp  
  175.   
  176. /* 
  177.  * Initialize eflags.  Some BIOS's leave bits like NT set.  This would 
  178.  * confuse the debugger if this code is traced. 
  179.  * XXX - best to initialize before switching to protected mode. 
  180.  */  
  181.     pushl $0  
  182.     popfl  
  183.   
  184. #ifdef CONFIG_SMP  
  185.     cmpb $0, ready  
  186.     jz  1f              /* 初始的CPU要清除BSS */  
  187.     jmp checkCPUtype  
  188. 1:  
  189. #endif /* CONFIG_SMP */  
    其中PTE_IDENT_ATTR等常量定义在arch/x86/include/asm/pgtable_types.h中,如下:
[cpp] view plain copy
  1. /* 
  2.  * 初期标识映射的pte属性宏 
  3.  */  
  4. #ifdef CONFIG_X86_64  
  5. #define __PAGE_KERNEL_IDENT_LARGE_EXEC  __PAGE_KERNEL_LARGE_EXEC  
  6. #else  
  7. /* 
  8.  * For PDE_IDENT_ATTR include USER bit. As the PDE and PTE protection 
  9.  * bits are combined, this will alow user to access the high address mapped 
  10.  * VDSO in the presence of CONFIG_COMPAT_VDSO 
  11.  */  
  12. #define PTE_IDENT_ATTR   0x003      /* PRESENT+RW */  
  13. #define PDE_IDENT_ATTR   0x067      /* PRESENT+RW+USER+DIRTY+ACCESSED */  
  14. #define PGD_IDENT_ATTR   0x001      /* PRESENT (no other attributes) */  
  15. #endif  
    分析(其中的非PAE模式):
    (1)swapper_pg_dir是临时全局页目录表起址,它是在内核编译过程中静态初始化的。首先 page_pde_offset得到开始目录项的索引。从这可以看出内核是在swapper_pg_dir的第768个表项开始建立页表。其对应线性地址就是__brk_base(内核编译时指定其值,默认为0xc0000000)以上的地址,即3GB以上的高端地址(3GB-4GB),再次强调这高端的1GB线性空间是内核占据的虚拟空间,在进行实际内存映射时,映射到物理内存却总是从最低地址(0x00000000)开始。
    (2)将目录表的地址swapper_pg_dir传给edx,表明内核也要从__brk_base开始建立页表,这样可以保证从以物理地址取指令到以线性地址在系统空间取指令的平稳过渡。
    (3)创建并保存PDE条目。
    (4)终止条件end + MAPPING_BEYOND_END决定了内核到底要建立多少页表,也就是要映射多少内存空间。在内核初始化程中内核只要保证能映射到包括内核的代码段,数据段,初始页表和用于存放动态数据结构的128k大小的空间就行。在这段代码中,内核为什么要把用户空间和内核空间的前几个目录项映射到相同的页表中去呢?虽然在head_32.S中内核已经进入保护模式,但是内核现在是处于保护模式的段式寻址方式下,因为内核还没有启用分页映射机制,现在都是以物理地址来取指令,如果代码中遇到了符号地址,只能减去0xc0000000才行,当开启了映射机制后就不用了。现在cpu中的取指令指针eip仍指向低区,如果只建立内核空间中的映射,那么当内核开启映射机制后,低区中的地址就没办法寻址了,因为没有对应的页表,除非遇到某个符号地址作为绝对转移或调用子程序为止。因此要尽快开启CPU的页式映射机制。
    (5)开启CPU页式映射机制:initial_page_table表示目录表起址,传到eax中,然后保存到cr3控制寄存器中(从而前面“内存模型”介绍中可知cr3保存页目录表起址)。把cr0的最高位置成1来开启映射机制(即设置PG位)。
    通过ljmp $__BOOT_CS,$1f这条指令使CPU进入了系统空间继续执行,因为__BOOT_CS是个符号地址,地址在0xc0000000以上。在head_32.S完成了内核临时页表的建立后,它继续进行初始化,包括初始化INIT_TASK,也就是系统开启后的第一个进程;建立完整的中断处理程序,然后重新加载GDT描述符,最后跳转到init/main.c中的start_kernel()函数继续初始化。
    3、内存映射机制的完整建立
    根据前面介绍,这一阶段在start_kernel()--->setup_arch()中完成。在Linux中,物理内存被分为低端内存区和高端内存区(如果内核编译时配置了高端内存标志的话),为了建立物理内存到虚拟地址空间的映射,需要先计算出物理内存总共有多少页面数,即找出最大可用页框号,这包含了整个低端和高端内存区。还要计算出低端内存区总共占多少页面。

    在setup_arch(),首先调用arch/x86/kernel/e820.c:e820_end_of_ram_pfn()找出最大可用页帧号(即总页面数),并保存在全局变量max_pfn中,这个变量定义可以在mm/bootmem.c中找到。它直接调用e820.c中的e820_end_pfn()完成工作。如下:

[cpp] view plain copy
  1. #ifdef CONFIG_X86_32  
  2. # ifdef CONFIG_X86_PAE  
  3. #  define MAX_ARCH_PFN      (1ULL<<(36-PAGE_SHIFT))  
  4. # else  
  5. #  define MAX_ARCH_PFN      (1ULL<<(32-PAGE_SHIFT))  
  6. # endif  
  7. #else /* CONFIG_X86_32 */  
  8. # define MAX_ARCH_PFN MAXMEM>>PAGE_SHIFT  
  9. #endif  
  10.   
  11. /* 
  12.  * 找出最大可用页帧号 
  13.  */  
  14. static unsigned long __init e820_end_pfn(unsigned long limit_pfn, unsigned type)  
  15. {  
  16.     int i;  
  17.     unsigned long last_pfn = 0;  
  18.     unsigned long max_arch_pfn = MAX_ARCH_PFN; /* 4G地址空间对应的页面数 */  
  19.     /* 对e820中所有的内存块,其中e820为从bios中探测到的页面数存放处 */  
  20.     for (i = 0; i < e820.nr_map; i++) {  
  21.         struct e820entry *ei = &e820.map[i]; /* 第i个物理页面块 */  
  22.         unsigned long start_pfn;  
  23.         unsigned long end_pfn;  
  24.   
  25.         if (ei->type != type) /* 与要找的类型不匹配 */  
  26.             continue;  
  27.         /* 起始地址和结束地址对应的页面帧号 */  
  28.         start_pfn = ei->addr >> PAGE_SHIFT;  
  29.         end_pfn = (ei->addr + ei->size) >> PAGE_SHIFT;  
  30.   
  31.         if (start_pfn >= limit_pfn)  
  32.             continue;  
  33.         if (end_pfn > limit_pfn) {  
  34.             /* 找到的结束页面帧号大于上限值时 */  
  35.             last_pfn = limit_pfn;  
  36.             break;  
  37.         }  
  38.         if (end_pfn > last_pfn) /* 保存更新last_pfn */  
  39.             last_pfn = end_pfn;  
  40.     }  
  41.     /* 大于4G空间时 */  
  42.     if (last_pfn > max_arch_pfn)  
  43.         last_pfn = max_arch_pfn;  
  44.     /* 打印输出信息 */  
  45.     printk(KERN_INFO "last_pfn = %#lx max_arch_pfn = %#lx\n",  
  46.              last_pfn, max_arch_pfn);  
  47.     /* 返回最后一个页面帧号 */  
  48.     return last_pfn;  
  49. }  
  50. unsigned long __init e820_end_of_ram_pfn(void)  
  51. {  
  52.     /* MAX_ARCH_PFN为4G空间 */  
  53.     return e820_end_pfn(MAX_ARCH_PFN, E820_RAM);  
  54. }  
    这里MAX_ARCH_PFN为通常可寻址的4GB空间,如果启用了PAE扩展,则为64GB空间。e820_end_of_ram_pfn()直接调用e820_end_pfn()找出最大可用页面帧号,它会遍历e820.map数组中存放的所有物理页面块,找出其中最大的页面帧号,这就是我们当前需要的max_pfn值。
    然后,setup_arch()会调用arch/x86/mm/init_32.c:find_low_pfn_range()找出低端内存区的最大可用页帧号,保存在全局变量max_low_pfn中(也定义在mm/bootmem.c中)。如下:
[cpp] view plain copy
  1. static unsigned int highmem_pages = -1;  
  2.   
  3. /* ...... */  
  4.   
  5. /* 
  6.  * 全部物理内存都在包含在低端空间中 
  7.  */  
  8. void __init lowmem_pfn_init(void)  
  9. {  
  10.     /* max_low_pfn is 0, we already have early_res support */  
  11.     max_low_pfn = max_pfn;  
  12.   
  13.     if (highmem_pages == -1)  
  14.         highmem_pages = 0;  
  15. #ifdef CONFIG_HIGHMEM  
  16.     if (highmem_pages >= max_pfn) {  
  17.         printk(KERN_ERR MSG_HIGHMEM_TOO_BIG,  
  18.             pages_to_mb(highmem_pages), pages_to_mb(max_pfn));  
  19.         highmem_pages = 0;  
  20.     }  
  21.     if (highmem_pages) {  
  22.         if (max_low_pfn - highmem_pages < 64*1024*1024/PAGE_SIZE) {  
  23.             printk(KERN_ERR MSG_LOWMEM_TOO_SMALL,  
  24.                 pages_to_mb(highmem_pages));  
  25.             highmem_pages = 0;  
  26.         }  
  27.         max_low_pfn -= highmem_pages;  
  28.     }  
  29. #else  
  30.     if (highmem_pages)  
  31.         printk(KERN_ERR "ignoring highmem size on non-highmem kernel!\n");  
  32. #endif  
  33. }  
  34.   
  35. #define MSG_HIGHMEM_TOO_SMALL \  
  36.     "only %luMB highmem pages available, ignoring highmem size of %luMB!\n"  
  37.   
  38. #define MSG_HIGHMEM_TRIMMED \  
  39.     "Warning: only 4GB will be used. Use a HIGHMEM64G enabled kernel!\n"  
  40. /* 
  41.  * 物理内存超出低端空间区:把它们放在高端地址空间中,或者通过启动时的highmem=x启动参数进行配置;  
  42.  * 如果不配置,在这里进行设置大小 
  43.  */  
  44. void __init highmem_pfn_init(void)  
  45. {  
  46.     /* MAXMEM_PFN为最大物理地址-(4M+4M+8K+128M);  
  47.     所以低端空间的大小其实比我们说的896M低一些 */  
  48.     max_low_pfn = MAXMEM_PFN;  
  49.   
  50.     if (highmem_pages == -1) /* 高端内存页面数如果在开机没有设置 */  
  51.         highmem_pages = max_pfn - MAXMEM_PFN; /* 总页面数减去低端页面数 */   
  52.     /* 如果highmem_pages变量在启动项设置了,那么在这里就要进行这样的判断, 
  53.     因为可能出现不一致的情况 */  
  54.     if (highmem_pages + MAXMEM_PFN < max_pfn)  
  55.         max_pfn = MAXMEM_PFN + highmem_pages;  
  56.   
  57.     if (highmem_pages + MAXMEM_PFN > max_pfn) {  
  58.         printk(KERN_WARNING MSG_HIGHMEM_TOO_SMALL,  
  59.             pages_to_mb(max_pfn - MAXMEM_PFN),  
  60.             pages_to_mb(highmem_pages));  
  61.         highmem_pages = 0;  
  62.     }  
  63. #ifndef CONFIG_HIGHMEM  
  64.     /* 最大可用内存是可直接寻址的 */  
  65.     printk(KERN_WARNING "Warning only %ldMB will be used.\n", MAXMEM>>20);  
  66.     if (max_pfn > MAX_NONPAE_PFN)  
  67.         printk(KERN_WARNING "Use a HIGHMEM64G enabled kernel.\n");  
  68.     else  
  69.         printk(KERN_WARNING "Use a HIGHMEM enabled kernel.\n");  
  70.     max_pfn = MAXMEM_PFN;  
  71. #else /* !CONFIG_HIGHMEM */  
  72. #ifndef CONFIG_HIGHMEM64G  
  73.     /* 在没有配置64G的情况下,内存的大小不能超过4G */  
  74.     if (max_pfn > MAX_NONPAE_PFN) {  
  75.         max_pfn = MAX_NONPAE_PFN;  
  76.         printk(KERN_WARNING MSG_HIGHMEM_TRIMMED);  
  77.     }  
  78. #endif /* !CONFIG_HIGHMEM64G */  
  79. #endif /* !CONFIG_HIGHMEM */  
  80. }  
  81.   
  82. /* 
  83.  * 确定低端和高端内存的页面帧号范围: 
  84.  */  
  85. void __init find_low_pfn_range(void)  
  86. {  
  87.     /* 会更新max_pfn */  
  88.   
  89.     /* 当物理内存本来就小于低端空间最大页框数时, 
  90.       直接没有高端地址映射 */  
  91.     if (max_pfn <= MAXMEM_PFN)  
  92.         lowmem_pfn_init();  
  93.     else /* 这是一般PC机的运行流程,存在高端映射 */  
  94.         highmem_pfn_init();  
  95. }  
    分析:
    (1)init_32.c中定义了一个静态全局变量highmem_pages,用来保存用户指定的高端空间的大小(即总页面数)。
    (2)在find_low_pfn_range()中,如果物理内存总页面数max_pfn不大于低端页面数上限MAXMEM_PFN(即物理内存大小没有超出低端空间范围),则直接没有高端地址映射,调用lowmem_pfn_init(),将max_low_pfn设成max_pfn。注意若内核编译时通过CONFIG_HIGHMEM指定必须有高端映射,则max_low_pfn的值需要减去高端页面数highmem_pages,以表示低端页面数。
    (3)如果物理内存总页面数大于低端页面数上限,则表明有高端映射,因为需要把超出的部分放在高端空间区,这是一般PC机的运行流程。调用highmem_pfn_init(),如果启动时用户没有指定高端页面数,则显然max_low_pfn=MAXMEM_PFN,highmem_pages = max_pfn - MAXMEM_PFN;如果启动时用户通过highmem=x启动参数指定了高端页面数highmem_pages,则仍然有max_low_pfn=MAXMEM_PFN,但max_pfn可能出现不一致的情况,需要更新为MAXMEM_PFN + highmem_pages,如果出现越界(高端空间区太小),则要做相应越界处理。
    有了总页面数、低端页面数、高端页面数这些信息,setup_arch()接着调用arch/x86/mm/init.c:init_memory_mapping(0, max_low_pfn<<PAGE_SHIFT)函数建立完整的内存映射机制。该函数在PAGE_OFFSET处建立物理内存的直接映射,即把物理内存中0~max_low_pfn<<12地址范围的低端空间区直接映射到内核虚拟空间(它是从PAGE_OFFSET即0xc0000000开始的1GB线性地址)。这在bootmem初始化之前运行,并且直接从物理内存获取页面,这些页面在前面已经被临时映射了。注意高端映射区并没有映射到实际的物理页面,只是这种机制的初步建立,页表存储的空间保留。代码如下:
[cpp] view plain copy
  1. unsigned long __init_refok init_memory_mapping(unsigned long start,  
  2.                            unsigned long end)  
  3. {  
  4.     unsigned long page_size_mask = 0;  
  5.     unsigned long start_pfn, end_pfn;  
  6.     unsigned long ret = 0;  
  7.     unsigned long pos;  
  8.   
  9.     struct map_range mr[NR_RANGE_MR];  
  10.     int nr_range, i;  
  11.     int use_pse, use_gbpages;  
  12.   
  13.     printk(KERN_INFO "init_memory_mapping: %016lx-%016lx\n", start, end);  
  14.   
  15. #if defined(CONFIG_DEBUG_PAGEALLOC) || defined(CONFIG_KMEMCHECK)  
  16.     /* 
  17.      * For CONFIG_DEBUG_PAGEALLOC, identity mapping will use small pages. 
  18.      * This will simplify cpa(), which otherwise needs to support splitting 
  19.      * large pages into small in interrupt context, etc. 
  20.      */  
  21.     use_pse = use_gbpages = 0;  
  22. #else  
  23.     use_pse = cpu_has_pse;  
  24.     use_gbpages = direct_gbpages;  
  25. #endif  
  26.     /* 定义了X86_PAE模式后进行调用 */  
  27.     set_nx();  
  28.     if (nx_enabled)  
  29.         printk(KERN_INFO "NX (Execute Disable) protection: active\n");  
  30.   
  31.     /* 激活PSE(如果可用) */  
  32.     if (cpu_has_pse)  
  33.         set_in_cr4(X86_CR4_PSE);  
  34.   
  35.     /* 激活PGE(如果可用) */  
  36.     if (cpu_has_pge) {  
  37.         set_in_cr4(X86_CR4_PGE);  
  38.         __supported_pte_mask |= _PAGE_GLOBAL;  
  39.     }  
  40.     /* page_size_mask在这里更新,在后面设置页表时用到 */  
  41.     if (use_gbpages)  
  42.         page_size_mask |= 1 << PG_LEVEL_1G;  
  43.     if (use_pse)  
  44.         page_size_mask |= 1 << PG_LEVEL_2M;  
  45.   
  46.     memset(mr, 0, sizeof(mr));  
  47.     nr_range = 0;  
  48.   
  49.     /* 作为初始页面帧号值,如果没有大内存页对齐 */  
  50.     start_pfn = start >> PAGE_SHIFT; /* 在setup函数中调用时,这里为0 */  
  51.     pos = start_pfn << PAGE_SHIFT; /* pos为0 */  
  52. #ifdef CONFIG_X86_32  
  53.     /* 
  54.      * Don't use a large page for the first 2/4MB of memory 
  55.      * because there are often fixed size MTRRs in there 
  56.      * and overlapping MTRRs into large pages can cause 
  57.      * slowdowns. 
  58.      */  
  59.     if (pos == 0) /* end_pfn的大小为1k,也就是4M大小的内存 */  
  60.         end_pfn = 1<<(PMD_SHIFT - PAGE_SHIFT);  
  61.     else  
  62.         end_pfn = ((pos + (PMD_SIZE - 1))>>PMD_SHIFT)  
  63.                  << (PMD_SHIFT - PAGE_SHIFT);  
  64. #else /* CONFIG_X86_64 */  
  65.     end_pfn = ((pos + (PMD_SIZE - 1)) >> PMD_SHIFT)  
  66.             << (PMD_SHIFT - PAGE_SHIFT);  
  67. #endif  
  68.     if (end_pfn > (end >> PAGE_SHIFT))  
  69.         end_pfn = end >> PAGE_SHIFT;  
  70.     if (start_pfn < end_pfn) { /* 4M空间将这个区间存放在mr数组中 */  
  71.         nr_range = save_mr(mr, nr_range, start_pfn, end_pfn, 0);  
  72.         pos = end_pfn << PAGE_SHIFT;  
  73.     }  
  74.   
  75.     /* 大内存页(2M)范围:对齐到PMD,换算成页面的多少 */  
  76.     start_pfn = ((pos + (PMD_SIZE - 1))>>PMD_SHIFT)  
  77.              << (PMD_SHIFT - PAGE_SHIFT);  
  78. #ifdef CONFIG_X86_32  
  79.     /* 这里的结束地址设置为调用的结束位页面数,也就是  
  80.       所有的物理页面数 */  
  81.     end_pfn = (end>>PMD_SHIFT) << (PMD_SHIFT - PAGE_SHIFT);  
  82. #else /* CONFIG_X86_64 */  
  83.     end_pfn = ((pos + (PUD_SIZE - 1))>>PUD_SHIFT)  
  84.              << (PUD_SHIFT - PAGE_SHIFT);  
  85.     if (end_pfn > ((end>>PMD_SHIFT)<<(PMD_SHIFT - PAGE_SHIFT)))  
  86.         end_pfn = ((end>>PMD_SHIFT)<<(PMD_SHIFT - PAGE_SHIFT));  
  87. #endif  
  88.   
  89.     if (start_pfn < end_pfn) {  
  90.         /* 将这段内存放入mr中,保存后面用到 */  
  91.         nr_range = save_mr(mr, nr_range, start_pfn, end_pfn,  
  92.                 page_size_mask & (1<<PG_LEVEL_2M)); /* 这里保证了运用PSE时为2M页面而不是PSE时,  
  93.                 仍然为4K页面(上面的按位或和这里的按位与) */  
  94.         pos = end_pfn << PAGE_SHIFT; /* 更新pos */  
  95.     }  
  96.   
  97. #ifdef CONFIG_X86_64  
  98.     /* 大内存页(1G)范围 */  
  99.     start_pfn = ((pos + (PUD_SIZE - 1))>>PUD_SHIFT)  
  100.              << (PUD_SHIFT - PAGE_SHIFT);  
  101.     end_pfn = (end >> PUD_SHIFT) << (PUD_SHIFT - PAGE_SHIFT);  
  102.     if (start_pfn < end_pfn) {  
  103.         nr_range = save_mr(mr, nr_range, start_pfn, end_pfn,  
  104.                 page_size_mask &  
  105.                  ((1<<PG_LEVEL_2M)|(1<<PG_LEVEL_1G)));  
  106.         pos = end_pfn << PAGE_SHIFT;  
  107.     }  
  108.   
  109.     /* 尾部不是大内存页(1G)对齐 */  
  110.     start_pfn = ((pos + (PMD_SIZE - 1))>>PMD_SHIFT)  
  111.              << (PMD_SHIFT - PAGE_SHIFT);  
  112.     end_pfn = (end >> PMD_SHIFT) << (PMD_SHIFT - PAGE_SHIFT);  
  113.     if (start_pfn < end_pfn) {  
  114.         nr_range = save_mr(mr, nr_range, start_pfn, end_pfn,  
  115.                 page_size_mask & (1<<PG_LEVEL_2M));  
  116.         pos = end_pfn << PAGE_SHIFT;  
  117.     }  
  118. #endif  
  119.   
  120.     /* 尾部不是大内存页(2M)对齐 */  
  121.     start_pfn = pos>>PAGE_SHIFT;  
  122.     end_pfn = end>>PAGE_SHIFT;  
  123.     nr_range = save_mr(mr, nr_range, start_pfn, end_pfn, 0);  
  124.   
  125.     /* 合并相同页面大小的连续的页面 */  
  126.     for (i = 0; nr_range > 1 && i < nr_range - 1; i++) {  
  127.         unsigned long old_start;  
  128.         if (mr[i].end != mr[i+1].start ||  
  129.             mr[i].page_size_mask != mr[i+1].page_size_mask)  
  130.             continue;  
  131.         /* move it */  
  132.         old_start = mr[i].start;  
  133.         memmove(&mr[i], &mr[i+1],  
  134.             (nr_range - 1 - i) * sizeof(struct map_range));  
  135.         mr[i--].start = old_start;  
  136.         nr_range--;  
  137.     }  
  138.     /* 打印相关信息 */  
  139.     for (i = 0; i < nr_range; i++)  
  140.         printk(KERN_DEBUG " %010lx - %010lx page %s\n",  
  141.                 mr[i].start, mr[i].end,  
  142.             (mr[i].page_size_mask & (1<<PG_LEVEL_1G))?"1G":(  
  143.              (mr[i].page_size_mask & (1<<PG_LEVEL_2M))?"2M":"4k"));  
  144.   
  145.     /* 
  146.      * 为内核直接映射的页表查找空间 
  147.      * 以后我们应该在内存映射的本地节点分配这些页表。不幸的是目前这需要在 
  148.      * 查找到节点之前来做 
  149.      */  
  150.     if (!after_bootmem)  /*如果内存启动分配器没有建立,则直接从e820.map中找到合适的 
  151.          连续内存,找到存放页表的空间首地址为e820_table_start */    
  152.         find_early_table_space(end, use_pse, use_gbpages);  
  153.   
  154. #ifdef CONFIG_X86_32  
  155.     for (i = 0; i < nr_range; i++) /* 对每个保存的区域设置页表映射 */  
  156.         kernel_physical_mapping_init(mr[i].start, mr[i].end,  
  157.                          mr[i].page_size_mask);  
  158.     ret = end;  
  159. #else /* CONFIG_X86_64 */  
  160.     for (i = 0; i < nr_range; i++)  
  161.         ret = kernel_physical_mapping_init(mr[i].start, mr[i].end,  
  162.                            mr[i].page_size_mask);  
  163. #endif  
  164.   
  165. #ifdef CONFIG_X86_32  
  166.     /* 对高端内存固定区域建立映射 */  
  167.     early_ioremap_page_table_range_init();  
  168.     /* 放入CR3寄存器 */  
  169.     load_cr3(swapper_pg_dir);  
  170. #endif  
  171.   
  172. #ifdef CONFIG_X86_64  
  173.     if (!after_bootmem && !start) {  
  174.         pud_t *pud;  
  175.         pmd_t *pmd;  
  176.   
  177.         mmu_cr4_features = read_cr4();  
  178.   
  179.         /* 
  180.          * _brk_end cannot change anymore, but it and _end may be 
  181.          * located on different 2M pages. cleanup_highmap(), however, 
  182.          * can only consider _end when it runs, so destroy any 
  183.          * mappings beyond _brk_end here. 
  184.          */  
  185.         pud = pud_offset(pgd_offset_k(_brk_end), _brk_end);  
  186.         pmd = pmd_offset(pud, _brk_end - 1);  
  187.         while (++pmd <= pmd_offset(pud, (unsigned long)_end - 1))  
  188.             pmd_clear(pmd);  
  189.     }  
  190. #endif  
  191.     __flush_tlb_all(); /* 刷新寄存器 */  
  192.     /* 将分配给建立页表机制的内存空间保留 */  
  193.     if (!after_bootmem && e820_table_end > e820_table_start)  
  194.         reserve_early(e820_table_start << PAGE_SHIFT,  
  195.                  e820_table_end << PAGE_SHIFT, "PGTABLE");  
  196.   
  197.     if (!after_bootmem)  
  198.         early_memtest(start, end);  
  199.   
  200.     return ret >> PAGE_SHIFT;  
  201. }  
    分析:
    (1)激活PSE和PGE,如果它们可用的话。更新page_size_mask掩码,这会在后面设置页表时用到。这个掩码可以用来区分使用的内存页大小,普通内存页为2KB,大内存页为4MB,启用了物理地址扩展(PAE)的系统上是2MB。
    (2)根据传进来的地址范围计算起始页面帧号start_pfn和终止页面帧号end_pfn,调用save_mr()将这段页面范围保存到mr数组中,并更新pos,后面会用到。这里mr是由map_range结构构成的结构体数组,map_range结构封装了一个映射范围。
    (3)遍历mr数组,合并相同页面大小的连接页面。
    (4)调用find_early_table_space()为内核空间直接映射的页表查找可用的空间。然后对mr中的每个物理页面区域,调用核心函数kernel_physical_mapping_init()设置页表映射,以将它映射到内核空间。
    (5)调用early_ioremap_page_table_range_init()对高端内存区建立页表映射,并把临时页表基址swapper_pg_dir加载到CR3寄存器中。
    (6)因为将基址放到了CR3寄存器中,所以要调用__flush_tlb_all()对其寄存器刷新,以表示将内容放到内存中。然后,调用reserve_early()将分配给建立页表机制的内存空间保留。
    map_range结构、save_mr(),以及find_early_table_space()的实现也都在arch/x86/mm/init.c中,如下:
[cpp] view plain copy
  1. unsigned long __initdata e820_table_start;  
  2. unsigned long __meminitdata e820_table_end;  
  3. unsigned long __meminitdata e820_table_top;  
  4.   
  5. int after_bootmem;  
  6.   
  7. int direct_gbpages  
  8. #ifdef CONFIG_DIRECT_GBPAGES  
  9.                 = 1  
  10. #endif  
  11. ;  
  12.   
  13. /* 查找页表需要的空间 */  
  14. static void __init find_early_table_space(unsigned long end, int use_pse,  
  15.                       int use_gbpages)  
  16. {  
  17.     unsigned long puds, pmds, ptes, tables, start;  
  18.     /* 计算需要用到多少pud,当没有pud存在的情况下pud=pgd */  
  19.     puds = (end + PUD_SIZE - 1) >> PUD_SHIFT;  
  20.     tables = roundup(puds * sizeof(pud_t), PAGE_SIZE);  
  21.   
  22.     if (use_gbpages) {  
  23.         unsigned long extra;  
  24.   
  25.         extra = end - ((end>>PUD_SHIFT) << PUD_SHIFT);  
  26.         pmds = (extra + PMD_SIZE - 1) >> PMD_SHIFT;  
  27.     } else  
  28.         pmds = (end + PMD_SIZE - 1) >> PMD_SHIFT;  
  29.     /* 计算映射所有内存所要求的所有pmd的个数 */  
  30.     tables += roundup(pmds * sizeof(pmd_t), PAGE_SIZE);  
  31.   
  32.     if (use_pse) {  
  33.         unsigned long extra;  
  34.   
  35.         extra = end - ((end>>PMD_SHIFT) << PMD_SHIFT);  
  36. #ifdef CONFIG_X86_32  
  37.         extra += PMD_SIZE;  
  38. #endif  
  39.         ptes = (extra + PAGE_SIZE - 1) >> PAGE_SHIFT;  
  40.     } else /* 计算所需要的pte个数 */  
  41.         ptes = (end + PAGE_SIZE - 1) >> PAGE_SHIFT;  
  42.   
  43.     tables += roundup(ptes * sizeof(pte_t), PAGE_SIZE);  
  44.   
  45. #ifdef CONFIG_X86_32  
  46.     /* for fixmap */  
  47.     /* 加上固定内存映射区的页表数量 */  
  48.     tables += roundup(__end_of_fixed_addresses * sizeof(pte_t), PAGE_SIZE);  
  49. #endif  
  50.   
  51.     /* 
  52.      * RED-PEN putting page tables only on node 0 could 
  53.      * cause a hotspot and fill up ZONE_DMA. The page tables 
  54.      * need roughly 0.5KB per GB. 
  55.      */  
  56. #ifdef CONFIG_X86_32  
  57.     start = 0x7000; /* 页表存放的开始地址,这里为什么从这里开始? */  
  58. #else  
  59.     start = 0x8000;  
  60. #endif  
  61.     /* 从e820.map中找到连续的足够大小的内存来存放用于映射的页表,  
  62.      返回起始地址 */  
  63.     e820_table_start = find_e820_area(start, max_pfn_mapped<<PAGE_SHIFT,  
  64.                     tables, PAGE_SIZE);  
  65.     if (e820_table_start == -1UL)  
  66.         panic("Cannot find space for the kernel page tables");  
  67.     /* 将页表起始地址的物理页面帧号保存到相关的全局变量中 */  
  68.     e820_table_start >>= PAGE_SHIFT;  
  69.     e820_table_end = e820_table_start;  
  70.     e820_table_top = e820_table_start + (tables >> PAGE_SHIFT);  
  71.   
  72.     printk(KERN_DEBUG "kernel direct mapping tables up to %lx @ %lx-%lx\n",  
  73.         end, e820_table_start << PAGE_SHIFT, e820_table_top << PAGE_SHIFT);  
  74. }  
  75.   
  76. struct map_range {  
  77.     unsigned long start;  
  78.     unsigned long end;  
  79.     unsigned page_size_mask;  
  80. };  
  81.   
  82. #ifdef CONFIG_X86_32  
  83. #define NR_RANGE_MR 3  
  84. #else /* CONFIG_X86_64 */  
  85. #define NR_RANGE_MR 5  
  86. #endif  
  87. /* 将要映射的页面范围保存到mr数组中 */  
  88. static int __meminit save_mr(struct map_range *mr, int nr_range,  
  89.                  unsigned long start_pfn, unsigned long end_pfn,  
  90.                  unsigned long page_size_mask)  
  91. {  
  92.     if (start_pfn < end_pfn) {  
  93.         if (nr_range >= NR_RANGE_MR)  
  94.             panic("run out of range for init_memory_mapping\n");  
  95.         mr[nr_range].start = start_pfn<<PAGE_SHIFT;  
  96.         mr[nr_range].end   = end_pfn<<PAGE_SHIFT;  
  97.         mr[nr_range].page_size_mask = page_size_mask;  
  98.         nr_range++;  
  99.     }  
  100.   
  101.     return nr_range;  
  102. }  
    分析:
    (1)save_mr()将要映射的页面范围start_pfn~end_pfn保存到数组mr的一个元素中去。
    (2)find_early_table_space()先计算映射所需的pud, pmd, pte个数,对32位系统,页表存放的起始地址为0x7000。然后,调用find_e820_area()从e820.map中找到连续的足够大小的内存来存放用于映射的页表,并将页表起始地址的物理页面帧号保存到相关的全局变量中。
    4、内核空间映射kernel_physical_mapping_init()分析
    对32位系统,该函数在arch/x86/mm/init_32.c中。它把低端区的所有max_low_pfn个物理内存页面映射到内核虚拟地址空间,映射页表从内核空间的起始地址处开始创建,即从PAGE_OFFSET(0xc0000000)开始的整个内核空间,直到物理内存映射完毕。理解了这个函数,就能大概理解内核是如何建立页表的,从而完整地弄清这个抽象模型。如下:
[cpp] view plain copy
  1. unsigned long __init  
  2. kernel_physical_mapping_init(unsigned long start,  
  3.                  unsigned long end,  
  4.                  unsigned long page_size_mask)  
  5. {  
  6.     int use_pse = page_size_mask == (1<<PG_LEVEL_2M);  
  7.     unsigned long start_pfn, end_pfn;  
  8.     pgd_t *pgd_base = swapper_pg_dir;  
  9.     int pgd_idx, pmd_idx, pte_ofs;  
  10.     unsigned long pfn;  
  11.     pgd_t *pgd;  
  12.     pmd_t *pmd;  
  13.     pte_t *pte;  
  14.     unsigned pages_2m, pages_4k;  
  15.     int mapping_iter;  
  16.     /* 得到要映射的起始地址和终止地址所在页在页帧号 */  
  17.     start_pfn = start >> PAGE_SHIFT;  
  18.     end_pfn = end >> PAGE_SHIFT;  
  19.   
  20.     /* 
  21.      * First iteration will setup identity mapping using large/small pages 
  22.      * based on use_pse, with other attributes same as set by 
  23.      * the early code in head_32.S 
  24.      * 
  25.      * Second iteration will setup the appropriate attributes (NX, GLOBAL..) 
  26.      * as desired for the kernel identity mapping. 
  27.      * 
  28.      * This two pass mechanism conforms to the TLB app note which says: 
  29.      * 
  30.      *     "Software should not write to a paging-structure entry in a way 
  31.      *      that would change, for any linear address, both the page size 
  32.      *      and either the page frame or attributes." 
  33.      */  
  34.     mapping_iter = 1;  
  35.   
  36.     if (!cpu_has_pse)  
  37.         use_pse = 0;  
  38.   
  39. repeat:  
  40.     pages_2m = pages_4k = 0;  
  41.     pfn = start_pfn;  
  42.     /* 返回页框在PGD表中的索引 */  
  43.     pgd_idx = pgd_index((pfn<<PAGE_SHIFT) + PAGE_OFFSET);  
  44.     pgd = pgd_base + pgd_idx;  
  45.     for (; pgd_idx < PTRS_PER_PGD; pgd++, pgd_idx++) {  
  46.         pmd = one_md_table_init(pgd); /* 创建该pgd目录项指向的pmd表 */  
  47.   
  48.         if (pfn >= end_pfn)  
  49.             continue;  
  50. #ifdef CONFIG_X86_PAE  
  51.         /* 三级映射需要设置pmd,因此得到页框在PMD表中的索引 */  
  52.         pmd_idx = pmd_index((pfn<<PAGE_SHIFT) + PAGE_OFFSET);  
  53.         pmd += pmd_idx;  
  54. #else  
  55.         pmd_idx = 0; /* 两级映射则无需设置 */  
  56. #endif  
  57.         for (; pmd_idx < PTRS_PER_PMD && pfn < end_pfn;  
  58.              pmd++, pmd_idx++) {  
  59.             unsigned int addr = pfn * PAGE_SIZE + PAGE_OFFSET;  
  60.   
  61.             /* 
  62.              * 如果可能,用大页面来映射,否则创建正常大小的页表: 
  63.              */  
  64.             if (use_pse) {  
  65.                 unsigned int addr2;  
  66.                 pgprot_t prot = PAGE_KERNEL_LARGE;  
  67.                 /* 
  68.                  * first pass will use the same initial 
  69.                  * identity mapping attribute + _PAGE_PSE. 
  70.                  */  
  71.                 pgprot_t init_prot =  
  72.                     __pgprot(PTE_IDENT_ATTR |  
  73.                          _PAGE_PSE);  
  74.   
  75.                 addr2 = (pfn + PTRS_PER_PTE-1) * PAGE_SIZE +  
  76.                     PAGE_OFFSET + PAGE_SIZE-1;  
  77.   
  78.                 if (is_kernel_text(addr) ||  
  79.                     is_kernel_text(addr2))  
  80.                     prot = PAGE_KERNEL_LARGE_EXEC;  
  81.   
  82.                 pages_2m++;  
  83.                 if (mapping_iter == 1)  
  84.                     set_pmd(pmd, pfn_pmd(pfn, init_prot));  
  85.                 else  
  86.                     set_pmd(pmd, pfn_pmd(pfn, prot));  
  87.   
  88.                 pfn += PTRS_PER_PTE;  
  89.                 continue;  
  90.             }  
  91.             pte = one_page_table_init(pmd); /* 返回PMD中第一个PTE */  
  92.             /* PTE的索引 */  
  93.             pte_ofs = pte_index((pfn<<PAGE_SHIFT) + PAGE_OFFSET);  
  94.             pte += pte_ofs; /* 定位带具体的pte */  
  95.             for (; pte_ofs < PTRS_PER_PTE && pfn < end_pfn;  
  96.                  pte++, pfn++, pte_ofs++, addr += PAGE_SIZE) {  
  97.                 pgprot_t prot = PAGE_KERNEL;  
  98.                 /* 
  99.                  * first pass will use the same initial 
  100.                  * identity mapping attribute. 
  101.                  */  
  102.                 pgprot_t init_prot = __pgprot(PTE_IDENT_ATTR);  
  103.   
  104.                 if (is_kernel_text(addr))  
  105.                     prot = PAGE_KERNEL_EXEC;  
  106.   
  107.                 pages_4k++; /* 没有PSE */  
  108.                 /* 设置页表,根据MAPPING_ITER变量的不同  
  109.                    对表设置不同的属性 */  
  110.                 if (mapping_iter == 1) /* 第一次迭代,属性设置都一样 */  
  111.                     set_pte(pte, pfn_pte(pfn, init_prot));  
  112.                 else /* 设置为具体的属性 */  
  113.                     set_pte(pte, pfn_pte(pfn, prot));  
  114.             }  
  115.         }  
  116.     }  
  117.     if (mapping_iter == 1) {  
  118.         /* 
  119.          * 只在第一次迭代中更新直接映射页的数量 
  120.          */  
  121.         update_page_count(PG_LEVEL_2M, pages_2m);  
  122.         update_page_count(PG_LEVEL_4K, pages_4k);  
  123.   
  124.         /* 
  125.          * local global flush tlb, which will flush the previous 
  126.          * mappings present in both small and large page TLB's. 
  127.          */  
  128.         __flush_tlb_all();  
  129.   
  130.         /* 
  131.          * 第二次迭代将设置实际的PTE属性 
  132.          */  
  133.         mapping_iter = 2;  
  134.         goto repeat;  
  135.     }  
  136.     return 0; /* 迭代两后返回 */  
  137. }  
  138.   
  139. static pmd_t * __init one_md_table_init(pgd_t *pgd)  
  140. {  
  141.     pud_t *pud;  
  142.     pmd_t *pmd_table;  
  143.   
  144. #ifdef CONFIG_X86_PAE  
  145.     /* 启用了PAE,需要三级映射,创建PMD表 */  
  146.     if (!(pgd_val(*pgd) & _PAGE_PRESENT)) {  
  147.         if (after_bootmem)  
  148.             pmd_table = (pmd_t *)alloc_bootmem_pages(PAGE_SIZE);  
  149.         else  
  150.             pmd_table = (pmd_t *)alloc_low_page();  
  151.         paravirt_alloc_pmd(&init_mm, __pa(pmd_table) >> PAGE_SHIFT);  
  152.         /* 设置PGD,将对应的PGD项设置为PMD表 */  
  153.         set_pgd(pgd, __pgd(__pa(pmd_table) | _PAGE_PRESENT));  
  154.         pud = pud_offset(pgd, 0);  
  155.         BUG_ON(pmd_table != pmd_offset(pud, 0));  
  156.   
  157.         return pmd_table;  
  158.     }  
  159. #endif  
  160.     /* 非PAE模式:只需二级映射,直接返回原来pgd地址 */  
  161.     pud = pud_offset(pgd, 0);  
  162.     pmd_table = pmd_offset(pud, 0);  
  163.   
  164.     return pmd_table;  
  165. }  
  166.   
  167. static pte_t * __init one_page_table_init(pmd_t *pmd)  
  168. {  
  169.     if (!(pmd_val(*pmd) & _PAGE_PRESENT)) {  
  170.         pte_t *page_table = NULL;  
  171.   
  172.         if (after_bootmem) {  
  173. #if defined(CONFIG_DEBUG_PAGEALLOC) || defined(CONFIG_KMEMCHECK)  
  174.             page_table = (pte_t *) alloc_bootmem_pages(PAGE_SIZE);  
  175. #endif  
  176.             if (!page_table)  
  177.                 page_table =  
  178.                 (pte_t *)alloc_bootmem_pages(PAGE_SIZE);  
  179.         } else /* 如果启动分配器还没有建立,那么  
  180.                    从刚才分配建立的表中分配空间 */  
  181.             page_table = (pte_t *)alloc_low_page();  
  182.   
  183.         paravirt_alloc_pte(&init_mm, __pa(page_table) >> PAGE_SHIFT);  
  184.          /* 设置PMD,将对应的PMD项设置为页表 */  
  185.         set_pmd(pmd, __pmd(__pa(page_table) | _PAGE_TABLE));  
  186.         BUG_ON(page_table != pte_offset_kernel(pmd, 0));  
  187.     }  
  188.   
  189.     return pte_offset_kernel(pmd, 0);  
  190. }  
  191.   
  192. static inline int is_kernel_text(unsigned long addr)  
  193. {  
  194.     if (addr >= PAGE_OFFSET && addr <= (unsigned long)__init_end)  
  195.         return 1;  
  196.     return 0;  
  197. }  
    分析:
    (1)函数开始定义了几个变量,pgd_base指向临时全局页表起始地址(即swapper_pg_dir)。pgd指向一个页表目录项开始的地址,pmd指向一个中间目录开始的地址,pte指向一个页表开始的地址,start_pfn为要映射的起始地址所在物理页框号,end_pfn为终止地址所在物理页框号。
    (2)函数实现采用两次迭代的方式来实现。第一次迭代使用基于use_pse标志的大内存页或小内存页来进行映射,其他属性则与前期head_32.S中的设置一致。第二次迭代设置内核映射需要的一些特别属性(NX, GLOBAL等)。这种两次迭代的实现方式是为了遵循TLB应用程序的理念,即对任何线性地址,软件不应该用改变页面大小或者物理页框及属性的方式来对页表条目进行写操作。TLB即Translation Lookaside Buffer,旁路转换缓冲,或称为页表缓冲;里面存放的是一些页表(虚拟地址到物理地址的转换表)。又称为快表技术。由于“页表”存储在主存储器中,查询页表所付出的代价很大,由此产生了TLB。
    在前面的“内存模型”中介绍过,x86系统使用三级页表机制,第一级页表称为页全局目录pgd,第二级为页中间目录pmd,第三级为页表条目pte。TLB和CPU里的一级、二级缓存之间不存在本质的区别,只不过前者缓存页表数据,而后两个缓存实际数据。当CPU执行机构收到应用程序发来的虚拟地址后,首先到TLB中查找相应的页表数据,如果TLB中正好存放着所需的页表,则称为TLB命中(TLB Hit),接下来CPU再依次看TLB中页表所对应的物理内存地址中的数据是不是已经在一级、二级缓存里了,若没有则到内存中取相应地址所存放的数据。既然说TLB是内存里存放的页表的缓存,那么它里边存放的数据实际上和内存页表区的数据是一致的,在内存的页表区里,每一条记录虚拟页面和物理页框对应关系的记录称之为一个页表条目(Entry),同样地,在TLB里边也缓存了同样大小的页表条目(Entry)。
    (3)迭代开始时,pgd_idx根据pgd_index宏计算出开始页框在PGD表中的索引,注意内核要从页目录表中第768个表项开始进行设置,因此索引值会从768开始。 从768到1024这个256个表项被linux内核设置成内核目录项,低768个目录项被用户空间使用。 pgd = pgd_base + pgd_idx使得pgd指向页框所在的pgd目录项。接下来的循环是要填充从该索引值到1024的这256个pgd目录项的内容。对其中每个表项,调用one_md_table_init()创建下一级pmd表,并让pgd表中的目录项指向它。其中若启用了PAE,则Linux需要三级分页以处理大内存页,因此创建pmd表;若没启用PAE,则只需二级映射,这会忽略pmd中间目录表的,因此通过pmd_offset直接返回pgd的地址。
    (4)对Linux三级映射模型,需要继续设置pmd表。因此用pmd_index宏计算出页框在PMD表中的索引,定位到对应的pmd目录项,然后用一个循环填充各个pmd目录项的内容(二级映射则直接忽略些循环)。对每个pmd目录项,先计算出物理页框要映射到的内核空间线性地址addr,从代码可以看到它从0xc000000开始的,也就是从内核空间开始。根据use_pse标志来决定是使用大内存页映射,如果是使用普通的4K内存页映射,则调用one_page_table_init()创建一个最终的页表pte,并让pmd目录项指向它。在该函数中,若启动分配器已建立,则利用alloc_bootmem_low_pages()分配一个4k大小的物理页面,否则从刚才分配建立的表中分配空间。然后用set_pmd(pmd, __pmd(__pa(page_table) | _PAGE_TABLE))来设置对应pmd表项。page_table显然属于线性地址,先通过__pa宏转化为物理地址,再与上_PAGE_TABLE宏,此时它们还是无符号整数,再通过__pmd宏把无符号整数转化为pmd类型,经过这些转换,就得到了一个具有属性的表项,然后通过set_pmd宏设置pmd表项。
    (5)设置pte表也是一个循环。pte表中有1024个表项,先要计算出要映射的页框所在的表项索引值,然后对每个页表项,用__pgprot(PTE_IDENT_ATTR)获取同一个初始化映射属性,因为在第一次迭代中使用这个属性。 is_kernel_text函数判断addr线性地址是否属于内核代码段。PAGE_OFFSET表示内核代码段的开始地址,__init_end是个内核符号,在内核链接的时候生成的,表示内核代码段的终止地址。如果是,那么在设置页表项的时候就要加个PAGE_KERNEL_EXEC属性,如果不是,则加个PAGE_KERNEL属性。第二次迭代会使用这个属性。这些属性定义可以在arch/x86/include/asm/pgtable_types.h中找到。最后通过set_pte(pte, pfn_pte(pfn, ...))来设置页表项,先通过pfn_pte宏根据页框号和页表项的属性值合并成一个页表项值,然户在用set_pte宏把页表项值写到页表项里。注意第一次迭代设置的是init_prot中的属性,第二次迭代设置prot中的属性。
    (6)是后,对第一次迭代,还要更新直接映射页面数。并调用__flush_tlb_all()刷新小内存页或大内存页的TLB中的映射内容。
    在开始的init_memory_mapping()执行中,当通过kernel_physical_mapping_init()建立完低端物理内存区与内核空间的三级页表映射后,内核页表就设置好了。然后调用early_ioremap_page_table_range_init()初始化高端内存的固定映射区。
    5、高端内存固定映射区的初始化
    early_ioremap_page_table_range_init()函数也是在arch/x86/mm/init_32.c中。它只是对固定映射区创建页表结构,并不建立实际映射,实际映射将由set_fixmap()来完成。如下:
[cpp] view plain copy
  1. void __init early_ioremap_page_table_range_init(void)  
  2. {  
  3.     pgd_t *pgd_base = swapper_pg_dir;  
  4.     unsigned long vaddr, end;  
  5.   
  6.     /* 
  7.      * 固定映射,只是创建页表结构,并不建立实际映射。实际映射将由set_fixmap()来完成: 
  8.      */  
  9.     vaddr = __fix_to_virt(__end_of_fixed_addresses - 1) & PMD_MASK;  
  10.     end = (FIXADDR_TOP + PMD_SIZE - 1) & PMD_MASK;  
  11.     /* 这里是对临时映射区域进行映射而为页表等分配了空间,  
  12.        但是没有建立实际的映射 */  
  13.     page_table_range_init(vaddr, end, pgd_base);  
  14.     /* 置变量after_paging_init为1,表示启动了分页机制 */  
  15.     early_ioremap_reset();  
  16. }  
  17.   
  18. static void __init  
  19. page_table_range_init(unsigned long start, unsigned long end, pgd_t *pgd_base)  
  20. {  
  21.     int pgd_idx, pmd_idx;  
  22.     unsigned long vaddr;  
  23.     pgd_t *pgd;  
  24.     pmd_t *pmd;  
  25.     pte_t *pte = NULL;  
  26.   
  27.     vaddr = start;  
  28.     pgd_idx = pgd_index(vaddr);  
  29.     pmd_idx = pmd_index(vaddr);  
  30.     pgd = pgd_base + pgd_idx;  
  31.   
  32.     for ( ; (pgd_idx < PTRS_PER_PGD) && (vaddr != end); pgd++, pgd_idx++) {  
  33.         pmd = one_md_table_init(pgd);  
  34.         pmd = pmd + pmd_index(vaddr);  
  35.         for (; (pmd_idx < PTRS_PER_PMD) && (vaddr != end);  
  36.                             pmd++, pmd_idx++) {  
  37.             /* early fixmap可能对临时映射区中的页表项已经分配了页表, 
  38.                为使页表分配的空间连续,需要对临时映射区的页表指定区间重新分配 */  
  39.             /* 在这里已经对pte进行了分配和初始化 */  
  40.             pte = page_table_kmap_check(one_page_table_init(pmd),  
  41.                                         pmd, vaddr, pte);  
  42.   
  43.             vaddr += PMD_SIZE;  
  44.         }  
  45.         pmd_idx = 0;  
  46.     }  
  47. }  
    分析:
    (1)先计算出固定映射区的起始和终止地址,然后调用page_table_range_init(),用新的bootmem页表项初始化这段高端物理内存要映射到的内核虚拟地址空间,但并不建立实际的映射。最后用early_ioremap_reset()设置after_paging_init为1,表示启动分页机制。
    (2)在函数page_table_range_init()中,先获取起址的pgd表项索引、pmd表项索引,然后类似地建立下一级pmd表,和最终的pte页表。在建立页表时需要调用page_table_kmap_check()进行检查,因为在前期可能对固定映射区已经分配了页表项,为使页表分配的空间连续,需要对固定映射区的页表指定区间重新分配。
    在init_memory_mapping()中,内核设置好内核页表,并初始化完高端固定映射区后,紧接着调用load_cr3(swapper_pg_dir),将页全局目录表基址swapper_pg_dir送入控制寄存器cr3。每当重新设置cr3时, CPU就会将页面映射目录所在的页面装入CPU内部高速缓存中的TLB部分。现在内存中(实际上是高速缓存中)的映射目录变了,就要再让CPU装入一次。由于页面映射机制本来就是开启着的,所以从load_cr3这条指令执行完以后就扩大了系统空间中有映射区域的大小, 使整个映射覆盖到整个物理内存(高端内存除外)。实际上此时swapper_pg_dir中已经改变的目录项很可能还在高速缓存中,所以还要通过__flush_tlb_all()将高速缓存中的内容冲刷到内存中,这样才能保证内存中映射目录内容的一致性。
    通过上述对init_memory_mapping()的剖析,我们可以清晰的看到,构建内核页表,无非就是向相应的表项写入下一级地址和属性。在内核空间保留着一部分内存专门用来存放内核页表。当cpu要进行寻址的时候,无论在内核空间,还是在用户空间,都会通过这个页表来进行映射。对于这个函数,内核把整个物理内存空间都映射完了,当用户空间的进程要使用物理内存时,岂不是不能做相应的映射了?其实不会的,内核只是做了映射,映射不代表使用,这样做是内核为了方便管理内存而已。

Linux内存管理(5):分页机制和管理区初始化

1、初始化启动内存分配器

    在内存子系统初始化以前,即boot阶段也需要进行内存管理,启动内存分配器是专为此而设计的。linux启动内存分配器是在伙伴系统、slab机制实现之前,为满足内核中内存的分配而建立的。本身的机制比较简单,使用位图来进行标志分配和释放。arch/x86/kernel/setup.c:setup_arch()在用init_memory_mapping(0, max_low_pfn<<PAGE_SHIFT)建立完内核页表之后,就会调用arch/x86/mm/init_32.c:initmem_init(0, max_pfn)启动bootmem内存分配器。如下:

[cpp] view plain copy
  1. #ifndef CONFIG_NEED_MULTIPLE_NODES  
  2. void __init initmem_init(unsigned long start_pfn,  
  3.                   unsigned long end_pfn)  
  4. {  
  5. #ifdef CONFIG_HIGHMEM  
  6.     highstart_pfn = highend_pfn = max_pfn;  
  7.     if (max_pfn > max_low_pfn)  
  8.         highstart_pfn = max_low_pfn;  
  9.     /* 注册内存活动区 */  
  10.     e820_register_active_regions(0, 0, highend_pfn);  
  11.     sparse_memory_present_with_active_regions(0);  
  12.     printk(KERN_NOTICE "%ldMB HIGHMEM available.\n",  
  13.         pages_to_mb(highend_pfn - highstart_pfn));  
  14.     num_physpages = highend_pfn;  
  15.     /* 计算高端内存地址 */  
  16.     high_memory = (void *) __va(highstart_pfn * PAGE_SIZE - 1) + 1;  
  17. #else  
  18.     e820_register_active_regions(0, 0, max_low_pfn);  
  19.     sparse_memory_present_with_active_regions(0);  
  20.     num_physpages = max_low_pfn;  
  21.     high_memory = (void *) __va(max_low_pfn * PAGE_SIZE - 1) + 1;  
  22. #endif  
  23. #ifdef CONFIG_FLATMEM  
  24.     max_mapnr = num_physpages;  
  25. #endif  
  26.     __vmalloc_start_set = true;  
  27.   
  28.     printk(KERN_NOTICE "%ldMB LOWMEM available.\n",  
  29.             pages_to_mb(max_low_pfn));  
  30.   
  31.     setup_bootmem_allocator(); /* 启动内存分配器 */  
  32. }  
  33. #endif /* !CONFIG_NEED_MULTIPLE_NODES */  
    主要工作是调用e820_register_active_regions()在节点0上注册内存活动区,然后调用setup_bootmem_allocator()建立启动内存分配器。Linux的内存活动区域其实就是全局变量e820中的内存块做了相关检查和处理后的区域,它会在管理区初始化等地方被用到。注册时,要根据是否配置了高端内存来决定活动的区的终止地址。
    函数e820_register_active_regions()在arch/x86/kernel/e820.c中,它扫描e820内存图,并在一个节点nid上注册活动区。如下:
[cpp] view plain copy
  1. /* 扫描e820内存图,并在一个节点上注册活动区 */  
  2. void __init e820_register_active_regions(int nid, unsigned long start_pfn,  
  3.                      unsigned long last_pfn)  
  4. {  
  5.     unsigned long ei_startpfn;  
  6.     unsigned long ei_endpfn;  
  7.     int i;  
  8.   
  9.     for (i = 0; i < e820.nr_map; i++)  
  10.         /* 从全局变量e820中查找活动区 */  
  11.         if (e820_find_active_region(&e820.map[i],  
  12.                         start_pfn, last_pfn,  
  13.                         &ei_startpfn, &ei_endpfn))  
  14.             /* 添加查找到的活动区 */  
  15.             add_active_range(nid, ei_startpfn, ei_endpfn);  
  16. }  
  17.   
  18. /* 
  19.  * 在start_pfn到last_pfn的地址范围内查找一个活动区,并在ei_startpfn和ei_endpfn中返回 
  20.  * 这个e820内存块的范围 
  21.  */  
  22. int __init e820_find_active_region(const struct e820entry *ei,  
  23.                   unsigned long start_pfn,  
  24.                   unsigned long last_pfn,  
  25.                   unsigned long *ei_startpfn,  
  26.                   unsigned long *ei_endpfn)  
  27. {  
  28.     u64 align = PAGE_SIZE;  
  29.   
  30.     *ei_startpfn = round_up(ei->addr, align) >> PAGE_SHIFT;  
  31.     *ei_endpfn = round_down(ei->addr + ei->size, align) >> PAGE_SHIFT;  
  32.   
  33.     /* 跳过内存图中比一个页面还小的各个内存块 */  
  34.     if (*ei_startpfn >= *ei_endpfn)  
  35.         return 0;  
  36.   
  37.     /* 如果内存图中的所有内存块都在节点范围之外,则跳过 */  
  38.     if (ei->type != E820_RAM || *ei_endpfn <= start_pfn ||  
  39.                     *ei_startpfn >= last_pfn)  
  40.         return 0;  
  41.   
  42.     /* 检查是否有重叠 */  
  43.     if (*ei_startpfn < start_pfn)  
  44.         *ei_startpfn = start_pfn;  
  45.     if (*ei_endpfn > last_pfn)  
  46.         *ei_endpfn = last_pfn;  
  47.   
  48.     return 1;  
  49. }  
    主要的工作是在start_pfn到last_pfn的地址范围内,从e820内存图的各内存块中查找一个物理活动区,若找到,则把其物理地址范围保存到ei_startpfn和ei_endpfn中,然后调用mm/page_alloc.c中的add_active_range()函数在nid节点上注册这块活动区。如下:
[cpp] view plain copy
  1. /* 添加活动区域,需要对原有的进行检查 */  
  2. void __init add_active_range(unsigned int nid, unsigned long start_pfn,  
  3.                         unsigned long end_pfn)  
  4. {  
  5.     int i;  
  6.   
  7.     mminit_dprintk(MMINIT_TRACE, "memory_register",  
  8.             "Entering add_active_range(%d, %#lx, %#lx) "  
  9.             "%d entries of %d used\n",  
  10.             nid, start_pfn, end_pfn,  
  11.             nr_nodemap_entries, MAX_ACTIVE_REGIONS);  
  12.   
  13.     mminit_validate_memmodel_limits(&start_pfn, &end_pfn);  
  14.   
  15.     /* 如果可能,与存在的活动内存区合并 */  
  16.     for (i = 0; i < nr_nodemap_entries; i++) {  
  17.         if (early_node_map[i].nid != nid)  
  18.             continue;  
  19.   
  20.         /* 如果一个存在的活动区包含这个要添加的新区,则跳过 */  
  21.         if (start_pfn >= early_node_map[i].start_pfn &&  
  22.                 end_pfn <= early_node_map[i].end_pfn)  
  23.             return;  
  24.   
  25.         /* 如果合适,则向前合并 */  
  26.         if (start_pfn <= early_node_map[i].end_pfn &&  
  27.                 end_pfn > early_node_map[i].end_pfn) {  
  28.             early_node_map[i].end_pfn = end_pfn;  
  29.             return;  
  30.         }  
  31.   
  32.         /* 如果合适,则向后合并 */  
  33.         if (start_pfn < early_node_map[i].end_pfn &&  
  34.                 end_pfn >= early_node_map[i].start_pfn) {  
  35.             early_node_map[i].start_pfn = start_pfn;  
  36.             return;  
  37.         }  
  38.     }  
  39.   
  40.     /* 检查early_node_map是否足够大 */  
  41.     if (i >= MAX_ACTIVE_REGIONS) {  
  42.         printk(KERN_CRIT "More than %d memory regions, truncating\n",  
  43.                             MAX_ACTIVE_REGIONS);  
  44.         return;  
  45.     }  
  46.   
  47.     early_node_map[i].nid = nid;  
  48.     early_node_map[i].start_pfn = start_pfn;  
  49.     early_node_map[i].end_pfn = end_pfn;  
  50.     nr_nodemap_entries = i + 1;  
  51. }  
    该函数注册一段页框范围指定的物理内存活动区,参数nid为要注册到的节点编号,start_pfn为可用物理内存的开始PFN(物理页框号),end_pfn为可用物理内存的终止PFN。这些活动被存储在全局的early_node_map[]数组中(该数组也定义在mm/page_alloc.c中),表示内存管理的早期节点显现图。它会被以后的free_area_init_nodes()用来计算管理区大小和空洞数量。如果活动区范围跨越一个内存空洞,则需要确保内存不会被bootmem分配器释放(依赖于体系结构)。如果可能,要注册的活动区可以跟已存在的活动区合并。
    回到arch/x86/mm/init_32.c:initmem_init(),最后是调用arch/x86/mm/init_32.c:setup_bootmem_allocator()建立内核引导时的启动内存分配器。在建立启动内存分配器的时候,会涉及到保留内存。也就是说,当分配器进行内存分配时,之前保留给页表、分配器本身(用于映射的位图)、io的这些保留内存就不能再分配了。linux中对保留内存空间的部分用下列数据结构表示,在arch/x86/kernel/e820.c中:
[cpp] view plain copy
  1. /* 
  2.  * Early reserved memory areas. 
  3.  */  
  4. #define MAX_EARLY_RES 20  /* 保留空间最大块数 */  
  5.   
  6. struct early_res {  /* 保留空间结构 */  
  7.     u64 start, end;  
  8.     char name[16];  
  9.     char overlap_ok;  
  10. };  
  11. /* 保留内存空间全局变量 */  
  12. static struct early_res early_res[MAX_EARLY_RES] __initdata = {  
  13.     { 0, PAGE_SIZE, "BIOS data page" }, /* BIOS data page */  
  14.     {}  
  15. };  
  16.     bootmem分配器的数据结构bootmem_data_t用于管理启动内存的分配、释放等,在include/linux/bootmem.h中,如下:  
  17. /* 用于bootmem分配器的节点数据结构 */  
  18. typedef struct bootmem_data {  
  19.     unsigned long node_min_pfn;  
  20.     unsigned long node_low_pfn;  
  21.     void *node_bootmem_map;  
  22.     unsigned long last_end_off;  
  23.     unsigned long hint_idx;  
  24.     struct list_head list;  
  25. } bootmem_data_t;  
    这些域分别为存放bootmem位图的第一个页面(即内核映象结束处的第一个页面)、低端内存最大页面号(物理内存的顶点,最高不超过896MB)、位图(各个位代表节点上的所有物理内存页,包括洞)、前一次分配的最后一个字节相对于last_pos的位移量、hint_idx为前一次分配的最后一个页面号、list是用于内存分配的链表。注意在内存节点pg_data_t数据结构中,用bdata指针批向了这个bootmem分配器的数据结构。
    全局链表定义可在mm/bootmeme.c中找到,如下:
static struct list_head bdata_list __initdata = LIST_HEAD_INIT(bdata_list);
    启动分配器的建立主要的流程为初始化映射位图、活动内存区的映射位置0(表示可用)、保留内存区域处理,其中保留区存放在上面介绍的全局数组中,这里只是将分配器中对应映射位图值1,表示已经分配。核心函数是arch/x86/mm/init_32.c:setup_bootmem_allocator(),以及setup_node_bootmem()。如下:
[cpp] view plain copy
  1. void __init setup_bootmem_allocator(void)  
  2. {  
  3.     int nodeid;  
  4.     unsigned long bootmap_size, bootmap;  
  5.     /* 
  6.      * 初始化引导时的内存分配器(只是低端内存区): 
  7.      */  
  8.     /* 计算所需要的映射页面大小一个字节一位,所以需要对总的页面大小除以8 */   
  9.     bootmap_size = bootmem_bootmap_pages(max_low_pfn)<<PAGE_SHIFT;  
  10.     /* 从e820中查找一个合适的内存块 */  
  11.     bootmap = find_e820_area(0, max_pfn_mapped<<PAGE_SHIFT, bootmap_size,  
  12.                  PAGE_SIZE);  
  13.     if (bootmap == -1L)  
  14.         panic("Cannot find bootmem map of size %ld\n", bootmap_size);  
  15.     /* 将用于位图映射的页面保留 */  
  16.     reserve_early(bootmap, bootmap + bootmap_size, "BOOTMAP");  
  17.   
  18.     printk(KERN_INFO "  mapped low ram: 0 - %08lx\n",  
  19.          max_pfn_mapped<<PAGE_SHIFT);  
  20.     printk(KERN_INFO "  low ram: 0 - %08lx\n", max_low_pfn<<PAGE_SHIFT);  
  21.     /* 扫描每个在线节点 */  
  22.     for_each_online_node(nodeid) {  
  23.          unsigned long start_pfn, end_pfn;  
  24.   
  25. #ifdef CONFIG_NEED_MULTIPLE_NODES  
  26.         /* 计算出当前节点的起始地址和终止地址 */  
  27.         start_pfn = node_start_pfn[nodeid];  
  28.         end_pfn = node_end_pfn[nodeid];  
  29.         if (start_pfn > max_low_pfn)  
  30.             continue;  
  31.         if (end_pfn > max_low_pfn)  
  32.             end_pfn = max_low_pfn;  
  33. #else  
  34.         start_pfn = 0;  
  35.         end_pfn = max_low_pfn;  
  36. #endif  
  37.         /* 对指定节点安装启动分配器 */  
  38.         bootmap = setup_node_bootmem(nodeid, start_pfn, end_pfn,  
  39.                          bootmap);  
  40.     }  
  41.     /* bootmem的分配制度到这里就已经建立完成,把after_bootmem变量置成1 */  
  42.     after_bootmem = 1;  
  43. }  
  44.   
  45. static unsigned long __init setup_node_bootmem(int nodeid,  
  46.                  unsigned long start_pfn,  
  47.                  unsigned long end_pfn,  
  48.                  unsigned long bootmap)  
  49. {  
  50.     unsigned long bootmap_size;  
  51.   
  52.     /* 初始化这个内存节点:将映射位图中的所有位置1。不要触及min_low_pfn */  
  53.     bootmap_size = init_bootmem_node(NODE_DATA(nodeid),  
  54.                      bootmap >> PAGE_SHIFT,  
  55.                      start_pfn, end_pfn);  
  56.     printk(KERN_INFO "  node %d low ram: %08lx - %08lx\n",  
  57.         nodeid, start_pfn<<PAGE_SHIFT, end_pfn<<PAGE_SHIFT);  
  58.     printk(KERN_INFO "  node %d bootmap %08lx - %08lx\n",  
  59.          nodeid, bootmap, bootmap + bootmap_size);  
  60.     /* 将活动内存区对应位图相关位置0,表示可被分配的 */   
  61.     free_bootmem_with_active_regions(nodeid, end_pfn);  
  62.     /* 将保留内存的相关页面对应位置为1,表示已经分配 
  63.        或者不可用(不能被分配) */  
  64.     early_res_to_bootmem(start_pfn<<PAGE_SHIFT, end_pfn<<PAGE_SHIFT);  
  65.     /* 返回映射页面的最后地址,下次映射即可以从这里开始 */  
  66.     return bootmap + bootmap_size;  
  67. }  
    设置分配器的主要工作是初始化引导时的内存分配器(只是低端内存区);在e820中查找引导内存块;对每个在线节点计算出其起始和终止地址,然后调用setup_node_bootmem()安装启动分配器。在这个函数中,调用init_bootmem_node()初始化这个节点的映射位图。将活动内存区对应位图相关位置0,表示可用;将保留内存的相关页面对应位置为1,表示已经分配(不可用)。其中初始化映射位图的函数init_bootmem_node()在mm/bootmem.c中,调用链为init_bootmem_node()--->init_bootmem_core()--->link_bootmem(bdata),最终将bdata添加到全局的bdata_list链表中。当所有在线内存节点设置好后,bootmem内存分配器就初始化完毕。
    mm/bootmem.c实现了完整的引导时物理内存分配器和配置器,包括内存节点初始化、内存分配、释放等各种操作。我们概述一下启动内存分配器的主要操作接口功能:
    init_bootmem_node():注册一个节点以作为启动内存。核心操作由init_bootmem_core()完成,每调用它一次来设置自己的分配器。
    link_bootmem():按顺序添加一个bdata到全局的bdata_list链表中。
    free_all_bootmem_node():释放一个节点的可用页面给伙伴系统。核心操作由free_all_bootmem_core()完成。
    free_bootmem_node():将指定节点上的一个页面范围标记为可用(即未分配)。
    reserve_bootmem_node():将指定节点上的一个页面范围标记为保留。
    __alloc_bootmem_node():为指定节点分配启动内存。核心操作由alloc_bootmem_core()完成。
    __free():bootmem分配器的释放内存操作。
    __reserve():bootmem分配器的保留内存操作。
    alloc_bootmem_core():bootmem分配器的分配内存操作。    
    2、建立永久的分页机制
    在前面的“内存映射机制“介绍中,init_memory_mapping()只是构建了内核页表,作为临时的分页映射。例如只对高端内存固定映射区创建了页表结构,并没有对高端内存区永久映射区进行初始化。setup_arch()在执行完init_memory_mapping()和initmem_init()后,就会调用arch/x86/mm/init_32.c:paging_init()建立虚拟内存管理要用到的完整页表和永久分页机制。如下:
[cpp] view plain copy
  1. void __init paging_init(void)  
  2. {  
  3.     pagetable_init();  
  4.   
  5.     __flush_tlb_all();  
  6.   
  7.     kmap_init();  
  8.   
  9.     /* 
  10.      * NOTE: 在这里bootmem分配器完全可用了 
  11.      */  
  12.     sparse_init();  
  13.     zone_sizes_init();  
  14. }  
    该函数建立完整的页表,注意起始的8MB已经被head_32.S映射了。该函数也会取消虚拟地址0处的页面映射,以便我们可以在内核中陷入并跟踪那些麻烦的NULL引用错误。它的主要工作包括页表初始化、内核永久映射区初始化、稀疏内存映射初始化、管理区初始化。下面重点讨论该函数。
    arch/x86/mm/init_32.c:pagetable_init()函数用于完成页表初始化,并初始化高端内存永久映射区。如下:
[cpp] view plain copy
  1. static void __init pagetable_init(void)  
  2. {  
  3.     pgd_t *pgd_base = swapper_pg_dir;  
  4.   
  5.     permanent_kmaps_init(pgd_base);  
  6. }  
  7.   
  8. #ifdef CONFIG_HIGHMEM  
  9. static void __init permanent_kmaps_init(pgd_t *pgd_base)  
  10. {  
  11.     unsigned long vaddr;  
  12.     pgd_t *pgd;  
  13.     pud_t *pud;  
  14.     pmd_t *pmd;  
  15.     pte_t *pte;  
  16.   
  17.     vaddr = PKMAP_BASE;  
  18.     /* 该阶段,也就是永久内存映射区的页表初始化 */  
  19.     page_table_range_init(vaddr, vaddr + PAGE_SIZE*LAST_PKMAP, pgd_base);  
  20.   
  21.     pgd = swapper_pg_dir + pgd_index(vaddr);  
  22.     pud = pud_offset(pgd, vaddr);  
  23.     pmd = pmd_offset(pud, vaddr);  
  24.     pte = pte_offset_kernel(pmd, vaddr);  
  25.     /* 将永久映射区间映射的第一个页表项保存到pkmap_page_table中 */  
  26.     pkmap_page_table = pte;  
  27. }  
  28. /* ...... */  
  29. #else  
  30. static inline void permanent_kmaps_init(pgd_t *pgd_base)  
  31. {  
  32. }  
  33. #endif /* CONFIG_HIGHMEM */  
    根据上面代码,只有定义了使用高端内存,才会有高端永久映射区。首先用pgd_base保存页全局目录表的起始地址swapper_pg_dir。而后在函数permanent_kmaps_init()中,调用page_table_range_init()建立页表,这个函数在前面分析过,它会先根据永久映射区起始地址PKMAP_BASE,获取pgd表项索引、pmd表项索引,然后建立下一级pmd表,和最终的pte页表。第一个页表项保存到pkmap_page_table中。如果内核不划分高端内存,则permanent_kmaps_init()什么也不做。注意paging_init()初始化完页表后,要用__flush_tlb_all()刷新缓存TLB中的映射内容。
    arch/x86/mm/init_32.c:kmap_init()函数用于缓存第一个kmap页表项,如下:
[cpp] view plain copy
  1. static void __init kmap_init(void)  
  2. {  
  3.     unsigned long kmap_vstart;  
  4.   
  5.     /* 
  6.      * Cache the first kmap pte: 
  7.      */  
  8.     /* 得到高端固定内存映射区域的起始内存的页表,将这个页表  
  9.      放到kmap_pte变量中。确切的说应该是固定内存中的临时内存映射区域 */  
  10.     kmap_vstart = __fix_to_virt(FIX_KMAP_BEGIN);  
  11.     kmap_pte = kmap_get_fixmap_pte(kmap_vstart);  
  12.   
  13.     kmap_prot = PAGE_KERNEL;  
  14. }  
    该函数首先把高端固定映射区(即高端临时内存映射区)的起始地址FIX_KMAP_BEGIN转换成虚拟地址,然后获取它的pte页表项,并保存到全局的kmap_pte中。
    mm/sparse.c:sparse_init()函数用于初始稀疏内存的映射,这里就不展开了。这里重点介绍管理区初始化,这是内存管理的重要组成部分,在arch/x86/mm/init_32.c:zone_sizes_init()中,如下:
[cpp] view plain copy
  1. static void __init zone_sizes_init(void)  
  2. {  
  3.     /* 初始化各种管理区中的最大页面数,在后面用于具体的初始化工作 */  
  4.     unsigned long max_zone_pfns[MAX_NR_ZONES];  
  5.     memset(max_zone_pfns, 0, sizeof(max_zone_pfns));  
  6.     max_zone_pfns[ZONE_DMA] = /* DMA区的最大页面帧号,后面的类似 */  
  7.         virt_to_phys((char *)MAX_DMA_ADDRESS) >> PAGE_SHIFT;  
  8.     max_zone_pfns[ZONE_NORMAL] = max_low_pfn;  
  9. #ifdef CONFIG_HIGHMEM  
  10.     max_zone_pfns[ZONE_HIGHMEM] = highend_pfn;  
  11. #endif  
  12.      /* 内存体系的MMU建立,包括伙伴系统的初步建立 */  
  13.     free_area_init_nodes(max_zone_pfns);  
  14. }  
    在“内存描述”一节中对各种管理区类型做了详细介绍,这里首先用数组max_zone_pfns保存各种类型管理区的最大页面数,宏MAX_DMA_ADDRESS在arch/x86/include/asm/dma.h中定义,表示能执行DMA传输的最大地址,其中x86-32非PAE模式下MAX_DMA_ADDRESS为PAGE_OFFSET + 0x1000000,即从内核空间开始处的16MB为DMA区的地址范围,因此DMA区的地址范围为3G~3G+16M这一段空间。把这个最大地址转换成页帧号保存到max_zone_pfns数组,接着保存NORMAL区和HIGHMEM区的最大页面号。最后调用核心函数mm/page_alloc.c:free_area_init_nodes()初始化所有pg_data_t内存节点的各种管理区数据,传入参数为由各管理区最大PFN构成的数组。代码如下:
[cpp] view plain copy
  1. void __init free_area_init_nodes(unsigned long *max_zone_pfn)  
  2. {  
  3.     unsigned long nid;  
  4.     int i;  
  5.   
  6.     /* Sort early_node_map as initialisation assumes it is sorted */  
  7.     sort_node_map(); /* 将活动区域进行排序 */  
  8.   
  9.     /* 记录管理区的界限 */  
  10.     memset(arch_zone_lowest_possible_pfn, 0,  
  11.                 sizeof(arch_zone_lowest_possible_pfn));  
  12.     memset(arch_zone_highest_possible_pfn, 0,  
  13.                 sizeof(arch_zone_highest_possible_pfn));  
  14.     /* 找出活动内存中最小的页面 */  
  15.     arch_zone_lowest_possible_pfn[0] = find_min_pfn_with_active_regions();  
  16.     arch_zone_highest_possible_pfn[0] = max_zone_pfn[0];  
  17.     for (i = 1; i < MAX_NR_ZONES; i++) {  
  18.         if (i == ZONE_MOVABLE)  
  19.             continue;  
  20.         /* 假定区域连续,下一个区域的最小页面为上一个区的最大页面 */  
  21.         arch_zone_lowest_possible_pfn[i] =  
  22.             arch_zone_highest_possible_pfn[i-1];  
  23.         arch_zone_highest_possible_pfn[i] =  
  24.             max(max_zone_pfn[i], arch_zone_lowest_possible_pfn[i]);  
  25.     }  
  26.     /* 对ZONE_MOVABLE区域设置为0 */  
  27.     arch_zone_lowest_possible_pfn[ZONE_MOVABLE] = 0;  
  28.     arch_zone_highest_possible_pfn[ZONE_MOVABLE] = 0;  
  29.   
  30.     /* 找出每个节点上ZONE_MOVABLE区的开始页面号 */  
  31.     memset(zone_movable_pfn, 0, sizeof(zone_movable_pfn));  
  32.     find_zone_movable_pfns_for_nodes(zone_movable_pfn);  
  33.   
  34.     /* 打印管理区的范围 */  
  35.     printk("Zone PFN ranges:\n");  
  36.     for (i = 0; i < MAX_NR_ZONES; i++) {  
  37.         if (i == ZONE_MOVABLE)  
  38.             continue;  
  39.         printk("  %-8s %0#10lx -> %0#10lx\n",  
  40.                 zone_names[i],  
  41.                 arch_zone_lowest_possible_pfn[i],  
  42.                 arch_zone_highest_possible_pfn[i]);  
  43.     }  
  44.   
  45.     /* 打印每个节点上ZONE_MOVABLE区开始的页面号 */  
  46.     printk("Movable zone start PFN for each node\n");  
  47.     for (i = 0; i < MAX_NUMNODES; i++) {  
  48.         if (zone_movable_pfn[i])  
  49.             printk("  Node %d: %lu\n", i, zone_movable_pfn[i]);  
  50.     }  
  51.   
  52.     /* 打印early_node_map[] */  
  53.     printk("early_node_map[%d] active PFN ranges\n", nr_nodemap_entries);  
  54.     for (i = 0; i < nr_nodemap_entries; i++)  
  55.         printk("  %3d: %0#10lx -> %0#10lx\n", early_node_map[i].nid,  
  56.                         early_node_map[i].start_pfn,  
  57.                         early_node_map[i].end_pfn);  
  58.   
  59.     /* 初始化每个节点 */  
  60.     mminit_verify_pageflags_layout(); /* 调试用 */  
  61.     setup_nr_node_ids();  
  62.     for_each_online_node(nid) {  
  63.         pg_data_t *pgdat = NODE_DATA(nid);  
  64.         /* zone中数据的初始化,伙伴系统建立,但是没有页面  
  65.            和数据,页面在后面的mem_init中得到 */  
  66.         free_area_init_node(nid, NULL,  
  67.                 find_min_pfn_for_node(nid), NULL);  
  68.   
  69.         /* 对该节点上的任何内存区 */  
  70.         if (pgdat->node_present_pages)  
  71.             node_set_state(nid, N_HIGH_MEMORY);  
  72.         /* 内存的相关检查 */  
  73.         check_for_regular_memory(pgdat);  
  74.     }  
  75. }  
  76.   
  77. void __paginginit free_area_init_node(int nid, unsigned long *zones_size,  
  78.         unsigned long node_start_pfn, unsigned long *zholes_size)  
  79. {  
  80.     pg_data_t *pgdat = NODE_DATA(nid);  
  81.   
  82.     pgdat->node_id = nid;  
  83.     /* 这个已在前面调用一个函数得到 */  
  84.     pgdat->node_start_pfn = node_start_pfn;  
  85.     /* 计算系统中节点nid的所有物理页面并保存在数据结构中 */  
  86.     calculate_node_totalpages(pgdat, zones_size, zholes_size);  
  87.     /* 当节点只有一个时,将节点的map保存到全局变量中 */  
  88.     alloc_node_mem_map(pgdat);  
  89. #ifdef CONFIG_FLAT_NODE_MEM_MAP  
  90.     printk(KERN_DEBUG "free_area_init_node: node %d, pgdat %08lx, node_mem_map %08lx\n",  
  91.         nid, (unsigned long)pgdat,  
  92.         (unsigned long)pgdat->node_mem_map);  
  93. #endif  
  94.     /* zone中相关数据的初始化,包括伙伴系统,等待队列,相关变量,  
  95.         数据结构、链表等 */  
  96.     free_area_init_core(pgdat, zones_size, zholes_size);  
  97. }  
  98.   
  99. static void __paginginit free_area_init_core(struct pglist_data *pgdat,  
  100.         unsigned long *zones_size, unsigned long *zholes_size)  
  101. {  
  102.     enum zone_type j;  
  103.     int nid = pgdat->node_id;  
  104.     unsigned long zone_start_pfn = pgdat->node_start_pfn;  
  105.     int ret;  
  106.   
  107.     pgdat_resize_init(pgdat);  
  108.     pgdat->nr_zones = 0;  
  109.     init_waitqueue_head(&pgdat->kswapd_wait);  
  110.     pgdat->kswapd_max_order = 0;  
  111.     pgdat_page_cgroup_init(pgdat);  
  112.       
  113.     for (j = 0; j < MAX_NR_ZONES; j++) {  
  114.         struct zone *zone = pgdat->node_zones + j;  
  115.         unsigned long size, realsize, memmap_pages;  
  116.         enum lru_list l;  
  117.         /* 下面的两个函数会获得指定节点的真实内存大小 */  
  118.         size = zone_spanned_pages_in_node(nid, j, zones_size);  
  119.         realsize = size - zone_absent_pages_in_node(nid, j,  
  120.                                 zholes_size);  
  121.   
  122.         /* 
  123.          * Adjust realsize so that it accounts for how much memory 
  124.          * is used by this zone for memmap. This affects the watermark 
  125.          * and per-cpu initialisations 
  126.          */  
  127.         memmap_pages = /* 存放页面所需要的内存大小 */  
  128.             PAGE_ALIGN(size * sizeof(struct page)) >> PAGE_SHIFT;  
  129.         if (realsize >= memmap_pages) {  
  130.             realsize -= memmap_pages;  
  131.             if (memmap_pages)  
  132.                 printk(KERN_DEBUG  
  133.                        "  %s zone: %lu pages used for memmap\n",  
  134.                        zone_names[j], memmap_pages);  
  135.         } else  
  136.             printk(KERN_WARNING  
  137.                 "  %s zone: %lu pages exceeds realsize %lu\n",  
  138.                 zone_names[j], memmap_pages, realsize);  
  139.   
  140.         /* Account for reserved pages */  
  141.         if (j == 0 && realsize > dma_reserve) {  
  142.             realsize -= dma_reserve; /* 减去为DMA保留的页面 */  
  143.             printk(KERN_DEBUG "  %s zone: %lu pages reserved\n",  
  144.                     zone_names[0], dma_reserve);  
  145.         }  
  146.         /* 如果不是高端内存区 */  
  147.         if (!is_highmem_idx(j))  
  148.             nr_kernel_pages += realsize;  
  149.         nr_all_pages += realsize;  
  150.           
  151.         /* 下面为初始化zone结构的相关变量 */  
  152.         zone->spanned_pages = size;  
  153.         zone->present_pages = realsize;  
  154. #ifdef CONFIG_NUMA  
  155.         zone->node = nid;  
  156.         zone->min_unmapped_pages = (realsize*sysctl_min_unmapped_ratio)  
  157.                         / 100;  
  158.         zone->min_slab_pages = (realsize * sysctl_min_slab_ratio) / 100;  
  159. #endif  
  160.         zone->name = zone_names[j];  
  161.         spin_lock_init(&zone->lock);  
  162.         spin_lock_init(&zone->lru_lock);  
  163.         zone_seqlock_init(zone);  
  164.         zone->zone_pgdat = pgdat;  
  165.   
  166.         zone->prev_priority = DEF_PRIORITY;  
  167.   
  168.         zone_pcp_init(zone);  
  169.         for_each_lru(l) { /* 初始化链表 */  
  170.             INIT_LIST_HEAD(&zone->lru[l].list);  
  171.             zone->reclaim_stat.nr_saved_scan[l] = 0;  
  172.         }  
  173.         zone->reclaim_stat.recent_rotated[0] = 0;  
  174.         zone->reclaim_stat.recent_rotated[1] = 0;  
  175.         zone->reclaim_stat.recent_scanned[0] = 0;  
  176.         zone->reclaim_stat.recent_scanned[1] = 0;  
  177.         zap_zone_vm_stats(zone);  
  178.         zone->flags = 0;  
  179.         if (!size)  
  180.             continue;  
  181.         /* 需要定义相关宏 */  
  182.         set_pageblock_order(pageblock_default_order());  
  183.         /* zone中变量pageblock_flags,表示从启动分配器中进行内存申请 */  
  184.         setup_usemap(pgdat, zone, size);  
  185.         /* zone中的任务等待队列和zone的伙伴系统(MAX_ORDER个链表)的初始化 */  
  186.         ret = init_currently_empty_zone(zone, zone_start_pfn,  
  187.                         size, MEMMAP_EARLY);  
  188.         BUG_ON(ret);  
  189.         /* zone中page相关属性的初始化工作 */  
  190.         memmap_init(size, nid, j, zone_start_pfn);  
  191.         zone_start_pfn += size;  
  192.     }  
  193. }  
    分析:
    (1)free_area_init_nodes()函数用于初始化所有的节点和它们的管理区数据。它会对系统中每个活动节点(即内存簇)调用free_area_init_node(),使用add_active_range()提供的页面范围来计算各节点上每种管理区和洞的大小。如果两个相邻管理区的最大PFN相同,则表明后面这个管理区是空的。例如,如果arch_max_dma_pfn == arch_max_dma32_pfn,则表明arch_max_dma32_pfn没有页面。我们假定管理区是连续的,即后一种管理区的开始位置紧接着前一种管理区的结束位置。例如ZONE_DMA32开始于at arch_max_dma_pfn。函数先计算各种管理区的下限页面号和上限页面号,保存在两个数组中,对于连续的相邻管理区(只有ZONE_MOVABLE管理区的内存是不连续的),后一个管理区的下限页面号为前一个管理区的上限页面号。而ZONE_MOVABLE的上下限页面号均设为0。然后调用find_zone_movable_pfns_for_nodes()找出每个节点上ZONE_MOVABLE的开始PFN。
    (2)对每个节点,调用free_area_init_node(),传入参数为节点ID,各个管理区的大小,节点的开始页面号,各洞的大小。该函数先调用calculate_node_totalpages()计算节点上的所有物理页面,并保存在节点的pgdat数据结构中,从“内存描述”一节中可知,节点pg_data_t结构中保存了该节点的所有管理区数据。然后调用free_area_init_core()初始化各个zone中相关数据,包括伙伴系统、等待队列、相关变量、数据结构、链表等。
    (3)free_area_init_core()用于设置管理区的各个数据结构,包括标记管理区的所有页面,标记所有内存空队列,清除内存位图。该函数对节点上的每个管理区,计算它需要映射的真实页面数realsize(即真实内存大小),注意对DMA区这需要减去为DMA保留的页面。然后初始化该管理区的zone数据结构中的相关变量,包括总页面数、真实页面数即realsize、未映射页面数的下限(低于此值时将进行页面回收)、用于slab分配器的页面数下限、保护伙伴系统和页面回收的LRU链表的自旋锁、LRU队列初始化、页面回收状态域、用于管理区使用情况统计的vm_stats置0,等等。最后调用init_currently_empty_zone()初始化zone中的任务等待队列和伙伴系统,调用memmap_init()初始化zone中所有page的相关属性。
    3、初始化管理区分配机制
    从以上分析可以看出,setup_arch()中的内存管理初始化工作是与体系结构相关的,这里介绍的是x86 32位的情况。start_kernel()在执行完setup_arch()后即建立起永久分页机制,然后就会调用mm/page_alloc.c:build_all_zonelists()来初始化管理区分配机制,它通过对每种管理区维护一个管理区队列来实现分配和回收,因此整个初始化工作的核心就是构建所有的管理区队列。一个分配请求在zonelist数据结构上进行操作,该结构在include/linux/mmzone.h中,如下:
[cpp] view plain copy
  1. #ifdef CONFIG_NUMA  
  2. #define MAX_ZONELISTS 2  
  3.   
  4. struct zonelist_cache {  
  5.     unsigned short z_to_n[MAX_ZONES_PER_ZONELIST];      /* zone->nid */  
  6.     DECLARE_BITMAP(fullzones, MAX_ZONES_PER_ZONELIST);  /* zone full? */  
  7.     unsigned long last_full_zap;        /* when last zap'd (jiffies) */  
  8. };  
  9. #else  
  10. #define MAX_ZONELISTS 1  
  11. struct zonelist_cache;  
  12. #endif  
  13.   
  14. struct zoneref {  
  15.     struct zone *zone;  /* Pointer to actual zone */  
  16.     int zone_idx;       /* zone_idx(zoneref->zone) */  
  17. };  
  18.   
  19. struct zonelist {  
  20.     struct zonelist_cache *zlcache_ptr;          // NULL or &zlcache  
  21.     struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1];  
  22. #ifdef CONFIG_NUMA  
  23.     struct zonelist_cache zlcache;               // optional ...  
  24. #endif  
  25. };  
    从前面“内存描述”介绍中可知,zonelist在节点的pg_data_t结构中维护,以作为节点的备用内存区,当节点没有可用内存时,就从队列中分配内存。一个zonelist表示一个管理区的一个队列,队列中的第一个管理区是分配的目标,其他则为备用管理区,以优先级递减的方式存放在队列中。zonelist_cache结构缓存了每个zonelist中的一些关键信息,以便在get_page_from_freelist()中扫描可用页面时,有更小的开销。其中位图fullzones用来跟踪当前zonelist中哪些管理区开始内存不足了;数组z_to_n[]把zonelist中的每个管理区映射到它的节点id,以便我们能估计在当前进程允许的内存范围内节点是否被设置。zoneref则包含了zonelist中实际的zone信息,封装成一个结构是为了避免解引用时进入一个大的结构体内并且搜索表格。
    在zonelist中,zlcache_ptr指针用来标识是否有zlcache。如果非空,则就是zlcache的地址;如果为空,则表示没有zlcache。为了加快zonelist的读取速度,zoneref保存了要读取条目的管理区索引。include/linux/mmzone.h中定义了一些访问zoneref的函数。zonelist_zone()函数返回zoneref中的zone,zonelist_zone_idx()为一个条目返回管理区索引,zonelist_node_idx()为一个条目返回zone中的节点索引。
    mm/page_alloc.c:build_all_zonelists()函数如下,这里介绍非NUMA的情况:
[cpp] view plain copy
  1. static void zoneref_set_zone(struct zone *zone, struct zoneref *zoneref)  
  2. {  
  3.     zoneref->zone = zone;  
  4.     zoneref->zone_idx = zone_idx(zone);  
  5. }  
  6.   
  7. /* 
  8.  * 构建管理区环形分配队列,把节点上的所有管理区添加到队列中 
  9.  */  
  10. static int build_zonelists_node(pg_data_t *pgdat, struct zonelist *zonelist,  
  11.                 int nr_zones, enum zone_type zone_type)  
  12. {  
  13.     struct zone *zone;  
  14.   
  15.     BUG_ON(zone_type >= MAX_NR_ZONES);  
  16.     zone_type++;  
  17.   
  18.     do {  
  19.         zone_type--;  
  20.         zone = pgdat->node_zones + zone_type;  
  21.         if (populated_zone(zone)) { /* 如果以页面为单位的管理区的总大小不为0 */  
  22.             zoneref_set_zone(zone,  /* 将管理区添加到链表中 */  
  23.                 &zonelist->_zonerefs[nr_zones++]);  
  24.             check_highest_zone(zone_type);  
  25.         }  
  26.   
  27.     } while (zone_type);  
  28.     return nr_zones;  
  29. }  
  30.   
  31. #ifdef CONFIG_NUMA  
  32. /* ...... */  
  33. static void build_zonelists(pg_data_t *pgdat)  
  34. {  
  35.     /* ...... */  
  36. }  
  37.   
  38. static void build_zonelist_cache(pg_data_t *pgdat)  
  39. {  
  40.     /* ...... */  
  41. }  
  42.   
  43. #else   /* non CONFIG_NUMA */  
  44. /* ...... */  
  45. static void build_zonelists(pg_data_t *pgdat)  
  46. {  
  47.     int node, local_node;  
  48.     enum zone_type j;  
  49.     struct zonelist *zonelist;  
  50.   
  51.     local_node = pgdat->node_id;  
  52.   
  53.     zonelist = &pgdat->node_zonelists[0];  
  54.     /* 将zone添加到zone链表中,这样,zone中page的  
  55.        分配等操作将依靠这个环形的链表 */    
  56.     j = build_zonelists_node(pgdat, zonelist, 0, MAX_NR_ZONES - 1);  
  57.   
  58.     /* 
  59.      * Now we build the zonelist so that it contains the zones 
  60.      * of all the other nodes. 
  61.      * We don't want to pressure a particular node, so when 
  62.      * building the zones for node N, we make sure that the 
  63.      * zones coming right after the local ones are those from 
  64.      * node N+1 (modulo N) 
  65.      */  
  66.     /* 对其他在线的节点创建zonelist */  
  67.     for (node = local_node + 1; node < MAX_NUMNODES; node++) {  
  68.         if (!node_online(node))  
  69.             continue;  
  70.         j = build_zonelists_node(NODE_DATA(node), zonelist, j,  
  71.                             MAX_NR_ZONES - 1);  
  72.     }  
  73.     for (node = 0; node < local_node; node++) {  
  74.         if (!node_online(node))  
  75.             continue;  
  76.         j = build_zonelists_node(NODE_DATA(node), zonelist, j,  
  77.                             MAX_NR_ZONES - 1);  
  78.     }  
  79.   
  80.     zonelist->_zonerefs[j].zone = NULL;  
  81.     zonelist->_zonerefs[j].zone_idx = 0;  
  82. }  
  83.   
  84. /* 构建zonelist缓存:对非NUMA的zonelist信息,只是把zlcache_ptr设成NULL */  
  85. static void build_zonelist_cache(pg_data_t *pgdat)  
  86. {  
  87.     pgdat->node_zonelists[0].zlcache_ptr = NULL;  
  88. }  
  89.   
  90. #endif  /* CONFIG_NUMA */  
  91.   
  92. /* 返回int值,因为可能通过stop_machine()调用本函数 */  
  93. static int __build_all_zonelists(void *dummy)  
  94. {  
  95.     int nid;  
  96.   
  97. #ifdef CONFIG_NUMA  
  98.     memset(node_load, 0, sizeof(node_load));  
  99. #endif  
  100.     for_each_online_node(nid) {  
  101.         pg_data_t *pgdat = NODE_DATA(nid);  
  102.         /* 创建zonelists,这个队列用来在分配内存时回绕,循环访问 */  
  103.         build_zonelists(pgdat);  
  104.         /* 创建zonelist缓存信息:在非NUMA中,仅仅是把相关缓存变量设成NULL */  
  105.         build_zonelist_cache(pgdat);  
  106.     }  
  107.     return 0;  
  108. }  
  109.   
  110. void build_all_zonelists(void)  
  111. {  
  112.     /* 设置全局变量current_zonelist_order */  
  113.     set_zonelist_order();  
  114.   
  115.     /* 对所有节点创建zonelists */  
  116.     if (system_state == SYSTEM_BOOTING) { /* 系统正在引导时 */  
  117.         __build_all_zonelists(NULL);    
  118.         mminit_verify_zonelist();  /* 调试用 */  
  119.         cpuset_init_current_mems_allowed();  
  120.     } else {  
  121.         /* 非引导时要停止所有cpu以确保没有使用zonelist */  
  122.         stop_machine(__build_all_zonelists, NULL, NULL);  
  123.         /* cpuset refresh routine should be here */  
  124.     }  
  125.     /* 计算所有zone中可分配的页面数之和 */  
  126.     vm_total_pages = nr_free_pagecache_pages();  
  127.     /* 
  128.      * Disable grouping by mobility if the number of pages in the 
  129.      * system is too low to allow the mechanism to work. It would be 
  130.      * more accurate, but expensive to check per-zone. This check is 
  131.      * made on memory-hotadd so a system can start with mobility 
  132.      * disabled and enable it later 
  133.      */  
  134.     if (vm_total_pages < (pageblock_nr_pages * MIGRATE_TYPES))  
  135.         page_group_by_mobility_disabled = 1;  
  136.     else  
  137.         page_group_by_mobility_disabled = 0;  
  138.   
  139.     printk("Built %i zonelists in %s order, mobility grouping %s.  "  
  140.         "Total pages: %ld\n",  
  141.             nr_online_nodes,  
  142.             zonelist_order_name[current_zonelist_order],  
  143.             page_group_by_mobility_disabled ? "off" : "on",  
  144.             vm_total_pages);  
  145. #ifdef CONFIG_NUMA  
  146.     printk("Policy zone: %s\n", zone_names[policy_zone]);  
  147. #endif  
  148. }  
    分析:
    (1)build_all_zonelists()调用__build_all_zonelists()来构建所有管理区队列。如果是系统引导时,则直接调用__build_all_zonelists()对所有节点创建zonelist;如果不是引导时,则要通过stop_machine()来调用__build_all_zonelists(),先停止所有CPU以确保没有使用zonelist。然后用nr_free_pagecache_pages()计算所有zone中可分配的页面总数,如果页面总数太小,则禁用页面分组移动功能(因为这个性能开销比较大)。
    (2)在__build_all_zonelists()中,对每个在线节点,调用build_zonelists()创建管理区分配的环形队列,调用build_zonelist_cache()创建队列的缓存信息。这两个函数有NUMA版本和非NUMA版本,这里略去NUMA版本,只介绍非NUMA版本。在build_zonelists()中,对每个在线节点,调用build_zonelists_node()构建环形分配队列,把节点上的所有管理区添加到队列中。在build_zonelist_cache()中,对非NUMA的zonelist信息,只是把zlcache_ptr设成NULL。
    (3)在build_zonelists_node()中,通过zoneref_set_zone()将每个产生的管理区添加到队列中。
    从以上分析可知,内存管理区初始化主要是借助于引导分配器和已初始化的e820全局变量。内存管理区初始化后相应的伙伴系统、slab机制等等就可以在此基础上建立了。


原创粉丝点击