Linux内存管理(二)物理内存管理(上)

Linux内存管理

Linux内存管理(一)Linux进程空间管理

Linux内存管理(二)物理内存管理(上)

Linux内存管理(三)物理内存管理(下)

Linux内存管理(四)用户态内存映射

Linux内存管理(五)内核态内存映射

Linux内存管理(二)物理内存管理(上)

一、物理内存的组织形式

由于物理内存是连续的,页也是连续的,每个页的大小一样,从0开始给每个页编号,每个页用struct page表示,存放在一个大数组里。因此对于任何一个地址,只要除以页的大小,就可以得到对应页的编号,根据下标就可以找到对应的struct page结构,这种模型是最经典的平坦内存模型

所有的CPU总是通过总线去访问内存,这是最经典的内存使用方法,它可以使用平坦内存模型来管理内存

img

在这种模式下,所有的CPU都在总线的一侧,所有的内存组成一大块内存在总线的另外一侧,CPU访问内存都需要通过总线访问,而且距离都是一样的,这种模式称为SMP(Symmetric multiprocessing),即为对称多处理器。这种模式有一个显著的缺点,就是每个CPU访问内存都需要通过总线,那么总线就会成为瓶颈

img

为了提高性能,有了一种更加高级的模式,NUMA(Non-uniform memory access),非一致内存访问。这种模式下,内存不是组成连续的一大块,而是每个CPU都有自己的一块内存,CPU访问内存不需要经过总线,所以速度上会更快,每个CPU和内存组成一个NUMA节点。但是在本地内存不足的情况下,每个CPU会去其他NUMA节点申请内存,此时内存的访问时间就比较长

这样内存被分为多个节点,每个节点都分成一个一个的页。由于页是全局唯一定位的,所以每个页都需要有一个全局唯一的页号。但是由于物理内存不再是连续的,所以页号也不是连续的,于是内存模型就变成了非连续内存模型,管理起来就会比较复杂

二、节点

下面解析当前主流场景,NUMA方式

为了表示一个NUMA节点,内核定义了struct pglist_struct这样一个结构体,如下

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;
	unsigned long node_start_pfn;
	unsigned long node_present_pages; /* total number of physical pages */
	unsigned long node_spanned_pages; /* total size of physical page range, including holes */
	int node_id;
......
} pg_data_t;

  • 每个节点都有自己的ID:node_id
  • node_mem_map是这个节点struct page数组,用于描述这个节点所有的页
  • node_start_pfn是这个节点的起始页号
  • node_spanned_pages是整个物理内存包含的页数目(包括空洞)
  • node_present_pages是真正可用的物理页数目

例如:64M物理内存隔着4M的空洞,然后再是另外的64M,换算成页数目,分别是16K、1K、16K。那么node_spanned_pages就是33K,node_spanned_pages就是32K

每个节点被分为一个一个的区域zone,存放在node_zones数组中,数组的大小为MAX_NR_ZONES,定义如下

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的物理内存
  • ZONE_DMA32:对于64位CPU,还有这个DMA区域
  • ZONE_NORMAL:就是直接映射区
  • ZONE_MOVABLE:可移动区域,通过将物理内存划分为可移动分配区域和不可移动分配区域来避免内存碎片
  • __MAX_NR_ZONES:内存区域的数量

内核中有一个数组用来存放节点

struct pglist_data *node_data[MAX_NUMNODES] __read_mostly;

三、区域

到这里,将内存分为节点,将节点分为区域,下面来看一看区域的定义

区域是zone结构体表示

struct zone {
......
	struct pglist_data	*zone_pgdat;
	struct per_cpu_pageset __percpu *pageset;


	unsigned long		zone_start_pfn;


	/*
	 * spanned_pages is the total pages spanned by the zone, including
	 * holes, which is calculated as:
	 * 	spanned_pages = zone_end_pfn - zone_start_pfn;
	 *
	 * present_pages is physical pages existing within the zone, which
	 * is calculated as:
	 *	present_pages = spanned_pages - absent_pages(pages in holes);
	 *
	 * managed_pages is present pages managed by the buddy system, which
	 * is calculated as (reserved_pages includes pages allocated by the
	 * bootmem allocator):
	 *	managed_pages = present_pages - reserved_pages;
	 *
	 */
	unsigned long		managed_pages;
	unsigned long		spanned_pages;
	unsigned long		present_pages;


	const char		*name;
......
	/* free areas of different sizes */
	struct free_area	free_area[MAX_ORDER];


	/* zone flags, see below */
	unsigned long		flags;


	/* Primarily protects free_area */
	spinlock_t		lock;
......
} ____cacheline_internodealigned_in_

  • zone_start_pfn:表示这个属于这个zone的第一个页
  • spanned_pages:注释里有spanned_pages = zone_end_pfn - zone_start_pfn,表示spanned_pages就是结束页面减去起始页面的页面数,不管中间是否存在空洞
  • present_pages:注释里有spanned_pages - absent_pages(pages in holes),表示减去空洞后的页面数
  • managed_pages:注释中有managed_pages = present_pages - reserved_pages,表示这个zone中被伙伴系统管理的所有的page数目
  • per_cpu_pageset:用于区分冷热页。什么是冷热页?指的是一个页是否被加载进CPU的高速缓存中

四、页

在了解区域后,再来看组成物理内存最基本的单位,页的数组结构使用struct page表示。这个结构体定义非常的复杂,因为支持多种使用模式,所以定义了许多union

    struct page {
    	unsigned long flags;
    	union {
    		struct address_space *mapping;	
    		void *s_mem;			/* slab first object */
    		atomic_t compound_mapcount;	/* first tail page */
    	};
    	union {
    		pgoff_t index;		/* Our offset within mapping. */
    		void *freelist;		/* sl[aou]b first free object */
    	};
    	union {
    		unsigned counters;
    		struct {
    			union {
    				atomic_t _mapcount;
    				unsigned int active;		/* SLAB */
    				struct {			/* SLUB */
    					unsigned inuse:16;
    					unsigned objects:15;
    					unsigned frozen:1;
    				};
    				int units;			/* SLOB */
    			};
    			atomic_t _refcount;
    		};
    	};
    	union {
    		struct list_head lru;	/* Pageout list	 */
    		struct dev_pagemap *pgmap; 
    		struct {		/* slub per cpu partial pages */
    			struct page *next;	/* Next partial slab */
    			int pages;	/* Nr of partial slabs left */
    			int pobjects;	/* Approximate # of objects */
    		};
    		struct rcu_head rcu_head;
    		struct {
    			unsigned long compound_head; /* If bit zero is set */
    			unsigned int compound_dtor;
    			unsigned int compound_order;
    		};
    	};
    	union {
    		unsigned long private;
    		struct kmem_cache *slab_cache;	/* SL[AU]B: Pointer to slab */
    	};
    ......
    }

第一模式

要用就使用一整页。这一整页的内存要么直接跟虚拟地址建立映射关系,这中称为匿名页(Anonymous Page)。或者关联一个文件,然后再跟虚拟地址建立映射

如果某一页使用此模式,那么union就使用以下的结构

  • struct address_space *mapping 就是用于内存映射,如果是匿名页,最低位为1;如果是映射文件,最低位为0
  • pgoff_t index 是映射区的偏移量
  • atomic_t _mapcount 每个进程都有自己的页表,这个变量是指有多少个页表映射到这个物理页
  • struct list_head lru 表示这一页应该在链表上,如果这一页被换出,那么就应该在换出页的链表中
  • compound相关的变量用于复合页,就是将物理上连续的两个或者多个看成一个独立的大页

第二种模式

仅需要分配一小块内存,并不需要一整页。为了满足这种需求,Linux系统采用了一种被称为slab allocator的技术,用于分配slab中的一小块内存。它的基本工作原理是申请一整块页,然后划分成许多小块的存储池,用复杂的队列来维护这些小块的状态(被分配了 / 被放回池子了 / 应该被回收)

slab对于队列的维护过于复杂,后来出现了一种不使用队列的分配器slub allocator,它保留的slab的用户接口,可以把它看作是slab的另一种实现

还有一种小块内存的分配器slob

如果某一页被切分为一小块一小块,那么union中就会使用以下结构

  • s_mem 是已经分配了正在使用的slab的第一个对象
  • freelist 是池子的空闲对象
  • rcu_head 是需要释放的列表

五、页的分配

前面讲了物理内存的组织,从NUMA节点到区域到页再到小块。接下俩看物理内存的分配

对于要分配比较大的内存,例如分配页级别的,可以使用伙伴系统(Buddy System)

Linux内存管理的页大小为4K。把所有空闲的页分组为11个页块链表,每个链表管理相应大小的页块,有1、2、4、8、16、32、64、128、256、512、1024个连续页的页块。最大可以申请1024个连续的页,对应4M大小的连续内存。每个页块第一页的起始地址是该页块大小的整数倍

img

在 struct zone 里面有以下的定义

struct free_area	free_area[MAX_ORDER];

MAX_ORDER表示2的指数

#define MAX_ORDER 11

当申请的页块大小介于free_area中两个页块大小之间时,会选取更大的一个页块大小,或者如果对应的大小没有空闲的页块,那么也会分配一个更大的页块。在得到一个更大的页块后,会将其进行拆分,然后将空闲的页块继续插入到对应页块大小的链表中

例如申请一个128个页的页块,如果没有,那么就找256,然后一直如此,直到能够找到。如果找到的是256个页的页块。那么就会将其拆分为128和128个页大小的页块,然后将一个空闲的页块添加到128对应的页块链表中

对于这些内容,可以在 alloc_pages 函数中找到定义

static inline struct page *
alloc_pages(gfp_t gfp_mask, unsigned int order)
{
	return alloc_pages_current(gfp_mask, order);
}


/**
 * 	alloc_pages_current - Allocate pages.
 *
 *	@gfp:
 *		%GFP_USER   user allocation,
 *      	%GFP_KERNEL kernel allocation,
 *      	%GFP_HIGHMEM highmem allocation,
 *      	%GFP_FS     don't call back into a file system.
 *      	%GFP_ATOMIC don't sleep.
 *	@order: Power of two of allocation size in pages. 0 is a single page.
 *
 *	Allocate a page from the kernel page pool.  When not in
 *	interrupt context and apply the current process NUMA policy.
 *	Returns NULL when no page can be allocated.
 */
struct page *alloc_pages_current(gfp_t gfp, unsigned order)
{
	struct mempolicy *pol = &default_policy;
	struct page *page;
......
	page = __alloc_pages_nodemask(gfp, order,
				policy_node(gfp, pol, numa_node_id()),
				policy_nodemask(gfp, pol));
......
	return page;
}

  • order:表示分配2的指数个页的页块
  • gfp:分配标志,表示要分配那么区域的物理页
    • GFP_USER 表示分配一个页映射到用户虚拟地址空间,并且希望直接被内核或者硬件访问,主要用于一个用户进程希望通过内存映射的方式,访问硬件缓存(如显卡缓存)
    • GFP_KERNEL 用于内核中分配页,主要分配 ZONE_NORMAL 区域的内存
    • GFP_HIGHMEM 用于分配高端区域的物理内存

接下来调用 get_page_from_freelist,这是伙伴系统的核心。它会先循环查找对应节点的zone,如果找不到,那么就看备用节点的zone

static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
						const struct alloc_context *ac)
{
......
	for_next_zone_zonelist_nodemask(zone, z, ac->zonelist, ac->high_zoneidx, ac->nodemask) {
		struct page *page;
......
		page = rmqueue(ac->preferred_zoneref->zone, zone, order,
				gfp_mask, alloc_flags, ac->migratetype);
......
}

每一个zone,都有伙伴系统维护的各种大小的队列

rmqueue 就是找到合适大小的队列,然后将页块取下来

最终会调用到 __rmqueue_smallest

static inline
struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
						int migratetype)
{
	unsigned int current_order;
	struct free_area *area;
	struct page *page;


	/* Find a page of the appropriate size in the preferred list */
	for (current_order = order; current_order < MAX_ORDER; ++current_order) {
		area = &(zone->free_area[current_order]);
		page = list_first_entry_or_null(&area->free_list[migratetype],
							struct page, lru);
		if (!page)
			continue;
		list_del(&page->lru);
		rmv_page_order(page);
		area->nr_free--;
		expand(zone, page, order, current_order, area, migratetype);
		set_pcppage_migratetype(page, migratetype);
		return page;
	}


	return NULL;

从指定的区域中,按照当前指定的指数开始查找,如果找不到,那么就到更大的指数查找。除了将页块从链表取下,还要将多余的页面插入到合适的链表中,expand 就是完成这个工作

static inline void expand(struct zone *zone, struct page *page,
	int low, int high, struct free_area *area,
	int migratetype)
{
	unsigned long size = 1 << high;


	while (high > low) {
		area--;
		high--;
		size >>= 1;
......
		list_add(&page[size].lru, &area->free_list[migratetype]);
		area->nr_free++;
		set_page_order(&page[size], high);
	}
}

六、总结

如果有多个CPU,就会有多个NUMA节点。每个节点使用 struct pglist_data 表示,存放在一个数组中

每个节点分为多个区域,每个区域使用 struct zone 表示,也存放在一个数组中

每个区域分为多个页,空闲页存放在 struct free_area 中,使用伙伴系统进行管理和分配

每一页都是使用 struct page 表示

img

发布了107 篇原创文章 · 获赞 197 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/weixin_42462202/article/details/102157454