linux内存管理笔记(二十六)----伙伴系统页面释放

对于内存释放函数也可以归纳到一个主要的函数(__free_pages),只是用不同的参数调用而已,前者是通过page,后者是通过虚拟地址addr,其定义如下

extern void __free_pages(struct page *page, unsigned int order);
extern void free_pages(unsigned long addr, unsigned int order);

对于free_pages和__free_pages之间的关系,首先需要将虚拟地址转换成指向struct page的指针

void free_pages(unsigned long addr, unsigned int order)
{
	if (addr != 0) {
		VM_BUG_ON(!virt_addr_valid((void *)addr));
		__free_pages(virt_to_page((void *)addr), order);
	}
}

对于__free_pages是一个基础函数,用于实现内核API中所有涉及到内存释放的接口函数,其代码流程如下:

void __free_pages(struct page *page, unsigned int order)
{
	if (put_page_testzero(page)) {//检查页框是否还有进程在使用,就是检查_count变量的值是否为0
		if (order == 0) //如果是1个页框,则放回每CPU高速缓存中
			free_hot_cold_page(page, false);
		else  //如果是多个页框,则放回伙伴系统
			__free_pages_ok(page, order);
	}
}
  • 首先,调用put_page_testzero来查看该页是否还有其他引用(struct page结构中的_count),如果没有被引用,就走到对应的页面释放流程中,如果还被引用,就啥也不做
  • 由申请页面的时候,会区分是申请的单页还是多页,那么释放的时候,就也做同样的处理。会判断所需释放的内存是单页还是较大的内存块。
  • 如果释放的是单页,则不还给伙伴系统,还是放回per-cpu缓存中。
  • 如果释放的时多页,就直接调用__free_pages_ok归还到伙伴系统中

1. free_hot_cold_page流程

void free_hot_cold_page(struct page *page, bool cold)
{
	struct zone *zone = page_zone(page);	//用于根据page得到所在zone
	struct per_cpu_pages *pcp;
	unsigned long flags;
	unsigned long pfn = page_to_pfn(page);	//根据给出页地址求出对应的页帧号
	int migratetype;
	//释放前pcp的准备工作,检查释放满足释放条件
	if (!free_pcp_prepare(page))
		return;
	//获取页框所在pageblock的页框类型
	migratetype = get_pfnblock_migratetype(page, pfn);
    //设置页框类型为pageblock的页框类型,因为在页框使用过程中,这段pageblock可以移动到了其他类型
	set_pcppage_migratetype(page, migratetype);
	local_irq_save(flags);
	__count_vm_event(PGFREE);
	//如果不是高速缓存类型,就放回到伙伴系统中
	if (migratetype >= MIGRATE_PCPTYPES) {
		if (unlikely(is_migrate_isolate(migratetype))) {
			free_one_page(zone, page, pfn, 0, migratetype);
			goto out;
		}
		migratetype = MIGRATE_MOVABLE;
	}
	//获取当前cpu页列表的zone->pageset->pcp
	pcp = &this_cpu_ptr(zone->pageset)->pcp;
	if (!cold) //hot page加入list 头部,便于优先使用
		list_add(&page->lru, &pcp->lists[migratetype]);
	else //对于cold page,将其加入list尾部
		list_add_tail(&page->lru, &pcp->lists[migratetype]);
	pcp->count++;
    //当前CPU高速缓存中页框数量高于最大值,将pcp->batch数量的页框放回伙伴系统
	if (pcp->count >= pcp->high) {
		unsigned long batch = READ_ONCE(pcp->batch);
		free_pcppages_bulk(zone, batch, pcp);
		pcp->count -= batch;
	}

out:
	local_irq_restore(flags);
}

对于冷页和热页,主要表现是当一个页被释放时,默认设置为热页的话,因为该页可能有些地址的数据还是处于映射到CPU cache,当该CPU上有进程申请单个页框时,优先把这些热页分配出去,这样能提高cache的命中率,提高效率,则软件上的实现方式也比较简单,热页,则直接加入到CPU页框高速缓存链表的链表头,冷页则直接加入到链表尾。

2. __free_pages_ok流程

再看看连续页框的释放,连续页框释放主要是__free_pages_ok()函数

static void __free_pages_ok(struct page *page, unsigned int order)
{
	unsigned long flags;
	int migratetype;
	unsigned long pfn = page_to_pfn(page);       //根据给出页地址求出对应的页帧号

	if (!free_pages_prepare(page, order, true))  //释放前pcp的准备工作,检查释放满足释放条件
		return;

	migratetype = get_pfnblock_migratetype(page, pfn);//获取页框所在pageblock的页框类型
	local_irq_save(flags);
	__count_vm_events(PGFREE, 1 << order);       //统计当前CPU一共释放的页框数
	free_one_page(page_zone(page), page, pfn, order, migratetype); //释放函数
	local_irq_restore(flags);
}

对于该接口无论是释放单页还是连续页,在释放时,会获取该页所载的pageblock的类型,然后把此页设置成pageblock一致的类型,因为有一种情况,比如一个pageblock为MIGRATE_MOVABLE类型,并且有部分页已经被使用(这些正在被使用的页都为MIGRATE_MOVABLE),然后MIGRATE_RECLAIMABLE类型的页不足,需要从MIGRATE_MOVABLE这里获取这个pageblock到MIGRATE_RECLAIMABLE类型中,这个pageblock的类型就被修改成了MIGRATE_RECLAIMABLE,这样就造成了正在使用的页的类型会与pageblock的类型不一致。最后调用free_one_page函数,其定义如下

static void free_one_page(struct zone *zone,
				struct page *page, unsigned long pfn,
				unsigned int order,
				int migratetype)
{
	unsigned long nr_scanned;
	spin_lock(&zone->lock);
	nr_scanned = node_page_state(zone->zone_pgdat, NR_PAGES_SCANNED);//数据更新
	if (nr_scanned)
		__mod_node_page_state(zone->zone_pgdat, NR_PAGES_SCANNED, -nr_scanned);
	//内存隔离
	if (unlikely(has_isolate_pageblock(zone) ||
		is_migrate_isolate(migratetype))) {
		migratetype = get_pfnblock_migratetype(page, pfn);
	}
	__free_one_page(page, pfn, zone, order, migratetype);//释放page开始的order次方个页框到伙伴系统,这些页框的类型时migratetype
	spin_unlock(&zone->lock);
}

整个释放过程的核心函数使__free_one_page,依据申请的算法,那么释放就涉及到对页面能够进行合并的。相关的内存区被添加到伙伴系统中适当的free_area列表中,在释放时,该函数将其合并为一个连续的内存区,放置到高一阶的free_are列表中。如果还能合并一个进一步的伙伴对,那么也进行合并,转移到更高阶的列表中。该过程会一致重复下去,直至所有可能的伙伴对都已经合并,并将改变尽可能向上传播。

static inline void __free_one_page(struct page *page,
		unsigned long pfn,
		struct zone *zone, unsigned int order,
		int migratetype)
{
	unsigned long page_idx;
	unsigned long combined_idx;
	unsigned long uninitialized_var(buddy_idx);
	struct page *buddy;
	unsigned int max_order;
	//则最大的order应该为MAX_ORDER与pageblock_order+1中最小那个
	max_order = min_t(unsigned int, MAX_ORDER, pageblock_order + 1);                
	//检查
	VM_BUG_ON(!zone_is_initialized(zone));
	VM_BUG_ON_PAGE(page->flags & PAGE_FLAGS_CHECK_AT_PREP, page);
	//
	VM_BUG_ON(migratetype == -1);
	if (likely(!is_migrate_isolate(migratetype)))
		__mod_zone_freepage_state(zone, 1 << order, migratetype);
	//将释放的页面转换成page idx
	page_idx = pfn & ((1 << MAX_ORDER) - 1);
	//如果被释放的页不是所释放阶的第一个页,则说明参数有误
	VM_BUG_ON_PAGE(page_idx & ((1 << order) - 1), page);
	VM_BUG_ON_PAGE(bad_range(zone, page), page);//检查页面是否处于zone之中

continue_merging://释放页以后,当前页面可能与前后的空闲页组成更大的空闲页面,直到放到最大阶的伙伴系统中
	while (order < max_order - 1) {
		buddy_idx = __find_buddy_index(page_idx, order);//找到与当前页属于同一个阶的伙伴页面索引
		buddy = page + (buddy_idx - page_idx);//计算伙伴页面的页地址
        //检查buddy是否描述了大小为order的空闲页框块的第一个页
		if (!page_is_buddy(page, buddy, order))
			goto done_merging;
		//页面调试功能,页面被释放时是整个的从内核地址空间中移除的。该选项显著地降低了速度,但它也能迅速指出特定类型的内存崩溃错误,需要配置CONFIG_DEBUG_PAGEALLOC
		if (page_is_guard(buddy)) {
			clear_page_guard(zone, buddy, order, migratetype);
		} else {
			list_del(&buddy->lru);//如果能够合并,则将伙伴页从伙伴系统中摘除
			zone->free_area[order].nr_free--;//同时减少当前阶中的空闲页计数
			rmv_page_order(buddy);//清除伙伴页的伙伴标志,因为该页会被合并
		}
		combined_idx = buddy_idx & page_idx;//将当前页与伙伴页合并后,新的页面起始地址
		page = page + (combined_idx - page_idx);
		page_idx = combined_idx;
		order++;
	}
	if (max_order < MAX_ORDER) {
	
		if (unlikely(has_isolate_pageblock(zone))) {
			int buddy_mt;

			buddy_idx = __find_buddy_index(page_idx, order);
			buddy = page + (buddy_idx - page_idx);
			buddy_mt = get_pageblock_migratetype(buddy);

			if (migratetype != buddy_mt
					&& (is_migrate_isolate(migratetype) ||
						is_migrate_isolate(buddy_mt)))
				goto done_merging;
		}
		max_order++;
		goto continue_merging;
	}

done_merging:
	set_page_order(page, order);//设置伙伴页中第一个空闲页的阶

 	 /**
     * 如果当前合并后的页不是最大阶的,那么将当前空闲页放到伙伴链表的最后。
     * 这样,它将不会被很快被分配,更有可能与更高阶页面进行合并。
     */
	if ((order < MAX_ORDER-2) && pfn_valid_within(page_to_pfn(buddy))) {
		struct page *higher_page, *higher_buddy;
		combined_idx = buddy_idx & page_idx;//计算更高阶的页面索引及页面地址
		higher_page = page + (combined_idx - page_idx);
		buddy_idx = __find_buddy_index(combined_idx, order + 1);
		higher_buddy = higher_page + (buddy_idx - combined_idx);
		if (page_is_buddy(higher_page, higher_buddy, order + 1)) {//更高阶的页面是空闲的,属于伙伴系统 
			//将当前页面合并到空闲链表的最后,尽量避免将它分配出去
            list_add_tail(&page->lru,
				&zone->free_area[order].free_list[migratetype]);
			goto out;
		}
	}
	//更高阶的页面已经分配出去,那么将当前页面放到链表前面
	list_add(&page->lru, &zone->free_area[order].free_list[migratetype]);
out:
	zone->free_area[order].nr_free++;//将当前阶的空闲计数加
}

但内核如何知道一个伙伴对的两个部分都位于空闲页的列表中呢?为将内存块放回伙伴系统,内核必须计算潜在的伙伴地址,以及在有可能合并的情况下合并后内存块的索引。内核提供辅助函数用于计算

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

对于__free_one_page试图释放一个order的一个内存块,有可能不只是当前内存块与能够与其合并的伙伴直接合并,而且高阶的伙伴也可以合并,因此内核需要找到可能的最大分配阶。假设释放一个0阶内存块,即一页,该页的索引值为10,假设页10是合并两个3阶伙伴最后形成一个4阶的内存块,计算如下图所示

ordrrpage_idxbuddy_index-page-index__find_combined_index
010110
110-28
2848
38-80

第一遍寻找到页10的伙伴页11,由于需要的不是伙伴的页号,而是指向对应page的实例指针,buddy_index-page_idx就派上用场了,该值表示当前页与伙伴系统的差值,page指针加上该值,即可得到伙伴page的实例。

然后通过page_is_buddy需要改指针来检查伙伴系统是否是空闲,如果恰好是空闲,那么久可以合并这两个伙伴。这时候就需要将页11从伙伴系统中移除,重新合并形成一个更大的内存块,而rmv_page_order负责清楚PG_buddy标志和private数据。然后下一遍循环工作类似,但这一次order=1,也就是说,内核试图合并两个2页的伙伴,得到一个4页的内存块,其合并图如下图所示
在这里插入图片描述

到此,伙伴系统的页释放流程也梳理完毕,其实现过程也比较简单。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值