Skip to content

Latest commit

 

History

History
420 lines (334 loc) · 13 KB

物理内存管理.md

File metadata and controls

420 lines (334 loc) · 13 KB

物理内存管理

物理内存的获取及存储

物理内存信息从固件或者设备树中进行解析并且存储在struct loongarchlist_mem_map结构中,供后续物理内存管理等操作。

struct loongarchlist_mem_map {
	u8	map_count;
	struct	loongson_mem_map {
		u32 mem_type;
		u64 mem_start;
		u64 mem_size;
	} __packed map[LOONGARCH_BOOT_MEM_MAP_MAX];
} __packed;

物理内存管理的框架

和大多数操作系统类似,kernel-travel 将内存分割为一个个页,页作为内存管理的最小单位。由于 loongarch 2k1000 开发板的物理内存是不连续的块,因此我们对这些内存块使用 DISCONTIGMEM 非连续内存模型分别进行管理。为了 kernel-travel 后期的扩展与优化(DMA等),我们采用 NUMA 体系架构,鉴于当前CPU核数限制,管理使用的是一个伪NUMA节点,一个单独的节点管理所有的内存。

Alt text

物理内存管理中用到的结构体

pglist_data

pglist_data 结构中记录了一个 NUMA 点中的信息。

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;
	unsigned long node_spanned_pages;
	int node_id;
	struct task_struct *kswapd;
	int kswapd_order;
	enum zone_type kswapd_highest_zoneidx;

	int kswapd_failures;

	unsigned long		totalreserve_pages;
	unsigned long		flags;

} pg_data_t;

使用全局变量 node_data 来管理所有的 NUMA 节点

struct pglist_data node_data[MAX_NUMNODES];

zone

在每一个 NUMA 节点下我们使用 zone 管理不同类型的物理内存。

struct zone {
	unsigned long _watermark[NR_WMARK];
	unsigned long watermark_boost;
	long low_reserved_mem[MAX_NR_ZONES];
	int node;
	struct pglist_data	*zone_pgdat;
	unsigned long		zone_start_pfn;
	atomic_long_t		managed_pages;
	unsigned long		managed_pages;	// for tmp use
	unsigned long		spanned_pages;
	unsigned long		present_pages;
	const char		*name;
	struct bitmap		*pageblock_flags;
	spinlock_t		lock;
	// 伙伴系统的核心数据结构
	struct free_area	free_area[MAX_ORDER + 1];
	bool			initialized;
};

我们将 zone 分为三种类型:

  • ZONE_DMA 用于 DMA 的物理内存分配
  • ZONE_NORMAL 用于普通物理页的分配
  • ZONE_MOVABLE 用于减少内存碎片,提高系统可用性的优化机制,将内存碎片合并为连续的大物理内存

每一个 zone 中使用伙伴系统来实现每个 zone 下的物理页的内存管理。

初始化 zone 结构体:

//初始化 zone 结构体的初始状态
static void __meminit zone_init_internals(struct zone *zone, enum zone_type idx, int nid, unsigned long remaining_pages);

对于 zone 的处理,提供了以下接口:

//计算并返回指定 NUMA 节点中一个内存区域的页数
static unsigned long __init zone_spanned_pages_in_node(int nid,unsigned long zone_type, unsigned long node_start_pfn, unsigned long node_end_pfn, unsigned long *zone_start_pfn,unsigned long *zone_end_pfn)

//计算在指定 NUMA 节点中某个范围内缺失的页面数量
unsigned long __init zone_absent_pages_in_node(int nid, unsigned long range_start_pfn, unsigned long range_end_pfn)

free_area

free_area 结构用于管理内存区域中的空闲页面,它通过链表组织不同迁移类型的空闲页面,并维护一个计数器来记录总的空闲页数。

struct free_area {
    struct list_head free_list[MIGRATE_TYPES];  // 用于存储不同迁移类型的空闲页面链表。
    unsigned long nr_free;                      // 记录该区域的自由页的总数。用于追踪该区域内总共有多少页处于空闲状态。
};

对于页面的类型我们将其分为以下三种:

  • MIGRATE_UNMOVABLE:不可移动的内存页。不能被迁移到其他节点或区域
  • MIGRATE_MOVABLE:可移动的内存页,可以在内存回收或迁移时移动到其他节点或区域
  • MIGRATE_RECLAIMABLE:可回收的内存页,低内存条件下可以被回收

伙伴系统

为了避免内存碎片化的问题,我们采取伙伴系统来减少页与页之间的内存碎片化。

伙伴系统核心数据结构:

struct free_area {
	struct list_head	free_list[MIGRATE_TYPES];
	unsigned long		nr_free;
};

当前结构的核心初始化函数

static void __init free_area_init_core(struct pglist_data *pg_data)
{
	int nid = pg_data->node_id;
	pgdat_init_internals(pg_data);

	for (enum zone_type j = 0; j < MAX_NR_ZONES; j++) {
		struct zone *zone = pg_data->node_zones + j;
		unsigned long size = zone->spanned_pages;
		unsigned long present_size = zone->present_pages;
		unsigned long memmap_pages_size = calc_memmap_pages_size(size);
		
		if(present_size > memmap_pages_size) {
			present_size -= memmap_pages_size;
			if(memmap_pages_size)
				printk("[%s zone]: %lu pages used for memmap\n",
					 zone_names[j], memmap_pages_size);
			else
				printk("[%s zone]: %lu memmap pages exceeds freesize %lu\n",
					zone_names[j], memmap_pages_size, present_size);
		}
		// 这里需要 DMA 的话还要减去为 DMA 保留的页
		// ...
		nr_kernel_pages += present_size;
		nr_all_pages += present_size;
		zone_init_internals(zone, j, nid, present_size);
		
		if (!size)
			continue;

		init_currently_empty_zone(zone, zone->zone_start_pfn, size);
	}
	return;
}

给 free_list 中添加,删除页的函数实现:

static inline void del_page_from_free_list(struct page *page, struct zone *zone,
					   unsigned int order)
{
	list_del(&page->buddy_list);
	set_page_private(page, 0);
	zone->free_area[order].nr_free--;
}

static inline void add_to_free_list_tail(struct page *page, struct zone *zone,
					 unsigned int order, int migratetype)
{
	struct free_area *area = &zone->free_area[order];

	list_add_tail(&page->buddy_list, &area->free_list[migratetype]);
	area->nr_free++;
}

static inline void add_to_free_list(struct page *page, struct zone *zone,
				    unsigned int order, int migratetype)
{
	struct free_area *area = &zone->free_area[order];

	list_add(&page->buddy_list, &area->free_list[migratetype]);
	area->nr_free++;
}

内核会为 NUMA 节点中的每个物理内存区域 zone 分配一个伙伴系统用于管理该物理内存区域 zone 里的空闲内存页,而伙伴系统的核心数据结构就封装在 struct zone 里。

伙伴系统所分配的物理内存页全部都是物理上连续的,并且只能分配 2 的整数幂个页,这里的整数幂在内核中称之为分配阶 order。

在我们调用物理内存分配接口时,均需要指定这个分配阶 order,意思是从伙伴系统申请多少个物理内存页,假设我们指定分配阶为 order,那么就会从伙伴系统中申请 2 的 order 次幂个物理内存页。

伙伴系统会将物理内存区域中的空闲内存根据分配阶 order 划分出不同尺寸的内存块,并将这些不同尺寸的内存块分别用一个双向链表组织起来。

比如:分配阶 order 为 0 时,对应的内存块就是一个 page。分配阶 order 为 1 时,对应的内存块就是 2 个 pages。依次类推,当分配阶 order 为 n 时,对应的内存块就是 2 的 order 次幂个 pages。

页面分裂逻辑

当请求的内存块大于当前可用的最大块时,伙伴系统会将大块分裂成两个较小的块,直到能满足请求大小为止。具体步骤如下:

  1. 查找合适的块:

从与请求大小最接近的较大块开始查找可用的内存块。

  1. 分裂操作:

如果没有找到合适大小的块,则从更大的块中分裂出两个较小的块,直到找到合适大小的块。每次分裂都会将较大的块分成两个相等的小块,称为“伙伴”。更新自由链表:将分裂出的块重新插入到对应大小的自由链表中。

  1. 分配内存:

将找到的合适块分配给请求方,并从自由链表中移除。

示例:

请求分配3页内存,而当前最大可用块为8页。将8页块分裂为两个4页块。将其中一个4页块分裂为两个2页块。再将其中一个2页块与剩下的1页块合并成3页块,满足请求。

从伙伴系统获取页的核心函数:

struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
						const struct alloc_context *ac)
{
	struct zone * zone;
	int i;
retry:
	for_pglist_data_each_zone(zone, node_data) {
	if (zone->name == zone_names[1]) {
		struct page *page;
		unsigned long mark;
		mark = wmark_pages(zone, alloc_flags & ALLOC_WMARK_MASK);
		if (!zone_watermark_fast(zone, order, mark,
				       ac->highest_zoneidx, alloc_flags,
				       gfp_mask)) {
			// 如果不能快速分配,直接返回 NULL
			return NULL;
		}
try_this_node:
		// 尝试从伙伴系统获取
		// page = rmqueue(ac->preferred_zoneref->zone, zone, order,
		// 		gfp_mask, alloc_flags, ac->migratetype);
		page = rmqueue(NULL, zone, order,
				gfp_mask, alloc_flags, MIGRATE_UNMOVABLE);
		if(page) {
			/*初始化当前 struct page*/
			return page;
		} else {
			/*分配失败重试*/
			goto retry;
		}
	}
	}
	return NULL;
}

struct page *__alloc_pages(gfp_t gfp, unsigned int order, int preferred_nid)
{
	struct page *page;
	unsigned int alloc_flags = ALLOC_WMARK_LOW;
	gfp_t alloc_gfp; /* The gfp_t that was actually used for allocation */
	struct alloc_context ac = { };
	
	if (order > MAX_PAGE_ORDER)
		return NULL;
	/* gfp 逻辑处理*/
	
	/*预先 alloc page*/
	// if (!prepare_alloc_pages(gfp, order, preferred_nid, &alloc_gfp))
	// 	return NULL;

	/*第一次尝试分配*/
	page = get_page_from_freelist(alloc_gfp, order, alloc_flags, &ac);
	if (likely(page))
		goto out;
	/*slow path alloc*/
out:
	return page;
}

查找伙伴页面中指定大小页面分配操作的核心函数:

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

	/* 从当前分配阶 order 开始在伙伴系统对应的  free_area[order]  里查找合适尺寸的内存块*/
	for (current_order = order; current_order <= MAX_ORDER; ++current_order) {
		area = &(zone->free_area[current_order]);
		page = get_page_from_free_area(area, migratetype);
		if (!page)
			// 没有空闲页,继续向上一级寻找
			continue;
		// 从当前的 free_list 的链表中删除
		del_page_from_free_list(page, zone,current_order);
		// 更新到更低的 free_list 中
		expand(zone, page, order, current_order, migratetype);
		// 设置页面的迁移类型
		page->index = migratetype;
		return page;
	}
	// 内存分配失败返回 null
	return NULL;
}

页面合并逻辑

当释放内存块时,伙伴系统会尝试将释放的块与其“伙伴”块合并,形成更大的块。具体步骤如下:

  1. 查找伙伴块:

根据释放块的地址计算出其伙伴块的地址。检查伙伴块是否也在自由链表中。

  1. 合并操作:

如果伙伴块也在自由链表中,将两个块合并成一个更大的块。合并后的块继续尝试与其新的伙伴块合并。继续此过程,直到无法合并为止。

  1. 更新自由链表: 将合并后的块插入到对应大小的自由链表中。

示例:

释放一个2页块,其伙伴块也为2页且在自由链表中。将两个2页块合并成一个4页块。再检查4页块的伙伴块,继续尝试合并。

释放页面到伙伴系统中的核心函数,原理同上:

void __free_pages_ok(struct page *page, unsigned int order,
			    bool fpi_flags)
{
	int migratetype;
	unsigned long pfn = page_to_pfn(page);
	struct zone *zone = page_zone(page);
	free_one_page(zone, page, pfn, order, MIGRATE_UNMOVABLE, fpi_flags);
}

void free_one_page(struct zone *zone,
				struct page *page, unsigned long pfn,
				unsigned int order,
				int migratetype, bool fpi_flags)
{
	struct page *buddy;
	unsigned long buddy_pfn = 0;
	unsigned long combined_pfn;
	bool to_tail;
	ASSERT(zone_is_initialized(zone) == true);
	while (order < MAX_PAGE_ORDER) {
		buddy = find_buddy_page_pfn(page, pfn, order, &buddy_pfn);
		if (!buddy)
			// 不需要合并
			goto done_merging;
		
		del_page_from_free_list(buddy, zone, order);
		combined_pfn = buddy_pfn & pfn;
		page = page + (combined_pfn - pfn);
		pfn = combined_pfn;
		order++;
	}
done_merging:
	set_buddy_order(page, order);
	if(fpi_flags == FPI_TO_TAIL)
		to_tail = true;
	else
	 	to_tail = buddy_merge_likely(pfn, buddy_pfn, page, order);
	
	if (to_tail)
		add_to_free_list_tail(page, zone, order, migratetype);
	else
		add_to_free_list(page, zone, order, migratetype);
}

检查是否是伙伴页面的核心函数

static inline unsigned long
__find_buddy_pfn(unsigned long page_pfn, unsigned int order)
{
	return page_pfn ^ (1 << order);
}

static inline unsigned int
buddy_order(struct page *page)
{
	return page->private;
}

static inline bool
page_is_buddy(struct page *page, struct page *buddy,
				 unsigned int order)
{
	if (buddy_order(buddy) != order)
		return false;
	/*这里只管理 Normal,忽略*/
	// if (page_zone_id(page) != page_zone_id(buddy))
	// 	return false;
	return true;
}