【Linux内核架构】【(二)内存管理】(N)UMA模型中的内存组织(上)

2.3 (N)UMA模型中的内存组织

Linux支持的各种不同体系结构在内存管理方面差别很大。
一个主要的问题是页表中不同数目的间接层。另一个关键是NUMA和UMA系统的划分。
内核对于UMA系统和NUMA系统使用相同的数据结构,所以算法几乎一样。
在UMA系统上,只使用一个NUMA结点来管理整个系统内存。而内存管理的其他部分则认为是在处理一个伪NUMA系统。

2.3.1 内存域zone的类型

内存管理中,内存是一个个的结点组成,每个结点都会关联到一个CPU,为pg_data_t结构。每个结点又由一个个内存域组成,最多由3个内存域组成。

内核中用下列枚举常量来表示系统中的内存域类型。

<mmzone.h> 
enum zone_type { 
#ifdef CONFIG_ZONE_DMA 
	ZONE_DMA, 
#endif 
#ifdef CONFIG_ZONE_DMA32 
	ZONE_DMA32, 
#endif 
	ZONE_NORMAL, 
#ifdef CONFIG_HIGHMEM 
	ZONE_HIGHMEM, 
#endif 
	ZONE_MOVABLE, 
	MAX_NR_ZONES 
};
  • ZONE_DMA:DMA(Direct Memory Access)的内存域。该区域的长度依赖于处理器类型。在IA-32系统上,一般的限制是16MB。

  • ZONE_DMA32:DMA的内存域,使用32位地址字可寻址。

    只有在64位系统上,两种DMA内存域才有差别。ZONE_DMA32在64位系统中才可能存在并有效,因为64位系统允许使用32位地址按字寻址。在32位计算机上,ZONE_DMA32是空的,即长度为0MB。

  • ZONE_NORMAL:可直接映射到内核段的普通内存域。只有该内存域在所有体系结构上保证都会存在,但无法保证该地址范围对应了实际的物理内存。例如,如果AMD64系统有2GB内存(系统内存较小),那么所有内存都属于ZONE_DMA32范围,ZONE_NORMAL则为空(在AMD64系统上,如果DMA设备需要访问的内存量较大,且系统内存较小,那么所有内存都可能被划分为ZONE_DMA32,以满足DMA设备的需求)。

  • ZONE_HIGHMEM:高端内存,超出内核映射范围的物理内存,64位系统一般不用。

根据编译时的配置,可能无需考虑某些内存域。例如在64位系统中,并不需要高端内存域。如果支持了只能访问4GB以下内存的32位外设,才需要DMA32内存域。

  • ZONE_MOVABLE:伪内存域,在防止物理内存碎片的机制中需要使用该内存域。
  • MAX_NR_ZONES:结束标记,在内核要迭代系统中的所有内存域时,会用到该常量。

各个内存域zone都有一个物理内存页page数组,用来组织属于该内存域的物理内存页(内核中叫页帧)。对于每个页帧,都分配了一个struct page实例以及所需的管理数据。

各个内存结点pg_data_t保存在一个单链表zonelist中,供内核遍历。

出于性能考虑,在为进程分配内存时,内核总是试图在当前运行的CPU相关联的NUMA结点上进行。但这并不总是可行的,例如,该结点的内存可能已经用尽。对此类情况,每个结点都提供了一个备用列表(借助于struct zonelist)。该列表包含了其他结点(和相关的内存域),可用于代替当前结点分配内存。列表项的位置越靠后,就越不适合分配。

下图是NUMA系统中内存各结构关系示意图。
在这里插入图片描述
UMA系统中仅有一个内存结点pg_data_t。

2.3.2 结点管理

2.3.2.1 内存结点pg_data_t

pg_data_t表示内存的一个结点。图片可以参照上图。

<mmzone.h> 
typedef struct pglist_data { 
    struct zone node_zones[MAX_NR_ZONES]; 
    struct zonelist node_zonelists[MAX_ZONELISTS]; 
    int nr_zones; 
    struct page *node_mem_map; 
    struct bootmem_data *bdata; 
    unsigned long node_start_pfn; 
    unsigned long node_present_pages;
    unsigned long node_spanned_pages; //物理内存页的总长度,包括空洞在内
    int node_id; 
    struct pglist_data *pgdat_next; 
    wait_queue_head_t kswapd_wait; 
    struct task_struct *kswapd; 
    int kswapd_max_order; 
} pg_data_t;
  • node_zones[]:包含结点中各内存域的数据结构。该数组总是有3个项,即使结点没有那么多内存域,也是3个。如果不足3个,则其余的用0填充。
  • node_zonelists[]:备用结点及其内存域的列表,在当前结点没有可用空间时,会在备用结点分配内存。
  • nr_zones:结点中不同内存域的数目。
  • node_mem_map:指向page实例数组的指针,用于描述结点的所有物理内存页。包含了结点中所有内存域的页。
  • bdata:指向自举内存分配器(boot memory allocator)数据结构的实例。在系统启动期间,内存管理子系统初始化之前,内核也需要使用内存(必须保留部分内存用于初始化内存管理子系统),所以内核使用了自举内存分配器。
  • node_start_pfn:pfn(page frame number)页帧号,node_start_pfn是该NUMA结点第一个页帧的逻辑编号。系统中所有结点的页帧是依次编号的,每个页帧的编号都是全局唯一的(不只是结点内唯一)。(页帧即为物理内存页)

node_start_pfn在UMA系统中是0,因为内存块只有一个结点,所以第一个页帧编号也是0。

  • node_present_pages:该结点中页帧的数量。
  • node_spanned_pages:该结点以页帧为单位计算的长度。与node_present_pages的值不一定相同,因为结点中可能有一些空洞,并不对应真正的页帧。
  • node_id:全局结点ID。系统中的NUMA结点都从0开始编号。
  • pgdat_next:指向下一个内存结点,系统中所有结点都通过单链表连接起来,其末尾为空指针。
  • kswapd_wait:kswapd(kernel swap daemon),是交换守护进程的等待队列,在将页帧换出结点时会用到(页面回收和页交换)。
  • kswapd:指向负责该结点的交换守护进程的task_struct。
  • kswapd_max_order:定义需要释放的区域的长度,用于页交换子系统的实现。

结点及其包含的内存域之间的关联,以及备用列表,这些是通过结点pg_data_t起始处的几个数组建立的。

2.3.2.2 结点状态node_states

如果系统中的结点多于一个,内核会维护一个位图(比特位),用以提供各个结点的状态信息。状态是用位掩码指定的,有以下的一些值。

<nodemask.h> 
enum node_states { 
    N_POSSIBLE, //结点在某个时候可能变为联机
    N_ONLINE, //结点是联机的
    N_NORMAL_MEMORY, //结点有普通内存域
#ifdef CONFIG_HIGHMEM 
	N_HIGH_MEMORY, //结点有普通或高端内存域
#else 
	N_HIGH_MEMORY = N_NORMAL_MEMORY, 
#endif 
	N_CPU, //结点有一个或多个CPU
	NR_NODE_STATES 
};

N_POSSIBLE、N_ONLINE和N_CPU用于CPU和内存的热插拔。

N_HIGH_MEMORY和N_NORMAL_MEMORY用于内存管理。如果结点有普通或高端内存时设置为N_HIGH_MEMORY,当结点没有高端内存时设置为N_NORMAL_MEMORY。

<nodemask.h> 
void node_set_state(int node, enum node_states state)  //设置某结点的位域
void node_clear_state(int node, enum node_states state) //清除某结点的位域
<linux/nodemask.h>
for_each_node_state(__node, __state);  //迭代处于特定状态的所有结点
#define for_each_node_state(nid, state, nodemask) \
    for (nid = first_node_state(state, nodemask); \
         nid != NUMA_NO_NODE; \
         nid = next_node_state(nid + 1, state, nodemask))

for_each_online_node(node); //迭代所有活动结点

nid为结点id,state为结点状态,nodemask是一个 nodemask_t类型的变量,表示节点集合。

first_node_state()和next_node_state()用于获取给定状态下的第一个和下一个节点ID。

如果内核编译为只支持单个结点(即使用平坦内存模型),那么没有结点位图,这些操作该位图的函数变为空操作。

2.3.2.3 内存域zone
<mmzone.h> 
struct zone {
    //通常由页分配器访问的字段
    unsigned long pages_min, pages_low, pages_high; 
    unsigned long lowmem_reserve[MAX_NR_ZONES]; 
    struct per_cpu_pageset pageset[NR_CPUS]; 
    
    //不同长度的空闲区域 
    spinlock_t lock; 
    struct free_area free_area[MAX_ORDER]; 
    
    ZONE_PADDING(_pad1_) 
    
    //通常由页面收回扫描程序访问的字段
    spinlock_t lru_lock; 
    struct list_head active_list; 
    struct list_head inactive_list; 
    unsigned long nr_scan_active; 
    unsigned long nr_scan_inactive; 
    unsigned long pages_scanned;
    unsigned long flags;  //内存域标志
    
    //内存域统计量
    atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS]; 
    
    int prev_priority; 
    
    ZONE_PADDING(_pad2_) 
        
    //很少使用或大多数情况下只读的字段
    wait_queue_head_t * wait_table;
    unsigned long wait_table_hash_nr_entries; 
    unsigned long wait_table_bits; 
    
    //支持不连续内存模型的字段
    struct pglist_data *zone_pgdat;
    unsigned long zone_start_pfn; 
    unsigned long spanned_pages; //总长度,包含空洞
    unsigned long present_pages; //内存数量,不包含空洞
    
    //很少使用
    char *name;
} ____cacheline_maxaligned_in_smp;  //编译器关键字,以实现最优的高速缓存对齐方式

该结构由ZONE_PADDING分隔为几个部分。因为内核对zone结构的访问非常频繁,在多处理器系统上,会有不同的CPU同时访问,所以使用锁防止它们彼此干扰。

内核会经常性地获取该结构的两个自旋锁zone->lock和zone->lru_lock(这些锁也叫作热点hotspot)。

如果数据保存在CPU高速缓存中,那么会处理得更快速。

高速缓存以行为单位,每一行负责不同的内存区。

内核使用ZONE_PADDING宏生成"填充"字段添加到zone结构中,以确保每个自旋锁都处于自身的缓存行中。

zone结构的最后两个部分也通过填充字段彼此分隔开来。两者都不包含锁,主要目的是将数据保持在一个缓存行中,便于快速访问,从而无需从内存加载数据(内存比CPU高速缓存慢)。

填充造成结构长度的增加是可以忽略的,特别是在内核内存中zone结构的实例相对很少。

内存管理比较复杂,所以该结构有的成员的真实意义需要联系其他篇章。

  • pages_min、pages_high、pages_low:页换出时使用的"水印"。若内存不足,内核可以将页写到硬盘。这3个成员会影响交换守护进程的行为。(页面回收和页交换)(分配页)
  1. 若空闲页数量多于pages_high,则内存域的状态是理想的。
  2. 若空闲页数量低于pages_low,则内核开始将页换出到硬盘。
  3. 若空闲页数量低于pages_min,则页回收工作的压力比较大,因为内存域中急需空闲页。

其重要性在页面回收和页交换中体现。

  • lowmem_reserve[]:分别为各种内存域指定了若干页,用于一些不会失败的关键性内存分配。各个内存域的份额根据重要性确定。计算各个内存域份额的算法在后续。
  • pageset[]:用于实现每个CPU的热/冷页帧列表。内核使用这些列表来保存可用于满足实现的“新鲜”页。冷热页、NR_CPUS、struct per_cpu_pageset的解释可见2.2.2.5。
  • lock:自旋锁(热点)。
  • lru_lock:自旋锁(热点)。
  • free_area[]:是struct free_area的数组,用于实现伙伴系统。每个数组元素都表示某种固定长度的一些连续内存区。对于包含在每个区域中的空闲内存页的管理,free_area是一个起点。

接下来的结构成员,用来根据活动情况对内存域中使用的页进行编目。若页访问频繁,则内核认为它是活动的;若页访问不频繁,则不是活动的。在需要换出页时,这种区别很重要。如果可能的话,频繁使用的页应该保持不动,而多余的不活动页则可以换出。

  • active_list:活动页链表(page实例链表)。
  • inactive_list:不活动页链表(page实例链表)。
  • nr_scan_active、nr_scan_inactive:指定在回收内存时需要扫描的活动页和不活动页的页数。
  • pages_scanned:上一次换出页以来,没有成功扫描的页。
  • flags:内存域的当前状态。有一些标志位。也有可能这些标志均未设置。这是内存域的正常状态。
<mmzone.h>
typedef enum {
    ZONE_ALL_UNRECLAIMABLE, //所有的页都已经“钉”住,常驻
    ZONE_RECLAIM_LOCKED, //防止并发回收
     ZONE_OOM_LOCKED, //内存域即可被回收
} zone_flags_t;
  1. ZONE_ALL_UNRECLAIMABLE状态:出现在内核试图重用该内存域的一些页时(页面回收),但因为所有的页都被钉住而无法回收。什么叫做”钉住“?例如,用户空间应用程序使用mlock()系统调用通知内核页不能从物理内存移出,比如移出到磁盘上。这样的页称为钉住的。如果一个内存域中的所有页都被钉住,那么该内存域是无法回收的,即设置该标志。为了不浪费时间,交换守护进程在寻找可供回收的页时,只会简要地扫描一下此类内存域。

  2. ZONE_RECLAIM_LOCKED标志:在SMP系统上,多个CPU可能试图并发地回收一个内存域。如果一个CPU在回收某个内存域,则设置该标志,防止了其他CPU尝试回收该内存域。

  3. ZONE_OOM_LOCKED标志:如果进程消耗了大量的内存,致使必要的操作都无法完成,那么内核会试图杀死消耗内存最多的进程,以获得更多的空闲页。该标志可以防止多个CPU同时进行这种操作。

内核提供了3个辅助函数用于测试和设置内存域的标志:

<mmzone.h> 
void zone_set_flag(struct zone *zone, zone_flags_t flag) //设置某一标志
int zone_test_and_set_flag(struct zone *zone, zone_flags_t flag) //测试是否设置了给定标志,如果没有设置,则设置该标志,并返回标志的原状态。
void zone_clear_flag(struct zone *zone, zone_flags_t flag) //清除某一标志
  • vm_stat[]:内存域统计。维护了大量有关该内存域的统计信息。内核中很多地方都会更新其中的信息。zone_page_state()用来读取vm_stat中的信息。(详细作用在数据同步的页状态)
<vmstat.h>
static inline unsigned long zone_page_state(struct zone *zone, enum zone_stat_item item);

例如,item参数可以为NR_ACTIVE或NR_INACTIVE,来查询存储active_list和inactive_list中的活动和不活动页的页数,为NR_FREE_PAGES,则可以获得内存域中空闲页的数目。

  • prev_priority:存储上一次扫描操作扫描该内存域的优先级,扫描操作是由try_to_free_pages()进行的,直至释放足够的页帧(页面回收和页交换)。扫描会根据prev_priority值判断是否换出映射的页(页交换)。
  • wait_table、wait_table_bits、wait_table_hash_nr_entries:实现了一个进程等待队列,可以让等待某一页变为可用状态的进程使用。进程需要的条件满足时,内核通知进程运行。(内核活动的等待队列)
  • zone_pgdat:指向父节点的pglist_data实例,关联了内存域和父结点。
  • zone_start_pfn:内存域第一个页帧的索引。
  • spanned_pages:内存域中页的总数。不是所有页都是可用的,因为内存域中可能有一些小的空洞。很少使用。
  • present_pages:实际上可用的页的数目。该计数器的值一般与spanned_pages相同。很少使用。
  • name:保存该内存域的一般名称。目前有3个选项可用:Normal、DMA和HighMem。很少使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值