Linux内存管理(五):描述物理内存

基于Linux 5.10, 体系结构是aarch64


Linux 把物理内存划分为三个层次来管理: 存储节点(Node)、内存管理区(Zone)和页面(Page)

在这里插入图片描述

现在有一个问题:

为什么linux描述物理内存需要划分这么多层次?

1. Nodes

1.1 为什么需要Node?

在回答这个问题之前, 我们需要先了解计算机系统中的两个内存架构: UMANUMA
UMA: Uniform Memory Access, 统一内存访问。每个CPU共享相同的内存地址空间。目前UMA架构广泛应用于嵌入式系统、手机以及个人电脑等。
在这里插入图片描述

NUMA: Non-Uniform Memory Access, 非统一内存访问。系统中会有很多的内存节点和多个CPU簇, 所有节点中的CPU可以访问全部的物理内存,但是CPU访问本地的节点速度远快于访问远端的内存节点的速度。目前NUMA架构广泛应用于大型的硬件平台,如服务器。
在这里插入图片描述

NUMA架构最主要的目的是提供可扩展的内存带宽。为了兼容这一设计,linux将系统的硬件资源分成称为“Node”的多个软件抽象。如上图所示,CPU0和CPU1组成一个节点(Node 0), 它们可以通过系统总线访问本地的L3 cache和内存, I/O总线等资源。对于UMA架构而言,内核可以把内存当成只有一个内存node节点。

1.2 pglist_data数据结构

[include/linux/mmzone.h]
node的数据结构为pglist_data, 每一个node对应一个struct pglist_data.

struct pglist_data *node_data[MAX_NUMNODES] __read_mostly;

pg_data_t的数据结构:

typedef struct pglist_data {
	struct zone node_zones[MAX_NR_ZONES];
	struct zonelist node_zonelists[MAX_ZONELISTS];
	int nr_zones;
	unsigned long node_start_pfn;
	unsigned long node_present_pages; 
	unsigned long node_spanned_pages; 
	int node_id;
	wait_queue_head_t kswapd_wait;
	wait_queue_head_t pfmemalloc_wait;
	struct task_struct *kswapd;
	int kswapd_order;
	enum zone_type kswapd_highest_zoneidx;
	int kswapd_failures;
	unsigned long		min_unmapped_pages;
	unsigned long		min_slab_pages;
	ZONE_PADDING(_pad1_)
	spinlock_t		lru_lock;
	struct deferred_split deferred_split_queue;
	struct lruvec		__lruvec;
	unsigned long		flags;
	ZONE_PADDING(_pad2_)
	struct per_cpu_nodestat __percpu *per_cpu_nodestats;
	atomic_long_t		vm_stat[NR_VM_NODE_STAT_ITEMS];
} pg_data_t;

关键的数据成员:

node_zones[MAX_NR_ZONES]对应该node包含的各个类型的zone
node_zonelists包含了2个zonelist,自身node的zones列表以及备用的zones列表(用于本地node分配不到内存时的备选,也称为fallback)
nr_zones包含zone的个数
node_start_pfn该node中内存的起始页帧号
node_present_pages该node地址范围内的实际管理的页面数量
node_spanned_pages该node地址范围内的所有页面数量, 包括空洞的页面
kswapd负责回收该node内存的内核线程,每个node对应一个内核线程kswapd
lru_lock用于保护Zone中lru链表的锁
lruvecLRU链表的集合
flags内存域的当前状态, 在mmzone.h定义了zone的所有可用zone_flag
vm_statnode的计数

ZONE_PADDING宏作用是让前后的成员分布在不同的cache line中, 以空间换取时间。

在linux环境中我们可以使用numactl命令查看Node中的cpu和内存,以及各个node之间的distance

numactl --hardware

available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 16 17 18 19 20 21 22 23
node 0 size: 131037 MB
node 0 free: 3019 MB
node 1 cpus: 8 9 10 11 12 13 14 15 24 25 26 27 28 29 30 31
node 1 size: 131071 MB
node 1 free: 9799 MB
node distances:
node 0 1
 0: 10 20
 1: 20 10

2. Zones

2.1 为什么需要将node拆分成不同的zone

其实这是个历史遗留问题,出于对不同架构的兼容性的考虑。
比如32位的处理器只支持4G的虚拟地址,然后1G的地址空间给内核,但这样无法对超过1个G的物理内存进行一一映射。 Linux内核提出的解决方案是将物理内存分成2部分,一部分直接做线性映射,另一部分叫高端内存。这两部分对应内存管理区就分别为ZONE_NORMAL和ZONE_HIGNMEM。 当然对于64位的架构而言,有足够大的内核地址空间可以映射物理内存,所以就不需要ZONE_HIGHMEM了。

所以,将node拆分成zone主要还是出于Linux为了兼容各种架构和平台,对不同区域的内存需要采用不同的管理方式和映射方式。

2.2 Zone type

linux内存管理区可以分为如下几种:[include/linux/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,
#ifdef CONFIG_ZONE_DEVICE
	ZONE_DEVICE,
#endif
	__MAX_NR_ZONES
};
zone typedescrpition
ZONE_DMAISA设备的DMA操作,范围是0~16M,ARM架构没有这个zone
ZONE_DMA32用于低于4G内存进行DMA操作的32位设备
ZONE_NORMAL标记了线性映射物理内存, 4G以上的物理内存。 如果系统内存不足4G, 那么所有的内存都属于ZONE_DMA32范围, ZONE_NORMAL则为空
ZONE_HIGHMEM高端内存,标记超出内核虚拟地址空间的物理内存段. 64位架构没有该ZONE
ZONE_MOVABLE虚拟内存域, 在防止物理内存碎片的机制中会使用到该内存区域
ZONE_DEVICE为支持热插拔设备而分配的Non Volatile Memory非易失性内存

在linux环境中,我们可以通过下面命令查看Zone的分类

cat /proc/zoneinfo |grep Node

Node 0, zone					DMA32
Node 0, zone					Normal
Node 0, zone					Movable
Node 1, zone					DMA32
Node 1, zone					Normal
Node 1, zone					Movable
Node 2, zone					DMA32
Node 2, zone					Normal
Node 2, zone					Movable
Node 3, zone					DMA32
Node 3, zone					Normal
Node 3, zone					Movable

2.3 zone的数据结构

zone的数据结构定义在[include/mm/mmzone.h]

struct zone {
	unsigned long _watermark[NR_WMARK];
	unsigned long watermark_boost;
	unsigned long nr_reserved_highatomic;
	long lowmem_reserve[MAX_NR_ZONES];
	int node;
	struct pglist_data	*zone_pgdat;
	struct per_cpu_pageset __percpu *pageset;
	unsigned long		*pageblock_flags;
	int initialized;
	ZONE_PADDING(_pad1_)
	struct free_area	free_area[MAX_ORDER];
	unsigned long		flags;
	spinlock_t		lock;
	ZONE_PADDING(_pad2_)
	unsigned long percpu_drift_mark;
	bool			contiguous;
	ZONE_PADDING(_pad3_)
	atomic_long_t		vm_stat[NR_VM_ZONE_STAT_ITEMS];
	atomic_long_t		vm_numa_stat[NR_VM_NUMA_STAT_ITEMS];
} ____cacheline_internodealigned_in_smp;

几个比较关键的数据成员:

watermark水线, 在系统启动时会计算WMARK_MIN、WMARK_LOW、WMARK_HIGH, 在kswapd页面回收中和分配页面时会有用到
lowmem_reserve本管理区预留的物理内存大小
zone_pgdat指向所属的node节点
pageset每个CPU维护一个page list,避免自旋锁的冲突
free_area[MAX_ORDER]空闲内存链表,按2的幂次分组,用于实现伙伴系统
lock对zone并发访问的保护的自旋锁
vm_statzone的计数

3. Pages

物理页面通常被称作Page Frames。 Linux内核使用struct page数据结构来描述一个物理页面, 这些page数据结构会存放在一个数组中。
struct page和物理页面是一对一的映射关系。
在这里插入图片描述
映射的关系取决于当前的内存模型, 当前内核支持3种内存模型FLATMEM、DISCONTIGMEM、SPARSEMEM。
关于内存模型,可以看wowotech的这篇文章【Linux内存模型

/*
 * supports 3 memory models.
 */
#if defined(CONFIG_FLATMEM)

#define __pfn_to_page(pfn)	(mem_map + ((pfn) - ARCH_PFN_OFFSET))
#define __page_to_pfn(page)	((unsigned long)((page) - mem_map) + \
				 ARCH_PFN_OFFSET)
#elif defined(CONFIG_DISCONTIGMEM)

#define __pfn_to_page(pfn)			\
({	unsigned long __pfn = (pfn);		\
	unsigned long __nid = arch_pfn_to_nid(__pfn);  \
	NODE_DATA(__nid)->node_mem_map + arch_local_page_offset(__pfn, __nid);\
})

#define __page_to_pfn(pg)						\
({	const struct page *__pg = (pg);					\
	struct pglist_data *__pgdat = NODE_DATA(page_to_nid(__pg));	\
	(unsigned long)(__pg - __pgdat->node_mem_map) +			\
	 __pgdat->node_start_pfn;					\
})

#elif defined(CONFIG_SPARSEMEM_VMEMMAP)

/* memmap is virtually contiguous.  */
#define __pfn_to_page(pfn)	(vmemmap + (pfn))
#define __page_to_pfn(page)	(unsigned long)((page) - vmemmap)

#elif defined(CONFIG_SPARSEMEM)
/*
 * Note: section's mem_map is encoded to reflect its start_pfn.
 * section[i].section_mem_map == mem_map's address - start_pfn;
 */
#define __page_to_pfn(pg)					\
({	const struct page *__pg = (pg);				\
	int __sec = page_to_section(__pg);			\
	(unsigned long)(__pg - __section_mem_map_addr(__nr_to_section(__sec)));	\
})

#define __pfn_to_page(pfn)				\
({	unsigned long __pfn = (pfn);			\
	struct mem_section *__sec = __pfn_to_section(__pfn);	\
	__section_mem_map_addr(__sec) + __pfn;		\
})
#endif /* CONFIG_FLATMEM/DISCONTIGMEM/SPARSEMEM */

3.1 page的数据结构

struct page {
	unsigned long flags;
	union {
		struct {	
			struct list_head lru;
			struct address_space *mapping;
			pgoff_t index;		/* Our offset within mapping. */
			unsigned long private;
		};
		struct {
			dma_addr_t dma_addr;
		};
		struct 
			union {
				struct list_head slab_list;
				struct {
					struct page *next;
					int pages;	
					int pobjects;
				};
			};
			struct kmem_cache *slab_cache;
			void *freelist;	
			union {
				void *s_mem;
				unsigned long counters;	
				struct {	
					unsigned inuse:16;
					unsigned objects:15;
					unsigned frozen:1;
				};
			};
		};
		struct {	
			unsigned long compound_head;	

			/* First tail page only */
			unsigned char compound_dtor;
			unsigned char compound_order;
			atomic_t compound_mapcount;
			unsigned int compound_nr;
		};
		struct {	
			unsigned long _compound_pad_1;	/
			atomic_t hpage_pinned_refcount;
			struct list_head deferred_list;
		};
		struct {	
			unsigned long _pt_pad_1;
			pgtable_t pmd_huge_pte; 
			unsigned long _pt_pad_2;
			union {
				struct mm_struct *pt_mm;
				atomic_t pt_frag_refcount; 
			};
			spinlock_t ptl;
		};
		struct {	
			struct dev_pagemap *pgmap;
			void *zone_device_data;
		};

		struct rcu_head rcu_head;
	};

	union {	
		atomic_t _mapcount;
		unsigned int page_type;
		unsigned int active;		/* SLAB */
		int units;			/* SLOB */
	};
	atomic_t _refcount;
#ifdef CONFIG_MEMCG
	union {
		struct mem_cgroup *mem_cgroup;
		struct obj_cgroup **obj_cgroups;
	};
#endif
	...
} _struct_page_alignment;

重要的数据成员:

flags用于描述page的状态或者其他属性, 每一bit代表一种状态;在 include/linux/page_flags.h定义有page的各个可用状态pageflags
mappingmapping指定了页帧所在的地址空间。有三种含义:
mapping = 0,说明该page属于swap cache;
mapping != 0,bit[0] = 0,说明该page属于页缓存或文件映射,mapping指向文件的地址空间address_space;
mapping != 0,bit[0] != 0,说明该page为匿名映射,mapping指向struct anon_vma对象
indexindex是页帧在映射内部的偏移量, 即在映射的虚拟空间(vma_area)内的偏移
private私有数据指针,由应用场景确定其具体的含义
_mapcount被页表映射的次数,也就是说该page同时被多少个进程共享.(注意:被映射了不一定在使用, 需要和下面的refcount进行区分)
_refcount表示引用计数。当count值为0时,该page frame可被free掉;如果不为0,说明该page正在被某个进程或者内核使用,调用page_count()可获得count值。

4. 初始化流程

介绍了Node、Zone和Page的基本概念,现在了解下内核是怎么对它们进行初始化的。
[Linux内存管理(四):paging_init分析]中介绍到了paging_init, 现在我们来到接下来的函数bootmem_init->zone_sizes_init()

[mm/page_alloc.c]
zone_sizes_init()的核心函数是free_area_init(), 该函数会去遍历系统中所有的nodes.

4.1 free_area_init

/**
 * free_area_init - Initialise all pg_data_t and zone data
 * @max_zone_pfn: an array of max PFNs for each zone
 *
 * This will call free_area_init_node() for each active node in the system.
 * Using the page ranges provided by memblock_set_node(), the size of each
 * zone in each node and their holes is calculated. If the maximum PFN
 * between two adjacent zones match, it is assumed that the zone is empty.
 * For example, if arch_max_dma_pfn == arch_max_dma32_pfn, it is assumed
 * that arch_max_dma32_pfn has no pages. It is also assumed that a zone
 * starts where the previous one ended. For example, ZONE_DMA32 starts
 * at arch_max_dma_pfn.
 */
void __init free_area_init(unsigned long *max_zone_pfn)
{
	...
	/* Initialise every node */
	mminit_verify_pageflags_layout();
	setup_nr_node_ids();
	init_unavailable_mem();
	for_each_online_node(nid) {
		pg_data_t *pgdat = NODE_DATA(nid);
		free_area_init_node(nid);

		/* Any memory on that node */
		if (pgdat->node_present_pages)
			node_set_state(nid, N_MEMORY);
		check_for_memory(pgdat, nid);
	}
	...
}
	

4.2 free_area_init_node

free_area_init_node()依次初始化各个node。

	free_area_init_node()
			|
			|---> calculate_node_totalpages()
							|--->zone_spanned_pages_in_node()
							|--->zone_absent_pages_in_node()
			|---> alloc_node_mem_map()
			|
			|---> free_area_init_core()
			

calculate_node_totalpages()会计算当前node种ZONE_DMA和ZONE_NORMAL的page的数量,确定node下node_spanned_pages和node_present_pages的值
在这里插入图片描述
node_present_pages = node_spanned_pages - node_absent_pages.
alloc_node_mem_map()为所有物理页面申请struct page用于管理这些页面。
free_area_init_core()则遍历node内的所有zones并依次初始化。

4.3 free_area_init_core

	free_area_init_core()
			|
			|--->pgdat_init_internals()
			|
	--------------------zone------------------------------
			|--->calc_memmap_size()
			|
			|--->zone_init_internals()
			|
			|--->set_pageblock_order()
			|
			|--->setup_usemap()
			|
			|--->init_currently_empty_zone()
					|--->zone_init_free_lists()
			|--->memmap_init()
					|--->memmap_init_zone()
    --------------------------------------------------------

  • pgdat_init_internals()初始化Node的pgdata结构体中的字段,比如lru锁, kswapd/kcompat队列等
  • calc_memmap_size() 用于计算mem_map大小, mem_map就是系统种保存所有page的数组;
  • zone_init_internals()初始化Zone的结构体
  • init_currently_empty_zone(), 空区初始化,前面有提到过,比如系统内存不足4G, 那么所有的内存都属于ZONE_DMA32范围, ZONE_NORMAL则为空区; zone_init_free_lists()会初始化与该区对应的伙伴系统
  • memmap_init()初始化mem_map数组。memmap_init_zone()通过pfn找到对应的struct page,初始化page实例, 它还会将所有的页最初都标记为可移动的(MIGRATE_MOVABLE)。设置为可移动的主要还是为了避免内存的碎片化,IGRATE_MOVABLE链表中都是可以迁移的页面, 把不连续的内存通过迁移的手段进行规整,把空闲内存组合成一块连续内存,那就可以在一定程度上达到内存申请的需求。

至此,Node节点和Zone管理区的关键数据已完成初始化.

5. 小结

主要介绍了linux内存管理所涉及到的Node, Zone和page, 介绍了它们的数据结构和初始化流程, 不过只是浅尝辄止。

本篇涉及到的与内存管理的数据结构有内存节点(struct pglist_data), 内存管理区(struct zone), 物理页面(struct page), 以及mem_map[]数组,PFN页帧号等。
在这里插入图片描述

6. 参考资料

  1. What is NUMA?
  2. http://jake.dothome.co.kr/free_area_init_node/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值