Linux内存管理

1.进程页表项中的内核部分是各进程复制的,并且采取了延迟更新的方式:

以vmalloc为例(最常使用),这部分区域对应的线性地址在内核使用vmalloc分配内存时,其实就已经分配了相应的物理内存,并做了相应的映射,建立了相应的页表项,但相关页表项仅写入了“内核页表”,并没有实时更新到“进程页表中”,内核在这里使用了“延迟更新”的策略,将“进程页表”真正更新推迟到第一次访问相关线性地址,发生page fault时,此时在page fault的处理流程中进行“进程页表”的更新:

2.进程的页表和页目录存储在内核空间还是用户空间?

A. 如果页表、页目录都在在内核空间的低端内存中,那么:
内核通过cr3能获得全局页目录中的物理地址,由于低端内存的线性映射,内核就能据此算出页目录的虚拟地址,进而实现对页目录的读写;同理,根据页目录中的内容可以获得页表的物理地址,如果页表也在内核空间的低端内存中,那么根据线性映射的偏移也能算出页表的虚拟地址,这样就能对页表进行读写。看似行的通,但是,
如果所有进程的页表都存放在低端内存,那进程数量很多时低端内存肯定不堪重负。

B. 如果页表在用户空间(或在内核空间的非线性映射区):
即使通过页目录读取到了页表的物理地址,由于内核只能通过虚拟地址对实际的内存进行访问,所以内核还是无法对页表中的内容进行读写,因为内核不知道页表的虚拟地址(这时已经不能用物理地址减去一个偏移量来计算虚拟地址了)。
通过上面的代码可知,当获取到一个页目录描述符dir后,可以在不依赖内存的线性映射条件下对页表的内容(页描述符)进行访问(关键在于获取其虚拟地址),如楼上所说,内核提供了三种方式来实现page到虚拟地址的映射,只有第三种方式是依赖线性映射的。
所以,页表没有必要放到内核空间的低端内存中。
但是对于页目录,似乎必须放在内核空间。
// 全局页目录、二级页目录和三级页目录的访问(即获取其虚拟地址)都依赖地址的线性映射,因此课件
// 这三级目录表都应放在内核空间的低端内存中。只有最后一级目录即页表本身可以不在低端内存中。



因为每一个外设都是通过总线(内部总线+外部总线)与处理器建立联系,每个外设在内存中都用规定的地址,处理器通过地址,即可与外设取的联系。

同时外设有自己寄存器(控制寄存器+状态寄存器+数据寄存器),也叫“I/O端口”,其寄存器也有地址,在使用外设之前,必须对外设寄存器进行相关配置,这是寄存器开发。

CPU对外设IO端口物理地址的编址方式 有两种:
一种是I/O映射方式(I/Omapped)称为端口映射。(X86)
     这是一个与CPU地RAM物理地址空间不同的地址空间,所有外设的I/O端口均在这一空间中进行编址。CPU通过设立专 门的I/O指令(如X86的IN和OUT指令)来访问这一空间中的地址单元(也即I/O端口)。这就是所谓的"I/O映射方式"(I/O-mapped),也就是“独立编址”。

另一种是存储空间映射方式(Memorymapped),称为内存映射。ARM体系的CPU均采用这一模式
      有些体系结构的CPU(如,PowerPC、m68k等)通常只实现一个物理地址空间(RAM)。在这种情况下,外设I/O端口的物理地址就被映射到 CPU的单一物理地址空间中,而成为内存的一部分,也就是所谓的“统一编址”,这样,CPU就可以像访问一个内存单元那样访问外设I/O端口(理解为I/O寄存器),而不需要设立专门的外设I/O指令。(统一编址

而具体采用哪一种则取决于CPU的体系结构。

现在流行的一种开发方式是库函数开发,因为寄存器的数量多,功能复杂,直接去配置必须对寄存器每个位的功能了解。所以将寄存器的使用直接封装成可以调用的函数,并提供相应的参数,在以后采用库函数开发时,不需要去查阅数据手册中的寄存器说明,直接调用相应的库函数即可。
CPU与外设交流的方式有两种:轮询和中断,前者为主动,后者为被动。


  存储器顺序:寄存器→Cache→内存→外存
Linux内存管理_第1张图片

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<
  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<
  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   
  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   
  107. #include   
  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<
[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</* 这里保证了运用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<
  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<
  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<"1G":(  
  143.              (mr[i].page_size_mask & (1<"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<
  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<
  96.         mr[nr_range].end   = end_pfn<
  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<
  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<
  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<
  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<
  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表中的索引,注意内核要从页目录表中第

你可能感兴趣的:(linux编程基础)