6.2.3 物理内存的描述

   为了对内存的初始化内容进行进一步的讨论,我们首先要了解Linux对物理内存的描述机制。

1.一致存储结构(UMA)和非一致存储结构(NUMA

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

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

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

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

Linux把物理内存划分为三个层次来管理:存储节点(Node)、管理区(Zone)和页面(Page),并用三个相应的数据结构来描述。

2.页面数据结构page

   对一个物理页面的描述在/include/linux/mm.h中:

/*

  * Each physical page in the system has a struct page associated with

  * it to keep track of whatever it is we are using the page for at the

  * moment. Note that we have no way to track which tasks are using

  * a page.

  *

  * Try to keep the most commonly accessed fields in single cache lines

  * here (16 bytes or greater).  This ordering should be particularly

  * beneficial on 32-bit processors.

  *

  * The first line is data used in page cache lookup, the second line

  * is used for linear searches (eg. clock algorithm scans).

  *

  * TODO: make this structure smaller, it could be as small as 32 bytes.

  */

 

typedef struct page {

        struct list_head list;          /* ->mapping has some page lists. */

         struct address_space *mapping;  /* The inode (or ...) we belong to. */

         unsigned long index;            /* Our offset within mapping. */

         struct page *next_hash;         /* Next page sharing our hash bucket in

                                            the pagecache hash table. */

         atomic_t count;                 /* Usage count, see below. */

         unsigned long flags;            /* atomic flags, some possibly

                                            updated asynchronously */

         struct list_head lru;           /* Pageout list, eg. active_list;

                                            protected by pagemap_lru_lock !! */

         wait_queue_head_t wait;         /* Page locked?  Stand in line... */

         struct page **pprev_hash;       /* Complement to *next_hash. */

         struct buffer_head * buffers;   /* Buffer maps us to a disk block. */

         void *virtual;                  /* Kernel virtual address (NULL if

                                            not kmapped, ie. highmem) */

         struct zone_struct *zone;       /* Memory zone we are in. */

} mem_map_t;

extern mem_map_t * mem_map

源代码的注释中对这个数据结构给出了一定的说明,从中我们可以对此结构有一定的理解,后面还要对此结构中的每个域给出具体的解释。

内核中用来表示这个数据结构的变量常常是pagemap

当页面的数据来自一个文件时,index代表着该页面中的数据在文件中的偏移量;当页面的内容被换出到交换设备上,则index指明了页面的去向。结构中各个成分的次序是有讲究的,尽量使得联系紧密的若干域存放在一起,这样当这个数据结构被装入到高速缓存中时,联系紧密的域就可以存放在同一缓冲行(cache line)中。因为同一缓冲行(其大小为16字节)中的内容几乎可以同时存取,因此,代码注释中希望这个数据结构尽量地小到用32个字节可以描述。

系统中的每个物理页面都有一个page(或mem_map_t)结构。系统在初始化阶段根据内存的大小建立起一个page结构的数组mem_map,数组的下标就是内存中物理页面的序号。

3. 管理区zone

为了对物理页面进行有效的管理,Linux又把物理页面划分为三个区:

·      专供DMA使用的ZONE_DMA区(小于16BM

·      常规的ZONE_NORMAL区(大于16MB小于896MB

·      内核不能直接映射的区ZONE_HIGME区(大于896MB)。

这里进一步说明为什么对DMA要单独设置管理区。首先,DMA使用的页面是磁盘IO所需的,如果在页面的分配过程中,所有的页面全被分配完,那么页面及盘区的交换就无法进行了,这是操作系统决不允许出现的现象。另外,在i386CPU中,页式存储管理的硬件支持是在CPU内部实现的,而不像有些CPU那样由一个单独的MMU来提供,所以DMA对内存的访问不经过MMU提供的地址映射。这样,外部设备就要直接访问物理页面的地址。可是,有些外设(特别是插在ISA总线上的外设接口卡)在这方面往往有些限制,要求用于DMA的物理地址不能过高。另一方面,当DMA所需的缓冲区超过一个物理页面的大小时,就要求两个物理页面在物理上是连续的,但因为此时DMA控制器不能依靠CPU内部的MMU将连续的虚存页面映射到物理上也连续的页面上,因此,用于DMA的物理页面必须加以单独管理。

   关于管理区的数据结构zone_struct(zone_t)将在后面进行描述。

4.存储节点的数据结构

  存储节点的数据结构为pglist_data,定义于Include/linux/mmzone.h中:

  typedef struct pglist_data {

            zone_t node_zones[MAX_NR_ZONES];

            zonelist_t node_zonelists[GFP_ZONEMASK+1];

            int nr_zones;

            struct page *node_mem_map;

            unsigned long *valid_addr_bitmap;

            struct bootmem_data *bdata;

            unsigned long node_start_paddr;

            unsigned long node_start_mapnr;

            unsigned long node_size;

            int node_id;

            struct pglist_data *node_next;

       } pg_data_t;

   显然,若干存储节点的pglist_data数据结构可以通过node_next形成一个单链表队列。每个结构中的node_mem_map指向具体节点的page结构数组,而数组node_zone[]就是该节点的最多三个页面管理区。

  pglist_data结构里设置了一个node_zonelists数组,其类型定义也在同一文件中:

   typedef struct zonelist_struct {

        zone_t *zone[MAX_NR_ZONE+1];  //NULL delimited

        Int gfp_mast;

      } zonelist_t

这里的zone[]是个指针数组,各个元素按特定的次序指向具体的页面管理区,表示分配页面时先试zone[0]所指向的管理区,如果不能满足要求就试zone[1]所指向的管理区,等等。这些管理区可以属于不同的存储节点。关于管理区的分配可以有很多种策略,例如,CPU模块1需要分配5个用于DMA的页面,可是它的ZONE_DMA只有三个页面,于是就从公用模块的ZONE_DMA中分配全部5个页面。就是说,每个zonelist_t规定了一种分配策略。然而,每个存储节点不应该只有一种分配策略,所以在pglist_data中提供的是一个zonelist_t数组,数组的大小NR_GFPINDEX100