2023-06-11  阅读(4)
原文作者:奇小葩 原文地址:https://blog.csdn.net/u012489236/category_9614673.html

对于内存释放函数也可以归纳到一个主要的函数(__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阶的内存块,计算如下图所示

ordrr page_idx buddy_index-page-index __find_combined_index
0 10 1 10
1 10 -2 8
2 8 4 8
3 8 -8 0

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

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

202306111242591801.png

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


Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文