ASM:Linux内存管理模型(1)

  既然学习了汇编,我一直想找现代操作系统的内存管理模型的文章看看,在网上看到一个写的挺详细的是以Linux为基础的,就转过来了。

  来源:http://blog.csdn.net/zhoudaxia/article/details/7880359

     http://blog.csdn.net/zhoudaxia/article/details/7880475

     http://blog.csdn.net/zhoudaxia/article/details/7880421

PART1:内存模型

  Linux使用的是单一整体式结构(Monolithic),其中定义了一组原语或系统调用以实现操作系统的服务,例如在几个模块中以超级模式运行的进程管理、并发控制和内存管理服务。尽管出于兼容性考虑,Linux依然将段控制单元模型(segment control unit model)保持一种符号表示,但实际上已经很少使用这种模型了。
    与内存管理有关的主要问题有:
    * 虚拟内存的管理,这是介于应用程序请求与物理内存之间的一个逻辑层。
    * 物理内存的管理。
    * 内核虚拟内存的管理/内核内存分配器,这是一个用来满足对内存的请求的组件。这种对内存的请求可能来自于内核,也可能来自于用户。
    * 虚拟地址空间的管理。
    * 交换和缓存。


    1. 分段模型概述(这个和我学的教材是一样的)
  在x86架构中,内存被划分成3种类型的地址:
  逻辑地址(logical address)是存储位置的地址,它可能直接对应于一个物理位置,也可能不直接对应于一个物理位置。逻辑地址通常在请求控制器中的信息时使用。
  线性地址(linear address)(或称为平面地址空间)是从0开始进行寻址的内存。之后的每个字节都可顺序使用下一数字来引用(0、1、2、3 等),直到内存末尾为止。这就是大部分非Intel CPU的寻址方式。Intel®架构使用了分段的地址空间,其中内存被划分成64KB的段,有一个段寄存器总是指向当前正在寻址的段的基址。这种架构中的32位模式被视为平面地址空间,不过它也使用了段。
  物理地址(physical address)是使用物理地址总线中的位表示的地址。物理地址可能与逻辑地址不同,内存管理单元可以将逻辑地址转换成物理地址。CPU使用两种单元将逻辑地址转换成物理地址。第一种称为分段单元(segmented unit),另外一种称为分页单元(paging unit)。 下图表示转换地址空间使用的两种单元。

  

  分段模型背后的基本思想是将内存分段管理。从本质上来说,每个段就是自己的地址空间。段由两个元素构成:基址(base address)包含某个物理内存位置的地址;长度值(length value)指定该段的长度。分段地址还包括两个组件,即段选择器(segment selector)和段内偏移量(offset into the segment)。段选择器指定了要使用的段(即基址和长度值),而段内偏移量组件则指定了实际内存位置相对于基址的偏移量。实际内存位置的物理地址就是这个基址值与偏移量之和。如果偏移量超过了段的长度,系统就会生成一个保护违例错误。上述内容可小结如下:分段单元可以表示成 -> 段: 偏移量 模型,也也可表示成 -> 段标识符: 偏移量。
  每个段都是一个16位的字段,称为段标识符(segment identifier)或段选择器(segment selector)。x86硬件包括几个可编程的寄存器,称为段寄存器(segment register),段选择器保存于其中。这些寄存器为 cs(代码段)、ds(数据段)和 ss(堆栈段)。每个段标识符都代表一个使用 64 位(8 个字节)的段描述符 (segment descriptor) 表示的段。这些段描述符可以存储在一个 GDT(全局描述符表,global descriptor table)中,也可以存储在一个 LDT(本地描述符表,local descriptor table)中。下图表示段描述符和段寄存器的相互关系。

  每次将段选择器加载到段寄存器中时,对应的段描述符都会从内存加载到相匹配的不可编程CPU寄存器中。每个段描述符长8个字节,表示内存中的一个段。这些都存储到LDT或GDT中。段描述符条目中包含一个指针和一个20位的值(Limit字段),前者指向由Base字段表示的相关段中的第一个字节,后者表示内存中段的大小。其他某些字段还包含一些特殊属性,例如优先级和段的类型(cs 或 ds)。段的类型是由一个4位的Type字段表示的。由于我们使用了不可编程寄存器,因此在将逻辑地址转换成线性地址时不引用GDT或LDT。这样可以加快内存地址的转换速度。
    段选择器包含以下内容:
    * 一个13位的索引,用来标识GDT或LDT中包含的对应段描述符条目。
    * TI(Table Indicator)标志指定段描述符是在GDT中还是在LDT中,如果该值是0,段描述符就在GDT中;如果该值是1,段描述符就在LDT中。
    * RPL(request privilege level)定义了在将对应的段选择器加载到段寄存器中时CPU的当前特权级别。
    由于一个段描述符的大小是8个字节,因此它在GDT或LDT中的相对地址可以这样计算:段选择器的高13位乘以8。例如,如果GDT存储在地址0x00020000处,而段选择器的Index域是2,那么对应的段描述符的地址就等于(2*8) + 0x00020000。GDT中可以存储的段描述符的总数等于 (2^13 - 1),即8191。下图展示了从逻辑地址获得线性地址。

  

   Linux对这个模型稍微进行了修改。我注意到Linux以一种受限的方法来使用这种分段模型(主要是出于兼容性方面的考虑)。在Linux中,所有的段寄存器都指向相同的段地址范围。换言之,每个段寄存器都使用相同的线性地址。这使Linux所用的段描述符数量受限,从而可将所有描述符都保存在GDT之中。这种模型有两个优点:一是当所有的进程都使用相同的段寄存器值时(当它们共享相同的线性地址空间时),内存管理更为简单。二是在大部分架构上都可以实现可移植性。某些RISC处理器也可通过这种受限的方式支持分段。下图展示了对模型的修改(段寄存器指向相同的地址集)。

  

  Linux使用以下段描述符:内核代码段、内核数据段、用户代码段、用户数据段、TSS段、默认LDT段。下面详细介绍这些段寄存器。
  GDT中的内核代码段(kernel code segment)描述符中的值如下:
      * Base = 0x00000000
      * Limit = 0xffffffff(2^32 -1) = 4GB
      * G(粒度标志)= 1,表示段的大小是以页为单位表示的
      * S = 1,表示普通代码或数据段
      * Type = 0xa,表示可以读取或执行的代码段
      * DPL值 = 0,表示内核模式
  与这个段相关的线性地址是4 GB,S = 1和type = 0xa表示代码段。选择器在cs寄存器中。Linux 中用来访问这个段选择器的宏是_KERNEL_CS。
  内核数据段(kernel data segment)描述符的值与内核代码段的值类似,惟一不同的就是Type字段值为 2。这表示此段为数据段,选择器存储在ds寄存器中。Linux中用来访问这个段选择器的宏是_KERNEL_DS。
  用户代码段(user code segment)由处于用户模式中的所有进程共享。存储在GDT中的对应段描述符的值如下:
      * Base = 0x00000000
      * Limit = 0xffffffff
      * G = 1
      * S = 1
      * Type = 0xa,表示可以读取和执行的代码段
      * DPL = 3,表示用户模式
  在Linux中,我们可以通过_USER_CS宏来访问此段选择器。
  在用户数据段(user data segment)描述符中,惟一不同的字段就是Type,它被设置为2,表示将此数据段定义为可读取和写入。Linux中用来访问此段选择器的宏是_USER_DS。
  除了这些段描述符之外,GDT还包含了另外两个用于每个创建的进程的段描述符:TSS和LDT段。每个TSS段(TSS segment)描述符都代表一个不同的进程。TSS中保存了每个CPU的硬件上下文信息,它有助于有效地切换上下文。例如,在U->K模式的切换中,x86 CPU就是从TSS中获取内核模式堆栈的地址。每个进程都有自己在 GDT 中存储的对应进程的 TSS 描述符。这些描述符的值如下:
      * Base = &tss(对应进程描述符的TSS字段的地址;例如 &tss_struct)这是在Linux内核的schedule.h文件中定义的
      * Limit = 0xeb(TSS段的大小是236字节)
      * Type = 9或11
      * DPL = 0。用户模式不能访问TSS。G标志被清除
  所有进程共享默认LDT段。默认情况下,其中会包含一个空的段描述符。这个默认LDT段描述符存储在GDT中。Linux所生成的LDT的大小是24个字节。默认有3个条目:LDT[0] = 空;LDT[1] = 用户代码段;LDT[2] = 用户数据/堆栈段描述符。
  为了计算GDT中最多可以存储多少条目,必须先理解NR_TASKS,这个变量决定了Linux可支持的并发进程数,内核源代码中的默认值是512,最多允许有256个到同一实例的并发连接。GDT中可存储的条目总数可通过以下公式确定:
  GDT中的条目数 = 12 + 2 * NR_TASKS。
  正如前所述,GDT可以保存的条目数 = 2^13 -1 = 8192。在这8192个段描述符中,Linux要使用6个段描述符,另外还有4个描述符将用于APM特性(高级电源管理特性),在GDT中还有4个条目保留未用。因此,GDT中的条目数等于8192 - 14,也就是8180。
  任何情况下,GDT中的条目数8180,因此:2 * NR_TASKS = 8180,NR_TASKS = 8180/2 = 4090。为什么使用2 * NR_TASKS?因为对于所创建的每个进程,都不仅要加载一个TSS描述符用来维护上下文切换的内容,另外还要加载一个LDT描述符。这种 x86架构中进程数量的限制Linux 2.2中的一个组件,但自2.4版的内核开始,这个问题已经不存在了,部分原因是使用了硬件上下文切换(这不可避免地要使用TSS),并将其替换为进程切换。 
  2、分页模型概述
  分页单元负责将线性地址转换成物理地址(请参见图 1)。线性地址会被分组成页的形式。这些线性地址实际上都是连续的。分页单元将这些连续的内存映射成对应的连续物理地址范围(称为页框)。注意,分页单元会直观地将RAM划分成固定大小的页框。正因如此,分页具有以下优点:为一个页定义的访问权限中保存了构成该页的整组线性地址的权限;页的大小等于页框的大小。
  将这些页映射成页框的数据结构称为页表(page table)。页表存储在主存储器中,可由内核在启用分页单元之前对其进行恰当的初始化。下图展示了页表映射到页框的情况。

  在Linux中,分页单元的使用多于分段单元。前面介绍Linux分段模型时已提到,每个分段描述符都使用相同的地址集进行线性寻址,从而尽可能降低使用分段单元将逻辑地址转换成线性地址的需要。通过更多地使用分页单元而非分段单元,Linux可以极大地促进内存管理及其在不同硬件平台之间的可移植性。  
  下面让我们来介绍一下用于在x86架构中指定分页的字段,这些字段有助于在Linux中实现分页功能。分页单元进入作为分段单元输出结果的线性字段,然后进一步将其划分成以下3个字段:
      * Directory 以10 MSB表示(Most Significant Bit,也就是二进制数字中值最大的位的位置 —— MSB 有时称为最左位)。
      * Table 以中间的10位表示。
      * Offset 以12 LSB表示。(Least Significant Bit,也就是二进制整数中给定单元值的位的位置,即确定这个数字是奇数还是偶数。LSB有时称为最右位。这与数字权重最轻的数字类似,它是最右边位置处的数字。)
  线性地址到对应物理位置的转换的过程包含两个步骤。第一步使用了一个称为页目录 (Page Directory) 的转换表(从页目录转换成页表),第二步使用了一个称为页表 (Page Table) 的转换表(即页表加偏移量再加页框)。下图展示了此过程。

  开始时,首先将页目录的物理地址加载到cr3寄存器中。线性地址中的Directory字段确定页目录中指向恰当的页表条目。Table字段中的地址确定包含页的页框物理地址所在页表中的条目。Offset字段确定了页框中的相对位置。由于Offset字段为12位,因此每个页中都包含有4 KB数据。
  下面小结物理地址的计算:
      (1) cr3 + Page Directory (10 MSB) = 指向 table_base
      (2) table_base + Page Table (10 中间位) = 指向 page_base
      (3) page_base + Offset = 物理地址 (获得页框)
  由于Page Directory字段和Page Table段都是10位,因此其可寻址上限为 1024*1024 KB,Offset可寻址的范围最大为2^12(4096 字节)。因此,页目录的可寻址上限为1024*1024*4096(等于2^32个内存单元,即4 GB)。因此在x86架构上,总可寻址上限是4 GB。
  对于扩展分页,则是通过删除页表转换表实现的;此后线性地址的划分即可在页目录 (10 MSB) 和偏移量(22 LSB)之间完成了。22 LSB构成了页框的4 MB边界(2^22)。扩展分页可以与普通的分页模型一起使用,并可用于将大型的连续线性地址映射为对应的物理地址。操作系统中删除页表以提供扩展页表。这可以通过设置PSE(page size extension)实现。36位的PSE扩展了36位的物理地址,可以支持4 MB页,同时维护一个4字节的页目录条目,这样就可以提供一种对超过4 GB的物理内存进行寻址的方法,而不需要对操作系统进行太大的修改。这种方法对于按需分页来说具有一些实际的限制。
  虽然Linux中的分页模型与普通的分页类似,但是 x86 架构引入了一种三级页表机制,包括:
      * 页全局目录 (Page Global Directory),即pgd,是多级页表的抽象最高层。每一级的页表都处理不同大小的内存 —— 这个全局目录可以处理4 MB的区域。每项都指向一个更小目录的低级表,因此pgd就是一个页表目录。当代码遍历这个结构时(有些驱动程序就要这样做),就称为是在“遍历”页表。
      * 页中间目录 (Page Middle Directory),pmd,是页表的中间层。在x86架构上,pmd在硬件中并不存在,但是在内核代码中它是与pgd合并在一起的。
      * 页表条目 (Page Table Entry),即pte,是页表的最低层,它直接处理页(参看PAGE_SIZE),该值包含某页的物理地址,还包含了说明该条目是否有效及相关页是否在物理内存中的位。
  为了支持大内存区域,Linux也采用了这种三级分页机制。在不需要为大内存区域时,即可将pmd定义成“1”,返回两级分页机制。注意分页级别是在编译时进行优化的,我们可以通过启用或禁用中间目录来启用两级和三级分页(使用相同的代码)。32位处理器使用的是pmd分页,而64位处理器使用的是pgd分页。下图说明三级分页的情况。

  如您所知,在64位处理器中,21 MSB保留未用;13 LSB由页面偏移量表示;其余的30位分为10位用于页表,10位用于页全局目录,10位用于页中间目录。我们可以从架构中看到,实际上使用了43位进行寻址。因此在64位处理器中,可以有效使用的内存是2的43次方。
  每个进程都有自己的页目录和页表。为了引用一个包含实际用户数据的页框,操作系统(在x86架构上)首先将pgd加载到cr3寄存器中。Linux将cr3寄存器的内容存储到TSS段中。此后只要在CPU上执行新进程,就从TSS段中将另外一个值加载到cr3寄存器中。从而使分页单元引用一组正确的页表。pgd表中的每一条目都指向一个页框,其中中包含了一组pmd条目;pmd表中的每个条目又指向一个页框,其中包含一组pte条目;pde表中的每个条目再指向一个页框,其中包含的是用户数据。如果正在查找的页已转出,那么就会在pte表中存储一个交换条目,(在缺页的情况下)以定位将哪个页框重新加载到内存中。图8说明我们连续为各级页表添加偏移量来映射对应的页框条目。我们通过进入作为分段单元输出的线性地址,再划分该地址来获得偏移量。要将线性地址划分成对应的每个页表元素,需要在内核中使用不同的宏。这里不详细介绍这些宏,下面我们通过下图来简单看一下线性地址的划分方式。 

  

  Linux为内核代码和数据结构预留了几个页框。这些页永远不会被转出到磁盘上。从0x0到0xc0000000(PAGE_OFFSET)的线性地址可由用户代码和内核代码进行引用。从PAGE_OFFSET到0xffffffff的线性地址只能由内核代码进行访问。这意味着在4 GB 的内存空间中,只有3 GB可以用于用户应用程序。
  Linux进程启用分页机制包括两个阶段:在启动时,系统为8 MB的物理内存设置页表。然后,第二个阶段完成对其余物理地址的映射。在启动阶段,startup_32()调用(是32位内核的入口函数,也称为进程0)负责对分页机制进行初始化。这是在arch/x86/kernel/head_32.S文件中实现的。这8 MB的映射发生在PAGE_OFFSET之上的地址中。这种初始化是通过一个静态定义的编译时数组 (swapper_pg_dir) 开始的。在编译时它被放到一个特定的地址(0x00101000)。这种操作为在代码中静态定义的两个页即pg0和pg1建立页表。这些页框的大小默认为4KB,除非我们设置了页大小扩展位(即扩展分页)。这个全局数组所指向的数据地址存储在cr3寄存器中,这是为Linux进程设置分页单元的第一阶段。其余的页项是在第二阶段中完成的。第二阶段由方法调用paging_init()来完成。
  在32位的x86架构上,RAM映射到PAGE_OFFSET和由4GB上限 (0xFFFFFFFF) 表示的地址之间。这意味着大约有1 GB的RAM可以在Linux启动时进行映射,这种操作是默认进行的。然而,如果有人设置了HIGHMEM_CONFIG,那么就可以将超过1 GB的内存映射到内核上,切记这是一种临时的安排。可以通过调用kmap()实现。
  上面已经展示了(32位架构上的) Linux内核按照3:1的比率来划分虚拟内存:3 GB的虚拟内存用于用户空间,1 GB的内存用于内核空间。内核代码及其数据结构都必须位于这1 GB的地址空间中,但是对于此地址空间而言,更大的消费者是物理地址的虚拟映射。之所以出现这种问题,是因为若一段内存没有映射到自己的地址空间中,那么内核就不能操作这段内存。因此,内核可以处理的最大内存总量就是可以映射到内核的虚拟地址空间减去需要映射到内核代码本身上的空间。结果,一个基于x86的Linux系统最大可以使用略低于1 GB的物理内存。
  为了迎合大量用户的需要,支持更多内存、提高性能,并建立一种独立于架构的内存描述方法,Linux内存模型就必须进行改进。为了实现这些目标,新模型将内存划分成分配给每个CPU的空间。每个空间都称为一个节点;每个节点都被划分成一些区域。区域(表示内存中的范围)可以进一步划分为以下类型:
      ZONE_DMA(0-16 MB):包含ISA/PCI设备需要的低端物理内存区域中的内存范围。
      ZONE_NORMAL:用户空间可用的正常内存。
      ZONE_HIGHMEM:由内核直接映射到高端范围的物理内存。只能由内核访问,用户空间访问不到。所有的内核操作都只能使用这个内存区域来进行,因此这是对性能至关重要的区域。
  节点的概念在内核中是使用struct pglist_data结构来实现的。区域是使用struct zone_struct结构来描述的。物理页框是使用struct page结构来表示的,所有这些struct都保存在全局结构数组struct mem_map 中,这个数组存储在 ZONE_NORMAL的开头。节点、区域和页框之间的基本关系如下图所示。 

  当实现了对Pentium II的虚拟内存扩展的支持(在32位系统上使用PAE——Physical Address Extension——可以访问64 GB的内存)和对4 GB的物理内存(同样是在32位系统上)的支持时,高端内存区域就会出现在内核内存管理中了。这是在x86和SPARC平台上引用的一个概念。通常这4 GB的内存可以通过使用kmap()将ZONE_HIGHMEM映射到ZONE_NORMAL来进行访问。请注意在32位的架构上使用超过16 GB的内存是不明智的,即使启用了PAE也是如此。PAE是Intel提供的内存地址扩展机制,它通过在宿主操作系统中使用Address Windowing Extensions API为应用程序提供支持,从而让处理器将可以用来寻址物理内存的位数从32位扩展为36位。
  这个物理内存区域的管理是通过一个 区域分配器(zone allocator)实现的。它负责将内存划分为很多区域;它可以将每个区域作为一个分配单元使用。每个特定的分配请求都利用了一组区域,内核可以从这些位置按照从高到低的顺序来进行分配。例如:
  * 对于某个用户页面的请求可以首先从“普通”区域中来满足(ZONE_NORMAL);
  * 如果失败,就从ZONE_HIGHMEM开始尝试;
  * 如果这也失败了,就从ZONE_DMA开始尝试。
  这种分配的区域列表依次包括ZONE_NORMAL、ZONE_HIGHMEM和ZONE_DMA区域。另一方面,对于DMA页的请求可能只能从DMA区域中得到满足,因此这种请求的区域列表就只包含DMA区域。 

PART2:内存描述

  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、节点:pg_data_t

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

 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中。如下:

 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类型用于适应大块连续内存的分配。

  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中,如下: 

 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的指针、文件中的位置(如果是文件映射)、状态跟踪指针等。

1 #ifndef CONFIG_DISCONTIGMEM
2 /* 物理页数组,对discontigmem使用pgdat->lmem_map */
3 extern struct page *mem_map;
4 #endif

  这个数组保存了所有的物理页page结构,它存储在ZONE_NORMAL内存区域的开头,用于跟踪所有的物理页面。

PART3:内存探测与初始化 

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()函数检测系统物理内存。如下:

 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中,如下:

 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()的代码,其它代码基本一样。

 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中,如下:

 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中,如下:

 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(),如下:

 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()函数,如下:

 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内核中用于建立内存管理框架的基础。例如建立初始化页表映射、管理区等都会用到它。

 

 

posted @ 2016-04-03 11:05  PhiliAI  阅读(496)  评论(0)    收藏  举报