首发于术道经纬
Linux中的物理内存管理 [一]

Linux中的物理内存管理 [一]

前面的文章介绍了Linux中虚拟地址空间的管理,本文将讨论Linux系统对物理内存的管理。

NUMA

所谓物理内存,就是安装在机器上的,实打实的内存设备(不包括硬件cache),被CPU通过总线访问。在多核系统中,如果物理内存对所有CPU来说没有区别,每个CPU访问内存的方式也一样,则这种体系结构被称为Uniform Memory Access(UMA)。

如果物理内存是分布式的,由多个cell组成(比如每个核有自己的本地内存),那么CPU在访问靠近它的本地内存的时候就比较快,访问其他CPU的内存或者全局内存的时候就比较慢,这种体系结构被称为Non-Uniform Memory Access(NUMA)。

以上是硬件层面上的NUMA,而作为软件层面的Linux,则对NUMA的概念进行了抽象。即便硬件上是一整块连续内存的UMA,Linux也可将其划分为若干的node。同样,即便硬件上是物理内存不连续的NUMA,Linux也可将其视作UMA。

所以,在Linux系统中,你可以基于一个UMA的平台测试NUMA上的应用特性。从另一个角度,UMA就是只有一个node的特殊NUMA,所以两者可以统一用NUMA模型表示。

在NUMA系统中,当Linux内核收到内存分配的请求时,它会优先从发出请求的CPU本地或邻近的内存node中寻找空闲内存,这种方式被称作local allocation,local allocation能让接下来的内存访问相对底层的物理资源是local的。

每个node由一个或多个zone组成(我们可能经常在各种对虚拟内存和物理内存的描述中迷失,但以后你见到zone,就知道指的是物理内存),每个zone又由若干page frames组成(一般page frame都是指物理页面)。

Page Frame

虽然内存访问的最小单位是byte或者word,但MMU是以page为单位来查找页表的,page也就成了Linux中内存管理的重要单位。包括换出(swap out)、回收(relcaim)、映射等操作,都是以page为粒度的。

因此,描述page frame的struct page自然成为了内核中一个使用频率极高,非常重要的结构体,来看下它是怎样构成的(为了讲解需要并非最新内核代码):

struct page {
    unsigned long flags;
    atomic_t count;  
    atomic_t _mapcount; 
    struct list_head lru;
    struct address_space *mapping;
    unsigned long index;         
    ...  
} 
  • flags表示page frame的状态或者属性,包括和内存回收相关的PG_active, PG_dirty, PG_writeback, PG_reserved, PG_locked, PG_highmem等。其实flags是身兼多职的,它还有其他用途,这将在下文中介绍到。
  • count表示引用计数。当count值为0时,该page frame可被free掉;如果不为0,说明该page正在被某个进程或者内核使用,调用page_count()可获得count值。
  • _mapcount表示该page frame被映射的个数,也就是多少个page table entry中含有这个page frame的PFN
  • lru是"least recently used"的缩写,根据page frame的活跃程度(使用频率),一个可回收的page frame要么挂在active_list双向链表上,要么挂在inactive_list双向链表上,以作为页面回收的选择依据,lru中包含的就是指向所在链表中前后节点的指针(参考这篇文章)。
  • 如果一个page是属于某个文件的(也就是在page cache中),则mapping指向文件inode对应的address_space(这个结构体虽然叫address_space,但并不是进程地址空间里的那个address space),index表示该page在文件内的offset(以page size为单位)。

有了文件的inode和index,当这个page的内容需要和外部disk/flash上对应的部分同步时,才可以找到具体的文件位置。如果一个page是anonymous的,则mapping指向表示swap cache的swapper_space,此时index就是swapper_space内的offset。

事实上,现在最新Linux版本的struct page实现中大量用到了union,也就是同一个元素在不同的场景下有不同的意义。这是因为每个page frame都需要一个struct page来描述,一个page frame占4KB,一个struct page占32字节,那所有的struct page需要消耗的内存占了整个系统内存的32/4096,不到1%的样子,说小也小,但一个拥有4GB物理内存的系统,光这一项的开销最大就可达30多MB。

如果能在struct page里省下4个字节,那就能省下4多MB的内存空间,所以这个结构体的设计必须非常考究,不能因为多一种场景的需要就在struct page中增加一个元素,而是应该尽量采取复用的方式。

需要注意的是,struct page描述和管理的是这4KB的物理内存,它并不关注这段内存中的数据变化。

Zone

因为硬件的限制,内核不能对所有的page frames采用同样的处理方法,因此它将属性相同的page frames归到一个zone中。对zone的划分与硬件相关,对不同的处理器架构是可能不一样的。

比如在i386中,一些使用DMA的设备只能访问0~16MB的物理空间,因此将0~16MB划分为了ZONE_DMA。ZONE_HIGHMEM则是适用于要访问的物理地址空间大于虚拟地址空间,不能建立直接映射的场景。除开这两个特殊的zone,物理内存中剩余的部分就是ZONE_NORMAL了。

在其他一些处理器架构中,ZONE_DMA可能是不需要的,

ZONE_HIGHMEM也可能没有。比如在64位的x64中,因为内核虚拟地址空间足够大,不再需要ZONE_HIGH映射,但为了区分使用32位地址的DMA应用和使用64位地址的DMA应用,64位系统中设置了ZONE_DMA32和ZONE_DMA。

所以,同样的ZONE_DMA,对于32位系统和64位系统表达的意义是不同的,ZONE_DMA32则只对64位系统有意义,对32位系统就等同于ZONE_DMA,没有单独存在的意义。

此外,还有防止内存碎片化的ZONE_MOVABLE和支持设备热插拔的ZONE_DEVICE。可通过“cat /proc/zoneinfo |grep Node”命令查看系统中包含的zones的种类。

Zone虽然是用于管理物理内存的,但zone与zone之间并没有任何的物理分割,它只是Linux为了便于管理进行的一种逻辑意义上的划分。Zone在Linux中用struct zone表示(以下为了讲解需要,调整了结构体中元素的顺序):

struct zone {
     spinlock_t         lock;

     unsigned long      spanned_pages;
     unsigned long      present_pages; 
     unsigned long      nr_reserved_highatomic;    
     atomic_long_t      managed_pages;

     struct free_area   free_area[MAX_ORDER];
     unsigned long      _watermark[NR_WMARK];
     long               lowmem_reserve[MAX_NR_ZONES];
     atomic_long_t      vm_stat[NR_VM_ZONE_STAT_ITEMS];

     unsigned long      zone_start_pfn;
     struct pglist_data *zone_pgdat;
     struct page        *zone_mem_map;
     ...    
} 
  • lock是用来防止并行访问struct zone的spin lock,它只能保护struct zone这个结构体哈,可不能保护整个zone里的所有pages。
  • spanned_pages是这个zone含有的总的page frames数目。在某些体系结构(比如Sparc)中,zone中可能存在没有物理页面的"holes",spanned_pages减去这些holes里的absent pages就是present_pages

nr_reserved_highatomic是为某些场景预留的内存(参考本系列的第三篇文章),managed_pages是由buddy内存分配系统管理的page frames数目,其实也就是present_pages减去reserved pages。

  • free_area由free list空闲链表构成,表示zone中还有多少空余可供分配的page frames。_watermark有min(mininum), low, high三种,可作为启动内存回收的判断标准(详情请参考这篇文章)。

lowmem_reserve是给更高位的zones预留的内存(更详细的介绍请参考这篇文章)。vm_stat作为zone的内存使用情况的统计信息,是“/proc/zoneinfo”的数据来源。

  • zone_start_pfn是zone的起始物理页面号,zone_start_pfn+spanned_pages就是该zone的结束物理页面号。zone_pgdat是指向这个zone所属的node的。zone_mem_map指向由struct page构成的mem_map数组。

因为内核对zone的访问是很频繁的,为了更好的利用硬件cache来提高访问速度,struct zone中还有一些填充位,用于帮助结构体元素的cache line对齐。这和struct page对内存精打细算的使用形成了鲜明的对比,因为zone的种类很有限,一个系统中一共也不会有多少个zones,struct zone这个结构体的体积大点也没有什么关系。

关于前面提到的node的介绍,请看下文分解。


原创文章,转载请注明出处。

编辑于 04-03

文章被以下专栏收录