【Linux】—内存管理

内存管理——伙伴系统

图1. 总体结构.

有图的文章

Zone

  Linux中通过对内存功能的不同,进行了划分,分成了ZONE_DMA、ZONE_NORMAL和ZONE_HIGHMEM等部分。

ZONE的区分

(Linux中对Zone的具体划分,待更)

物理内存管理

对物理内存管理,我们就主要关心以下:物理内存管理指什么,如何实现内存页面的分配、如果实现内存页面的释放

伙伴系统

  计算机在使用中,不可避免会遇到重复地申请和释放内存的请求,如果不合理的对内存进行管理,频繁的调入调出可能会使得内存严重的碎片化,不对这种碎片化的情况进行处理,可能会造成一些大页面请求无法满足,从而影响计算机的正常运行。

  • free_area
      为了避免内存的碎片化,Linux采用了不同的伙伴系统算法。伙伴系统下,内存会被进行分别管理——每个zone下会存在一个frea_area的链表,链表中按页面块的大小不同进行划分,其划分的单位是 2 n 2^n 2n,如图2所示。

  • 某种感觉上,伙伴系统一直就是在管理free_area上的数据

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

图2. free_area布局.

  • 迁移类型
      同时根据使用功能的不同,会被设置为不同的迁移类型。
    #define MIGRATE_UNMOVABLE 0 
    #define MIGRATE_RECLAIMABLE 1 
    #define MIGRATE_MOVABLE 2 
    #define MIGRATE_RESERVE 3 
    #define MIGRATE_ISOLATE 4 /* 不能从这里分配 */ 
    #define MIGRATE_TYPES 5
    

  迁移类型的使用,是为了内存管理中出现的碎片化,通过迁移类型的方式将内存页面进行划分,各自管理。

迁移类型含义
MIGRATE_UNMOVABLE不可移动页。这些页包括内核代码、DMA 缓冲区等,它们通常被锁定在内存中且不能被交换出去
MIGRATE_RECLAIMABLE可回收页面。这些页是可以被回收的,但需要进行一定程度的处理才能回收,例如清理缓存
MIGRATE_MOVABLE可移动页面。这些页既不属于不可移动页也不属于可回收页,但它们可以被迁移到其他节点来实现负载均衡
MIGRATE_RESERVE预留页。这些页已经被分配出去,但尚未使用,可以被迁移到其他节点以避免浪费内存
MIGRATE_ISOLATE独立页面。这些页不能被用于分配新的物理页框,通常是为了做某些特殊用途而保留的
MIGRATE_TYPES用于定义free_area链表数量时使用

  在进行内存分配时,可能存在某一个迁移类型的列表无法满足的情况,为了缓解这个情况,伙伴系统允许该迁移列表向其它迁移列表中需求合适的页面。同时,在伙伴系统中,不同的迁移类型向其他迁移列表请求时有不同的遍历顺序

static int fallbacks[MIGRATE_TYPES][MIGRATE_TYPES-1] = {
	[MIGRATE_UNMOVABLE]   = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE,   MIGRATE_RESERVE },
	[MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE,   MIGRATE_MOVABLE,   MIGRATE_RESERVE },
	[MIGRATE_MOVABLE]     = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_RESERVE },
	[MIGRATE_RESERVE]     = { MIGRATE_RESERVE,     MIGRATE_RESERVE,   MIGRATE_RESERVE }, /* Never used */
};

内存页面的分配

标识符-gfp_mask、ALLOC_XXX
  • 在进行内存页面分配之前,需要先了解一些分配标志,这些分配标识规定了分配页面时的不同限制,即gfp_maskALLOC_XXX。一般而言,二者都是指定内存页面的属性,gfp_mask会指定一些影响内存分配算法的属性,ALLOC_XXX则针对页面的特定属性进行管理。
  • gfp_mask允许用户更加细粒度地控制内存分配的行为,而ALLOC_XXX则提供了一些快捷方式来设置一些常见的行为。
  • 这里只提供在分配和释放时会遇到的标志符号
标志符含义
__GFP_WAIT可以等待和重调度?
__GFP_HARDWALL只允许在进程允许运行的CPU所关联的结点分配内存
__GFP_NOMEMALLOC不使用紧急分配链表
__GFP_NOFAIL一直重试,不会失败
__GFP_FS可以调用底层文件系统?
__GFP_NORETRY不重试,可能失败
__GFP_NOWARN禁止分配失败警告
__GFP_HIGH应该访问紧急分配池
标识符含义
ALLOC_WMARK_LOW使用pages_low水印
ALLOC_CPUSET检查内存结点是否对应着指定的CPU集合
ALLOC_WMARK_MIN使用pages_min水印
ALLOC_HARDER试图更努力地分配,即放宽限制
ALLOC_HIGH设置了__GFP_HIGH
ALLOC_NO_WATERMARKS完全不检查水印
ALLOC_WMARK_HIGH检查内存结点是否对应着指定的CPU集合

内存块的释放

内存分配,都是在__alloc_pages中发生

  __alloc_pages在分配页面过程中会根据分配的具体情况,修改分配标识符,来实现内存页面申请。restart:第一次尝试失败后,调整分配策略,再尝试第二次。rebalance:当前两次都申请失败,则会对进行一次调度,再申请最后第三次

图3. restart.

  • 第一次尝试申请
    调用 __alloc_pages 后,做一些简单的测试,设置好分配标志符,进行第一次调用,其分配要求:
  - __GFP_HARDWALL:只允许在进程允许运行的CPU所关联的结点分配内存
  - ALLOC_WMARK_LOW|ALLOC_CPUSET:使用pages_low水印或者检查内存结点是否对应着指定的CPU集合。

水印的要求

pages_high如果空闲页多于pages_high
pages_low将页换出到硬盘
pages_min页回收工作的压力就比较大,因为内存域中急需空闲页
  • 第二次尝试申请
    如果第一次获取不成功,且允许回收内存,则进行一次内存回收
    for (z = zonelist->zones; *z; z++)
    		wakeup_kswapd(*z, order);//这里会对每个zone的内存进行缩减和回收
    
    然后重新修改内存标识符
      alloc_flags = ALLOC_WMARK_MIN;//分配要求进一步降低,水印标记为pages_min水印
      if ((unlikely(rt_task(p)) && !in_interrupt()) || !wait)
      	alloc_flags |= ALLOC_HARDER;
      if (gfp_mask & __GFP_HIGH)
      	alloc_flags |= ALLOC_HIGH;
      if (wait)
      	alloc_flags |= ALLOC_CPUSET;
      page = get_page_from_freelist(gfp_mask, order, zonelist, alloc_flags);
    
    • 首先将alloc_flags设置为ALLOC_WMARK_MIN,表明内存域急需内存页
      进一步根据进程和分配标志的情况对标志符进行修正
      • 如果进程为实时进程且不在中断中
        • alloc_flags设置为ALLOC_HARDER,则希望伙伴系统尽可能地分配页面(实时系统优先级高)
        • 后面的情况就表明该进程为非实时进程或者在中断中
      • 如果gfp_mask & __GFP_HIGH,即表明可以使用紧急备用内存
        • alloc_flags设置为ALLOC_HIGH
      • 如果是可以等待的
        • alloc_flags设置为ALLOC_CPUSET,则还是保持原有设置
    • 重新设置了alloc_flags后,再尝试一次页面获取

图4. rebalance.

  • 第三次尝试申请
    • 检测线程状态

      如果进程标志PF_MEMALLOC,必须分配内存或者当前线程仍在等待释放内存 且没有在中断中

      • 检测gfp_mask,该进程允许使用紧急备用链表 __GFP_NOMEMALLOC
        • 则将alloc_flag设置为无水印的形式,表示不再有要求,有空闲内存就行
    • 经过上述处理还是没有获取内存(表明进程获取内存的意愿并没有那么强烈,那么就可以等等)
      • 进行一次cpu调度
      • 尝试对内存取中的内存进行一次释放
        did_some_progress = try_to_free_pages(zonelist->zones, order, gfp_mask);
        
      • 再进行一次cpu调度
      • 如果释放页面成功,则再进行一次尝试
      • 如果释放页面失败,且gfp_mask为__GFP_FS,表明允许使用底层文件系统
        • 则再调用一次,重新设置内存分配标志,这次只要求页面是当前关联cpu即可
          gfp_mask:__GFP_HARDWALL
          alloc_flag:ALLOC_WMARK_HIGH|ALLOC_CPUSET
          
get_page_from_freelist

get_page_from_freelist会对zonelist进行扫描,然后选取符合要求的要求进行返回(在get_page_from_freelist并不是直接向伙伴系统获取页面,而更像是一个中间层,会通过对相关标志符进行检测,根据zone区域的状态进行调整,选择合适的zone来获取页面)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F2ydwbMH-1686223158787)(img/mm_6.png)]

  • 循环对zone_list中的内存区的内存进行检测
    • 首先对该zone中的内存区进行一个遍历筛选,返回zonelist中的可用内存区域(如果有,就继续向下检测)
    • alloc_flag 标志进行检测
      • alloc_flag & ALLOC_CPUSET,则必须在指定的cpu上允许
        • 使用 **cpuset_zone_allowed_softwall**检测给定内存域是否属于该进程允许的CPU,如果不属于,则遍历下一个zone
      • 根据alloc_flag设定内存索引标志mark
        • 设定后,尝试从zone中获取,是否有合适的内存页面可以分配 zone_watermark_ok
          • 如果没有,则尝试进行页面回收,如果还是没有,就说明无空闲内存。
          static struct page *
          get_page_from_freelist(gfp_t gfp_mask, unsigned int order,
          		struct zonelist *zonelist, int alloc_flags)
          {
          
          zonelist_scan:
          	/*
          	 * Scan zonelist, looking for a zone with enough free.
          	 * See also cpuset_zone_allowed() comment in kernel/cpuset.c.
          	 */
          	//扫描区域列表,寻找具有足够空闲的区域
          	z = zonelist->zones;
          
          	do {
          		//这里是检测zonelist是否需要过滤?
          		//应该就是找到该z下是否合适gfp_zone,如果不合适,就跳过
          		if (unlikely(alloc_should_filter_zonelist(zonelist))) {
          			if (highest_zoneidx == -1)
          				highest_zoneidx = gfp_zone(gfp_mask);
          			if (zone_idx(*z) > highest_zoneidx)
          				continue;
          		}
          
          		if (NUMA_BUILD && zlc_active &&
          			!zlc_zone_worth_trying(zonelist, z, allowednodes))
          				continue;
          		zone = *z;
          		//alloc_flags要求指定了cpu,但是该zone不合适,则跳过寻找下一个zone:检查给定内存域是否属于该进程允许的CPU
          		//cpuset_zone_allowed_softwall:检查给定内存域是否属于该进程允许运行的CPU
          		if ((alloc_flags & ALLOC_CPUSET) &&
          			!cpuset_zone_allowed_softwall(zone, gfp_mask))
          				goto try_next_zone;
          		/*
          		- 如果需要检测水印(进行分配限制检测)
          			- 如果alloc_flags是ALLOC_WMARK_MIN,则mark指定zone->pages_min
          			- 如果alloc_flags是ALLOC_WMARK_LOW,则mark指定zone->pages_low
          			- 如果都不是,则mark指定zone->pages_high
          		*/
          		if (!(alloc_flags & ALLOC_NO_WATERMARKS)) {
          			unsigned long mark;
          			if (alloc_flags & ALLOC_WMARK_MIN)
          				mark = zone->pages_min;
          			else if (alloc_flags & ALLOC_WMARK_LOW)
          				mark = zone->pages_low;
          			else
          				mark = zone->pages_high;
          			//zone_watermark_ok:判断基于以上分配策略设置,能够从内存区域中获取页面吗
          			//如果不能,且从该zone进行回收还是没有成功,那么就是zone满了,进行跳转
          			//zone_watermark_ok:遍历该内存域,是否有足够的空闲页,并试图分配一个连续内存块
          			//如果不够,且zone_reclaim_mode,就会对内存页面进行回收,如果不能回收说明zone无空闲内存,分配失败
          			if (!zone_watermark_ok(zone, order, mark,
          				    classzone_idx, alloc_flags)) {
          				if (!zone_reclaim_mode ||
          				    !zone_reclaim(zone, gfp_mask, order))
          					goto this_zone_full;
          			}
          		}
          		//走到这里,就是满足,从这里获取页面,上面也是调整gfp_mask
          		page = buffered_rmqueue(zonelist, zone, order, gfp_mask);
          		if (page)//获取成功就跳出
          			break;
          this_zone_full:
          		if (NUMA_BUILD)
          			zlc_mark_zone_full(zonelist, z);
          try_next_zone:
          		if (NUMA_BUILD && !did_zlc_setup) {
          			/* we do zlc_setup after the first zone is tried */
          			allowednodes = zlc_setup(zonelist, alloc_flags);
          			zlc_active = 1;
          			did_zlc_setup = 1;
          		}
          	} while (*(++z) != NULL);//对zone进行循环
          
          	if (unlikely(NUMA_BUILD && page == NULL && zlc_active)) {
          		/* Disable zlc cache for second zonelist scan */
          		zlc_active = 0;
          		goto zonelist_scan;
          	}
          	return page;
          }
          
buffered_rmqueue

buffered_rmqueue和get_page_from_freelist一样,并不承担实际的页面分配功能。

  • get_page_from_freelist中会根据水印标志,检测zone中是否有合适的内存空间可以进行分配。如何存在合适空间的zone,则把实际的分配业务交给buffered_rmqueue处理。
  • 同样,buffered_rmqueue也不承担具体的分配任务,而是申请页面的阶数进行检测,根据检测结果从不同的区域中获取页面。(get_xx中对分配策略方面的标志进行检测,探测zone是否合适,get_xxx处理后,buffered_rmqueue则进一步对页面大小进行区分,实现页面的分配)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jLI8GkXs-1686223158788)(img/mm_7.png)]

  • 当阶数等于0

    • 获取cpu中的冷页链表pcp[cold]
    • 判断pcp->count是否充足,如果充足调用rmqueue_bulk进行获取
    • 判断获取页面的迁移类型migratetype,如果不一致,就需要重新再取一次
  • 当阶数大于0

    • 直接调用__rmqueue进行获取
    static struct page *buffered_rmqueue(struct zonelist *zonelist,
    			struct zone *zone, int order, gfp_t gfp_flags)
    {
    	unsigned long flags;
    	struct page *page;
    	int cold = !!(gfp_flags & __GFP_COLD);
    	int cpu;
    	int migratetype = allocflags_to_migratetype(gfp_flags);//根据gfp_flags(分配标志)来指定不同的迁移域
    again:
    	cpu  = get_cpu();//获取cpu
    	if (likely(order == 0)) {
    		struct per_cpu_pages *pcp;//per_cpu_pages存储cpu中的页表和高速缓存信息
    		pcp = &zone_pcp(zone, cpu)->pcp[cold];//获取cpu对应内存zone的冷页
    		/*禁止本地CPU中断,禁止前先保存中断状态:
    		 *这里需要关中断,因为内存回收过程可能发送核间中断,强制每个核从每CPU缓存中
    		 *释放页面。而且中断处理函数也会分配单页。 
    		 */
    		local_irq_save(flags);
    		//如果pcp中没有数据,则通过rmqueue_bulk进行获取
    		if (!pcp->count) {
    			pcp->count = rmqueue_bulk(zone, 0,
    					pcp->batch, &pcp->list, migratetype);
    			if (unlikely(!pcp->count))//如果还是为空,则分配失败
    				goto failed;
    		}
    		//pcp获取到足够的页面后,则对链表进行遍历,找到合适page
    		list_for_each_entry(page, &pcp->list, lru)//应该是遍历链表,
    			if (page_private(page) == migratetype)//如果page的迁移类型合适,则跳出
    				break;
    		/* Allocate more to the pcp list if necessary */
    		/*
    		如果迁移类型不一致
    		同时page->lru没在pcp->list中,则要从rmqueue_bulk中重新获取相应类型的page
    		*/
    		if (unlikely(&page->lru == &pcp->list)) {
    			pcp->count += rmqueue_bulk(zone, 0,
    					pcp->batch, &pcp->list, migratetype);
    			page = list_entry(pcp->list.next, struct page, lru);
    		}
    		list_del(&page->lru);
    		pcp->count--;
    	} else {
    		spin_lock_irqsave(&zone->lock, flags);
    		page = __rmqueue(zone, order, migratetype);//页面超过1,则从__rmqueue中获取,否则从pcp中获取
    		spin_unlock(&zone->lock);
    		if (!page)
    			goto failed;
    	}
    }
    
rmqueue_bulk

从伙伴分配器中获取指定数量的元素

  • rmqueue_bulk实现上也是调用**__rmqueue**,来获取页面
      static int rmqueue_bulk(struct zone *zone, unsigned int order, 
      			unsigned long count, struct list_head *list,
      			int migratetype){
      	int i;
      	spin_lock(&zone->lock);
      	for (i = 0; i < count; ++i) {
      		struct page *page = __rmqueue(zone, order, migratetype);
      		if (unlikely(page == NULL))
      			break;
      		list_add(&page->lru, list);
      		set_page_private(page, migratetype);
      		list = &page->lru;
      	}
      	spin_unlock(&zone->lock);
      	return i;
      }
    
__rmqueue

static struct page *__rmqueue(struct zone *zone, unsigned int order,int migratetype)

static struct page *__rmqueue(struct zone *zone, unsigned int order,int migratetype){
	struct page *page;
	//扫描页的列表,直至找到适当的连续内存块
	page = __rmqueue_smallest(zone, order, migratetype);
	if (unlikely(!page))
	//如果没有从指定的迁移类型中获取到相应的页面,则尝试从其他迁移列表中获取,作为补救
		page = __rmqueue_fallback(zone, order, migratetype);
	return page;
}

zone:内存区,order:阶数,migratetype:迁移类型

  • __rmqueue会调用 __rmqueue_smallest 来获取页面,如果页面分配失败,则使用备用队列
    __rmqueue_fallback
__rmqueue_smallest

遍历给定迁移类型的空闲列表,并从空闲列表中删除最小可用页

  • __rmqueue_smallest会对zone->freeare中的数据进行遍历,zone->freearea按阶数进行管理,__rmqueue_smallest则从当前阶数向最大阶数进行遍历,找到满足页面请求的最佳区域。
    • 获取 zone->freeare[order] 下的空闲页面链表区域area
    • 判断这片区域下对应migratetype的页面是否还有,如果没有则找下一个阶
    • 如果找到,则将该页面取出,并对area统计计数进行更新。
    • 进行expand(遍历给定的迁移类型的空闲列表,并从空闲列表中删除最小可用页)
      for (current_order = order; current_order < MAX_ORDER; ++current_order) {
          area = &(zone->free_area[current_order]);//找到该zone下对应阶数的空闲内存区域area
          if (list_empty(&area->free_list[migratetype]))//如果该内存区域下,没有对应迁移类型的数据,则跳过
          	continue;
      
          page = list_entry(area->free_list[migratetype].next,
          					struct page, lru);
          list_del(&page->lru);//获取到后,从lru链表中移除该内存块
          rmv_page_order(page);//将该页从伙伴系统中移除
          area->nr_free--;//该区域的空余量-1
          __mod_zone_page_state(zone, NR_FREE_PAGES, - (1UL << order));
          //可能存在分配的页面阶数不高,但是area的阶数高,这是就需要使用expand进行分裂
          expand(zone, page, order, current_order, area, migratetype);
      	return page;
      }
      
  • 走到这一步,基本上就获取到页面了,但是还需要做后续的处理,例如当freeare的阶数高,但是需要的阶数并不高,则需要做一些分裂。
    static struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,int migratetype){
    	unsigned int current_order;
    	struct free_area * area;
    	struct page *page;
    	//根据阶数进行遍历
    	for (current_order = order; current_order < MAX_ORDER; ++current_order) {
    		area = &(zone->free_area[current_order]);//找到该zone下对应阶数的空闲内存区域area
    		if (list_empty(&area->free_list[migratetype]))//如果该内存区域下,没有对应迁移类型的数据,则跳过
    			continue;
    		page = list_entry(area->free_list[migratetype].next,
    							struct page, lru);
    		list_del(&page->lru);//获取到后,从lru链表中移除该内存块
    		rmv_page_order(page);//将该页从伙伴系统中移除
    		area->nr_free--;//该区域的空余量-1
    		__mod_zone_page_state(zone, NR_FREE_PAGES, - (1UL << order));
    		//可能存在分配的页面阶数不高,但是area的阶数高,这是就需要使用expand进行分裂
    		expand(zone, page, order, current_order, area, 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)
zone:提取页面的区域
page:对应的page
low:预期需要的分配阶
high:该内存取自的分配阶
area:空闲区域
migratetype:迁移类型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DQIyh2wA-1686223158788)(img/mm_8.png)]

  • 指定的阶数够大,期望阶数不大,则需要进行分裂
    分裂的方法:
    • area中向下走一步到低一个阶的区域,然后size取半
      将后一半内存块放进该area中,然后依次遍历直到获取到low阶的页面,遍历过程中page没动,但通过size在切割page,到对应的free_list中去。
      while (high > low) {
          //area阶数向后退一步
          area--;
          high--;//要放回的链表阶
          size >>= 1;//将size向后移位,并将后半部分放入area对应的空闲队列中
          VM_BUG_ON(bad_range(zone, &page[size]));
          list_add(&page[size].lru, &area->free_list[migratetype]);//这里就相当于将内存页面进行分裂,扩展到对应阶数的area中去
          area->nr_free++;//该阶下的area中的空闲内存量+1
          set_page_order(&page[size], high);
      }
      
    • expand执行结束后,正常流程中,页面分配也就结束了。但还有一个问题没有解决,如果__rmqueue_smallest失败了,该怎么办。为了尽可能地分配内存,Linux会从备选列表中继续尝试分配__rmqueue_fallback。
      static inline void expand(struct zone *zone, struct page *page,
      	int low, int high, struct free_area *area,
      	int migratetype)
      	/*low:是预期的分配阶,high:表示内存取自哪个分配阶*/
      {
      	unsigned long size = 1 << high;
      	//每比较一次order向后退一次
      	while (high > low) {
      		//area阶数向后退一步
      		area--;
      		high--;//要放回的链表阶
      		size >>= 1;//将size向后移位,并将后半部分放入area对应的空闲队列中
      		VM_BUG_ON(bad_range(zone, &page[size]));
      		list_add(&page[size].lru, &area->free_list[migratetype]);//这里就相当于将内存页面进行分裂,扩展到对应阶数的area中去
      		area->nr_free++;//该阶下的area中的空闲内存量+1
      		set_page_order(&page[size], high);
      	}
      }
      
__rmqueue_fallback

从伙伴系统的备选列表中获取元素
Linux系统中,会根据备选选项的不同,维护了一个备选的迁移类型选项
在对备选内存进行分配的原则是,从大到小遍历,避免碎片,如果分配到的迁移类型不同,则尽可能让获取的内存足够大
如果移动这片内存是可回收的内存块,并且这片内存块中有一半空闲,则修改为目标迁移类型

  • 迁移列表的备选队列

    static int fallbacks[MIGRATE_TYPES][MIGRATE_TYPES-1] = {
      [MIGRATE_UNMOVABLE]   = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE,   MIGRATE_RESERVE },
      [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE,   MIGRATE_MOVABLE,   MIGRATE_RESERVE },
      [MIGRATE_MOVABLE]     = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_RESERVE },
      [MIGRATE_RESERVE]     = { MIGRATE_RESERVE,     MIGRATE_RESERVE,   MIGRATE_RESERVE }, /* Never used */
      };
    
  • __rmqueue_smallest中,通过 从小到大的方式,找到最合适的阶的free_area,然后获取。__rmqueue_fallback同样,对空闲列表进行遍历,只是不同在于__rmqueue_fallback对其他迁移类型的空闲队列进行遍历。

  • __rmqueue_fallback在进行遍历时,会从最大阶开始遍历,这样做的原因是为了避免小碎片过多(从大的开始,这样做即使分裂,剩下的也是大碎片)

    • 遍历备选空闲队列

      • 不到必要时刻,不对MIGRATE_RESERVE的内存对象进行处理
    • 如果迁移类型合适,则将起对应的free_list中删除

    • 若满足以下条件,则修改该片内存的迁移类型

      • 阶数>pageblock_order 或者起始的页面迁移类型是可回收类型MIGRATE_RECLAIMABLE
      • 满足条件,就将备选迁移类型的页面转移到需要的页面的迁移类型队列中
    • 获得相应内存后,就进行expand

    • 如果经过上面的处理后,还是没能实现对页面的获取,就启用紧急备用页面

      __rmqueue_smallest(zone, order, MIGRATE_RESERVE);
      

      static struct page *__rmqueue_fallback(struct zone *zone, int order,
      						int start_migratetype)
      {
      	struct free_area * area;
      	int current_order;
      	struct page *page;
      	int migratetype, i;
      	//在Linux系统中,备用迁移域是有一个搜索顺序的
      	/* Find the largest possible block of pages in the other list */
      	//阶数从大到小进行遍历,这样尽量对大块内存进行分割,避免过多的碎片
      	//内核的分配策略是,如果无法避免分配迁移类型不同的内存块,则分配一个尽可能大的内存块
      	for (current_order = MAX_ORDER-1; current_order >= order;
      						--current_order) {
      		//根据搜索顺序,来获取相应的内存块
      		for (i = 0; i < MIGRATE_TYPES - 1; i++) {
      			migratetype = fallbacks[start_migratetype][i];
      			/* MIGRATE_RESERVE handled later if necessary */
      			if (migratetype == MIGRATE_RESERVE)//应该是不到迫不得己不适用紧急分配内存
      				continue;
      			area = &(zone->free_area[current_order]);//获取area
      			if (list_empty(&area->free_list[migratetype]))
      				continue;
      
      			page = list_entry(area->free_list[migratetype].next,
      					struct page, lru);//从area中获取对应类型的page
      			area->nr_free--;//统计量-1
      
      			/*
      			 * 如果分解一个大内存块,则将所有空闲页移动到优先选用的分配列表。
      			 * 如果内核在备用列表中分配可回收内存块,则会更为积极地取得空闲页的所有权
      			 * pageblock_order就是Linux认为的大内存的界限,如果没有超过这个值,就不会进行所有权迁移
      			 */
      			if (unlikely(current_order >= (pageblock_order >> 1)) ||
      					start_migratetype == MIGRATE_RECLAIMABLE) {
      				unsigned long pages;
      				//这里试图将pageblock_order阶数个页的整个内存块转移到新的迁移列表中(转移迁移列表,但只有空闲页才会移动)
      				pages = move_freepages_block(zone, page,
      								start_migratetype);
      				//如果大内存中有1/2是空闲的,则修改整个内存块的迁移类型(应该是将移动的部分的迁移类型进行修改)
      				//如果只是少部分,应该还是不修改的
      				/* Claim the whole block if over half of it is free */
      				if (pages >= (1 << (pageblock_order-1)))
      					//修改整个内存块的迁移类型
      					set_pageblock_migratetype(page,
      								start_migratetype);
      				migratetype = start_migratetype;
      			}
      			/* Remove the page from the freelists */
      			list_del(&page->lru);//从空闲列表中移除
      			rmv_page_order(page);
      			__mod_zone_page_state(zone, NR_FREE_PAGES,
      							-(1UL << order));
      			if (current_order == pageblock_order)
      				set_pageblock_migratetype(page,
      							start_migratetype);
      			//进行分裂
      			expand(zone, page, order, current_order, area, migratetype);
      			return page;
      		}
      	}
      	/* Use MIGRATE_RESERVE rather than fail an allocation *///如果还是没成功,就只能从紧急分配对象中获取内存
      	return __rmqueue_smallest(zone, order, MIGRATE_RESERVE);
      }
      

内存页面的释放

  • Linux中内存对页面的释放,最终都会调用__free_pages进行处理

    fastcall void __free_pages(struct page *page, unsigned int order){
    	if (put_page_testzero(page)) {//若是单页放回CPU高速缓存,如果多页则放入伙伴系统空闲链表
    		if (order == 0)
    			free_hot_page(page);
    		else
    			__free_pages_ok(page, order);
    	}
    }
    
    • 释放内存页时,会检测pages的阶数
      • 如果阶数等于0,则该页是在cpu的页面队列,使用free_hot_page进行释放,其中最终会调用free_hot_cold_page
      • 如果阶数大于0,则从伙伴系统中对页面进行释放,调用__free_pages_ok实现

    我们先来看看单页释放的过程

单page
free_hot_cold_page

和分配页面相类似,在最开始的时候,并不会马上进行页面分配,而是先对页面做检测

static void fastcall free_hot_cold_page(struct page *page, int cold)
{
	//0阶的释放过程
	struct zone *zone = page_zone(page);//获取该页对应的zone
	struct per_cpu_pages *pcp;
	unsigned long flags;
	if (PageAnon(page))//该页是否是匿名页?
		page->mapping = NULL;
	if (free_pages_check(page))//释放前,对页面状态进行释放
		return;
	if (!PageHighMem(page))//判断该页是否在高端内存
		debug_check_no_locks_freed(page_address(page), PAGE_SIZE);
	arch_free_page(page, 0);//貌似这里会将该页面返回给系统
	kernel_map_pages(page, 1, 0);
	pcp = &zone_pcp(zone, get_cpu())->pcp[cold];//获取冷页链表
	local_irq_save(flags);//锁住cpu
	__count_vm_event(PGFREE);
	list_add(&page->lru, &pcp->list);//应该是将该页加入到pcp的链表中
	set_page_private(page, get_pageblock_migratetype(page));
	pcp->count++;//链表pcp增加
	if (pcp->count >= pcp->high) {//如果pcp中的页面存储数量达到一定限度,就释放到伙伴系统中
		free_pages_bulk(zone, pcp->batch, &pcp->list, 0);//一页,这里阶数就用的0阶
		pcp->count -= pcp->batch;
	}
	local_irq_restore(flags);//恢复
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZBjFAN21-1686223158788)(img/mm_10.png)]

  • 释放前的检测过程
    • 检测
      • 该页是否是匿名页
        • 如果是,就将其映射关系置空
      • 对页面状态进行检测,有效就继续
    • 清理
      • 将该page页面内容清零
      • 并将其重新映射一个虚拟地址上
      • 将该页挂在cpu的冷页链表上
    • 惰性释放
      • 只有一个页的情况下,linux不会立即对其释放,当pcp积累到一定数量上才会进行批处理释放。
free_pages_bulk

这个函数,通过一个循环,将每个页一一释放

static void free_pages_bulk(struct zone *zone, int count,
					struct list_head *list, int order)
/*count:需要回收的数据 list:需要回收的页面order:阶数*/
{
	spin_lock(&zone->lock);
	zone_clear_flag(zone, ZONE_ALL_UNRECLAIMABLE);
	zone->pages_scanned = 0;
	//根据count一直进行遍历
	while (count--) {
		struct page *page;
		VM_BUG_ON(list_empty(list));
		page = list_entry(list->prev, struct page, lru);//从链表上获取该page
		/* have to delete it as __free_one_page list manipulates */
		list_del(&page->lru);//从列表中删除
		__free_one_page(page, zone, order);//将对应页free到伙伴系统中
	}
	spin_unlock(&zone->lock);
}

最终通过调用__free_one_page来实现对页面的释放,多阶的释放也会调用该函数,后面再来了解__free_one_page,先来看看高阶应该怎么处理。

多page
__free_pages_ok

这个函数也只是做一个有效性检测的过程

static void __free_pages_ok(struct page *page, unsigned int order)
{
	unsigned long flags;
	int i;
	int reserved = 0;
	for (i = 0 ; i < (1 << order) ; ++i)
		reserved += free_pages_check(page + i);//判断对应页面是否可以被释放,并进行计数
	if (reserved)
		return;
	if (!PageHighMem(page))
		debug_check_no_locks_freed(page_address(page),PAGE_SIZE<<order);
	arch_free_page(page, order);//将这些页面进行释放
	kernel_map_pages(page, 1 << order, 0);
	local_irq_save(flags);
	__count_vm_events(PGFREE, 1 << order);//统计当前cpu一共释放的页框数
	//获取该page所在zone,
	free_one_page(page_zone(page), page, order);
	local_irq_restore(flags);
}
  • 多page的情况下,也只是对page进行一下合法检测,然后清零后调用free_one_page再进一步释放。 free_one_page中也是直接调用__free_one_page进行处理
__free_one_page

上面将页面都清零了,还需要再做一次处理,看看页面是否还可以继续合并

  • 先对页面做一个检测
    • 是否为复合页
      • 复合页:一个虚拟内存页只映射到一个物理页面。但是,在某些情况下,一个较大的虚拟内存页需要映射到多个物理页面。这时就可以使用复合页来实现。例如,当进程请求大量内存时,操作系统可能会分配一个大的虚拟内存区域,并将其映射到多个物理页面上。这些物理页面组成了一个复合页,被统一管理起来。
      • 所以一个page是复合页,那么就需要破除其的复合页关系
    • 该page索引是不是第一个页框
    • 该page上是否有空洞
  • 通过阶数进行循环,尝试进行合并
    • 找到与当前page同属一个伙伴页块的首地址页框号
    • 判断该页是否能够合并
      • Buddy不在一个空洞中且
      • Buddy在Buddy系统中且
      • 一个页面和它的Buddy具有相同的阶数且
      • 一个页面和它的Buddy在同一个区域内。
    • 如果满足,就将该页进行合并,然后继续循环,直到不能合并为止
  • 将最后合并的页,添加至对应的zone->free_area[order].free_list[migratetype]链表上
    static inline void __free_one_page(struct page *page,
    		struct zone *zone, unsigned int order)
    {
    	unsigned long page_idx;
    	int order_size = 1 << order;
    	//获取迁移类型
    	int migratetype = get_pageblock_migratetype(page);
    
    	if (unlikely(PageCompound(page)))
    		destroy_compound_page(page, order);
    	//获取该页在页块中的索引
    	page_idx = page_to_pfn(page) & ((1 << MAX_ORDER) - 1);
    	//检测1:若释放页不是释放页框的第一个页,则错误
    	VM_BUG_ON(page_idx & (order_size - 1));
    	//检测2:检测是否有空洞
    	VM_BUG_ON(bad_range(zone, page));
    
    	__mod_zone_page_state(zone, NR_FREE_PAGES, order_size);
    	/*
    	释放页块以后,当前页块可能与前后的空闲页块组成更大的空闲页面。存在的话就将空闲页块从伙伴系统中
    	取出来进行合并。循环上述操作,直到无法从伙伴系统中找出合适空闲页块与当前页块进行合并或者order < MAX_ORDER-1为止
    	最后退出循环后,将最终页块添加到伙伴系统对应的空闲链表中
    	*/
    	//大概意思是,可以看看是否还能合并
    	while (order < MAX_ORDER-1) {
    		unsigned long combined_idx;
    		struct page *buddy;
    		//找到与当前页块属于同一个阶的伙伴页块首地址的物理页框号
    		buddy = __page_find_buddy(page, page_idx, order);
    		/*
    		判断释放页块和伙伴页块是否能进行合并操作
    		(a) Buddy不在一个空洞中且
    		(b) Buddy在Buddy系统中且
    		(c) 一个页面和它的Buddy具有相同的阶数且
    		(d) 一个页面和它的Buddy在同一个区域内。
    		*/
    		if (!page_is_buddy(page, buddy, order))
    			break;		/* Move the buddy up one level. */
    		//走到这里应该就是可以合并
    		list_del(&buddy->lru);//将该buddy从列表中删除
    		zone->free_area[order].nr_free--;//当前阶数的空闲数量-1
    		rmv_page_order(buddy);
    		combined_idx = __find_combined_index(page_idx, order);
    		page = page + (combined_idx - page_idx);//计算页地址
    		page_idx = combined_idx;
    		order++;
    	}
    	//上面通过循环的方式,来找到一个块大的页面
    	set_page_order(page, order);
    	list_add(&page->lru,
    		&zone->free_area[order].free_list[migratetype]);//然后将其加入对应阶数的空闲区域中(但是好像上面的方式中,没有考虑迁移类型)
    	zone->free_area[order].nr_free++;
    }
    

内存管理——伙伴系统

图1. 总体结构.

Zone

  Linux中通过对内存功能的不同,进行了划分,分成了ZONE_DMA、ZONE_NORMAL和ZONE_HIGHMEM等部分。

ZONE的区分

(Linux中对Zone的具体划分,待更)

物理内存管理

对物理内存管理,我们就主要关心以下:物理内存管理指什么,如何实现内存页面的分配、如果实现内存页面的释放

伙伴系统

  计算机在使用中,不可避免会遇到重复地申请和释放内存的请求,如果不合理的对内存进行管理,频繁的调入调出可能会使得内存严重的碎片化,不对这种碎片化的情况进行处理,可能会造成一些大页面请求无法满足,从而影响计算机的正常运行。

  • free_area
      为了避免内存的碎片化,Linux采用了不同的伙伴系统算法。伙伴系统下,内存会被进行分别管理——每个zone下会存在一个frea_area的链表,链表中按页面块的大小不同进行划分,其划分的单位是 2 n 2^n 2n,如图2所示。

  • 某种感觉上,伙伴系统一直就是在管理free_area上的数据

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

图2. free_area布局.

  • 迁移类型
      同时根据使用功能的不同,会被设置为不同的迁移类型。
    #define MIGRATE_UNMOVABLE 0 
    #define MIGRATE_RECLAIMABLE 1 
    #define MIGRATE_MOVABLE 2 
    #define MIGRATE_RESERVE 3 
    #define MIGRATE_ISOLATE 4 /* 不能从这里分配 */ 
    #define MIGRATE_TYPES 5
    

  迁移类型的使用,是为了内存管理中出现的碎片化,通过迁移类型的方式将内存页面进行划分,各自管理。

迁移类型含义
MIGRATE_UNMOVABLE不可移动页。这些页包括内核代码、DMA 缓冲区等,它们通常被锁定在内存中且不能被交换出去
MIGRATE_RECLAIMABLE可回收页面。这些页是可以被回收的,但需要进行一定程度的处理才能回收,例如清理缓存
MIGRATE_MOVABLE可移动页面。这些页既不属于不可移动页也不属于可回收页,但它们可以被迁移到其他节点来实现负载均衡
MIGRATE_RESERVE预留页。这些页已经被分配出去,但尚未使用,可以被迁移到其他节点以避免浪费内存
MIGRATE_ISOLATE独立页面。这些页不能被用于分配新的物理页框,通常是为了做某些特殊用途而保留的
MIGRATE_TYPES用于定义free_area链表数量时使用

  在进行内存分配时,可能存在某一个迁移类型的列表无法满足的情况,为了缓解这个情况,伙伴系统允许该迁移列表向其它迁移列表中需求合适的页面。同时,在伙伴系统中,不同的迁移类型向其他迁移列表请求时有不同的遍历顺序

static int fallbacks[MIGRATE_TYPES][MIGRATE_TYPES-1] = {
	[MIGRATE_UNMOVABLE]   = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE,   MIGRATE_RESERVE },
	[MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE,   MIGRATE_MOVABLE,   MIGRATE_RESERVE },
	[MIGRATE_MOVABLE]     = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_RESERVE },
	[MIGRATE_RESERVE]     = { MIGRATE_RESERVE,     MIGRATE_RESERVE,   MIGRATE_RESERVE }, /* Never used */
};

内存页面的分配

标识符-gfp_mask、ALLOC_XXX
  • 在进行内存页面分配之前,需要先了解一些分配标志,这些分配标识规定了分配页面时的不同限制,即gfp_maskALLOC_XXX。一般而言,二者都是指定内存页面的属性,gfp_mask会指定一些影响内存分配算法的属性,ALLOC_XXX则针对页面的特定属性进行管理。
  • gfp_mask允许用户更加细粒度地控制内存分配的行为,而ALLOC_XXX则提供了一些快捷方式来设置一些常见的行为。
  • 这里只提供在分配和释放时会遇到的标志符号
标志符含义
__GFP_WAIT可以等待和重调度?
__GFP_HARDWALL只允许在进程允许运行的CPU所关联的结点分配内存
__GFP_NOMEMALLOC不使用紧急分配链表
__GFP_NOFAIL一直重试,不会失败
__GFP_FS可以调用底层文件系统?
__GFP_NORETRY不重试,可能失败
__GFP_NOWARN禁止分配失败警告
__GFP_HIGH应该访问紧急分配池
标识符含义
ALLOC_WMARK_LOW使用pages_low水印
ALLOC_CPUSET检查内存结点是否对应着指定的CPU集合
ALLOC_WMARK_MIN使用pages_min水印
ALLOC_HARDER试图更努力地分配,即放宽限制
ALLOC_HIGH设置了__GFP_HIGH
ALLOC_NO_WATERMARKS完全不检查水印
ALLOC_WMARK_HIGH检查内存结点是否对应着指定的CPU集合

内存块的释放

内存分配,都是在__alloc_pages中发生

  __alloc_pages在分配页面过程中会根据分配的具体情况,修改分配标识符,来实现内存页面申请。restart:第一次尝试失败后,调整分配策略,再尝试第二次。rebalance:当前两次都申请失败,则会对进行一次调度,再申请最后第三次

图3. restart.

  • 第一次尝试申请
    调用 __alloc_pages 后,做一些简单的测试,设置好分配标志符,进行第一次调用,其分配要求:
  - __GFP_HARDWALL:只允许在进程允许运行的CPU所关联的结点分配内存
  - ALLOC_WMARK_LOW|ALLOC_CPUSET:使用pages_low水印或者检查内存结点是否对应着指定的CPU集合。

水印的要求

pages_high如果空闲页多于pages_high
pages_low将页换出到硬盘
pages_min页回收工作的压力就比较大,因为内存域中急需空闲页
  • 第二次尝试申请
    如果第一次获取不成功,且允许回收内存,则进行一次内存回收
    for (z = zonelist->zones; *z; z++)
    		wakeup_kswapd(*z, order);//这里会对每个zone的内存进行缩减和回收
    
    然后重新修改内存标识符
      alloc_flags = ALLOC_WMARK_MIN;//分配要求进一步降低,水印标记为pages_min水印
      if ((unlikely(rt_task(p)) && !in_interrupt()) || !wait)
      	alloc_flags |= ALLOC_HARDER;
      if (gfp_mask & __GFP_HIGH)
      	alloc_flags |= ALLOC_HIGH;
      if (wait)
      	alloc_flags |= ALLOC_CPUSET;
      page = get_page_from_freelist(gfp_mask, order, zonelist, alloc_flags);
    
    • 首先将alloc_flags设置为ALLOC_WMARK_MIN,表明内存域急需内存页
      进一步根据进程和分配标志的情况对标志符进行修正
      • 如果进程为实时进程且不在中断中
        • alloc_flags设置为ALLOC_HARDER,则希望伙伴系统尽可能地分配页面(实时系统优先级高)
        • 后面的情况就表明该进程为非实时进程或者在中断中
      • 如果gfp_mask & __GFP_HIGH,即表明可以使用紧急备用内存
        • alloc_flags设置为ALLOC_HIGH
      • 如果是可以等待的
        • alloc_flags设置为ALLOC_CPUSET,则还是保持原有设置
    • 重新设置了alloc_flags后,再尝试一次页面获取

图4. rebalance.

  • 第三次尝试申请
    • 检测线程状态

      如果进程标志PF_MEMALLOC,必须分配内存或者当前线程仍在等待释放内存 且没有在中断中

      • 检测gfp_mask,该进程允许使用紧急备用链表 __GFP_NOMEMALLOC
        • 则将alloc_flag设置为无水印的形式,表示不再有要求,有空闲内存就行
    • 经过上述处理还是没有获取内存(表明进程获取内存的意愿并没有那么强烈,那么就可以等等)
      • 进行一次cpu调度
      • 尝试对内存取中的内存进行一次释放
        did_some_progress = try_to_free_pages(zonelist->zones, order, gfp_mask);
        
      • 再进行一次cpu调度
      • 如果释放页面成功,则再进行一次尝试
      • 如果释放页面失败,且gfp_mask为__GFP_FS,表明允许使用底层文件系统
        • 则再调用一次,重新设置内存分配标志,这次只要求页面是当前关联cpu即可
          gfp_mask:__GFP_HARDWALL
          alloc_flag:ALLOC_WMARK_HIGH|ALLOC_CPUSET
          
get_page_from_freelist

get_page_from_freelist会对zonelist进行扫描,然后选取符合要求的要求进行返回(在get_page_from_freelist并不是直接向伙伴系统获取页面,而更像是一个中间层,会通过对相关标志符进行检测,根据zone区域的状态进行调整,选择合适的zone来获取页面)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-naS695TN-1686223159932)(img/mm_6.png)]

  • 循环对zone_list中的内存区的内存进行检测
    • 首先对该zone中的内存区进行一个遍历筛选,返回zonelist中的可用内存区域(如果有,就继续向下检测)
    • alloc_flag 标志进行检测
      • alloc_flag & ALLOC_CPUSET,则必须在指定的cpu上允许
        • 使用 **cpuset_zone_allowed_softwall**检测给定内存域是否属于该进程允许的CPU,如果不属于,则遍历下一个zone
      • 根据alloc_flag设定内存索引标志mark
        • 设定后,尝试从zone中获取,是否有合适的内存页面可以分配 zone_watermark_ok
          • 如果没有,则尝试进行页面回收,如果还是没有,就说明无空闲内存。
          static struct page *
          get_page_from_freelist(gfp_t gfp_mask, unsigned int order,
          		struct zonelist *zonelist, int alloc_flags)
          {
          
          zonelist_scan:
          	/*
          	 * Scan zonelist, looking for a zone with enough free.
          	 * See also cpuset_zone_allowed() comment in kernel/cpuset.c.
          	 */
          	//扫描区域列表,寻找具有足够空闲的区域
          	z = zonelist->zones;
          
          	do {
          		//这里是检测zonelist是否需要过滤?
          		//应该就是找到该z下是否合适gfp_zone,如果不合适,就跳过
          		if (unlikely(alloc_should_filter_zonelist(zonelist))) {
          			if (highest_zoneidx == -1)
          				highest_zoneidx = gfp_zone(gfp_mask);
          			if (zone_idx(*z) > highest_zoneidx)
          				continue;
          		}
          
          		if (NUMA_BUILD && zlc_active &&
          			!zlc_zone_worth_trying(zonelist, z, allowednodes))
          				continue;
          		zone = *z;
          		//alloc_flags要求指定了cpu,但是该zone不合适,则跳过寻找下一个zone:检查给定内存域是否属于该进程允许的CPU
          		//cpuset_zone_allowed_softwall:检查给定内存域是否属于该进程允许运行的CPU
          		if ((alloc_flags & ALLOC_CPUSET) &&
          			!cpuset_zone_allowed_softwall(zone, gfp_mask))
          				goto try_next_zone;
          		/*
          		- 如果需要检测水印(进行分配限制检测)
          			- 如果alloc_flags是ALLOC_WMARK_MIN,则mark指定zone->pages_min
          			- 如果alloc_flags是ALLOC_WMARK_LOW,则mark指定zone->pages_low
          			- 如果都不是,则mark指定zone->pages_high
          		*/
          		if (!(alloc_flags & ALLOC_NO_WATERMARKS)) {
          			unsigned long mark;
          			if (alloc_flags & ALLOC_WMARK_MIN)
          				mark = zone->pages_min;
          			else if (alloc_flags & ALLOC_WMARK_LOW)
          				mark = zone->pages_low;
          			else
          				mark = zone->pages_high;
          			//zone_watermark_ok:判断基于以上分配策略设置,能够从内存区域中获取页面吗
          			//如果不能,且从该zone进行回收还是没有成功,那么就是zone满了,进行跳转
          			//zone_watermark_ok:遍历该内存域,是否有足够的空闲页,并试图分配一个连续内存块
          			//如果不够,且zone_reclaim_mode,就会对内存页面进行回收,如果不能回收说明zone无空闲内存,分配失败
          			if (!zone_watermark_ok(zone, order, mark,
          				    classzone_idx, alloc_flags)) {
          				if (!zone_reclaim_mode ||
          				    !zone_reclaim(zone, gfp_mask, order))
          					goto this_zone_full;
          			}
          		}
          		//走到这里,就是满足,从这里获取页面,上面也是调整gfp_mask
          		page = buffered_rmqueue(zonelist, zone, order, gfp_mask);
          		if (page)//获取成功就跳出
          			break;
          this_zone_full:
          		if (NUMA_BUILD)
          			zlc_mark_zone_full(zonelist, z);
          try_next_zone:
          		if (NUMA_BUILD && !did_zlc_setup) {
          			/* we do zlc_setup after the first zone is tried */
          			allowednodes = zlc_setup(zonelist, alloc_flags);
          			zlc_active = 1;
          			did_zlc_setup = 1;
          		}
          	} while (*(++z) != NULL);//对zone进行循环
          
          	if (unlikely(NUMA_BUILD && page == NULL && zlc_active)) {
          		/* Disable zlc cache for second zonelist scan */
          		zlc_active = 0;
          		goto zonelist_scan;
          	}
          	return page;
          }
          
buffered_rmqueue

buffered_rmqueue和get_page_from_freelist一样,并不承担实际的页面分配功能。

  • get_page_from_freelist中会根据水印标志,检测zone中是否有合适的内存空间可以进行分配。如何存在合适空间的zone,则把实际的分配业务交给buffered_rmqueue处理。
  • 同样,buffered_rmqueue也不承担具体的分配任务,而是申请页面的阶数进行检测,根据检测结果从不同的区域中获取页面。(get_xx中对分配策略方面的标志进行检测,探测zone是否合适,get_xxx处理后,buffered_rmqueue则进一步对页面大小进行区分,实现页面的分配)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hkCoYm2G-1686223159932)(img/mm_7.png)]

  • 当阶数等于0

    • 获取cpu中的冷页链表pcp[cold]
    • 判断pcp->count是否充足,如果充足调用rmqueue_bulk进行获取
    • 判断获取页面的迁移类型migratetype,如果不一致,就需要重新再取一次
  • 当阶数大于0

    • 直接调用__rmqueue进行获取
    static struct page *buffered_rmqueue(struct zonelist *zonelist,
    			struct zone *zone, int order, gfp_t gfp_flags)
    {
    	unsigned long flags;
    	struct page *page;
    	int cold = !!(gfp_flags & __GFP_COLD);
    	int cpu;
    	int migratetype = allocflags_to_migratetype(gfp_flags);//根据gfp_flags(分配标志)来指定不同的迁移域
    again:
    	cpu  = get_cpu();//获取cpu
    	if (likely(order == 0)) {
    		struct per_cpu_pages *pcp;//per_cpu_pages存储cpu中的页表和高速缓存信息
    		pcp = &zone_pcp(zone, cpu)->pcp[cold];//获取cpu对应内存zone的冷页
    		/*禁止本地CPU中断,禁止前先保存中断状态:
    		 *这里需要关中断,因为内存回收过程可能发送核间中断,强制每个核从每CPU缓存中
    		 *释放页面。而且中断处理函数也会分配单页。 
    		 */
    		local_irq_save(flags);
    		//如果pcp中没有数据,则通过rmqueue_bulk进行获取
    		if (!pcp->count) {
    			pcp->count = rmqueue_bulk(zone, 0,
    					pcp->batch, &pcp->list, migratetype);
    			if (unlikely(!pcp->count))//如果还是为空,则分配失败
    				goto failed;
    		}
    		//pcp获取到足够的页面后,则对链表进行遍历,找到合适page
    		list_for_each_entry(page, &pcp->list, lru)//应该是遍历链表,
    			if (page_private(page) == migratetype)//如果page的迁移类型合适,则跳出
    				break;
    		/* Allocate more to the pcp list if necessary */
    		/*
    		如果迁移类型不一致
    		同时page->lru没在pcp->list中,则要从rmqueue_bulk中重新获取相应类型的page
    		*/
    		if (unlikely(&page->lru == &pcp->list)) {
    			pcp->count += rmqueue_bulk(zone, 0,
    					pcp->batch, &pcp->list, migratetype);
    			page = list_entry(pcp->list.next, struct page, lru);
    		}
    		list_del(&page->lru);
    		pcp->count--;
    	} else {
    		spin_lock_irqsave(&zone->lock, flags);
    		page = __rmqueue(zone, order, migratetype);//页面超过1,则从__rmqueue中获取,否则从pcp中获取
    		spin_unlock(&zone->lock);
    		if (!page)
    			goto failed;
    	}
    }
    
rmqueue_bulk

从伙伴分配器中获取指定数量的元素

  • rmqueue_bulk实现上也是调用**__rmqueue**,来获取页面
      static int rmqueue_bulk(struct zone *zone, unsigned int order, 
      			unsigned long count, struct list_head *list,
      			int migratetype){
      	int i;
      	spin_lock(&zone->lock);
      	for (i = 0; i < count; ++i) {
      		struct page *page = __rmqueue(zone, order, migratetype);
      		if (unlikely(page == NULL))
      			break;
      		list_add(&page->lru, list);
      		set_page_private(page, migratetype);
      		list = &page->lru;
      	}
      	spin_unlock(&zone->lock);
      	return i;
      }
    
__rmqueue

static struct page *__rmqueue(struct zone *zone, unsigned int order,int migratetype)

static struct page *__rmqueue(struct zone *zone, unsigned int order,int migratetype){
	struct page *page;
	//扫描页的列表,直至找到适当的连续内存块
	page = __rmqueue_smallest(zone, order, migratetype);
	if (unlikely(!page))
	//如果没有从指定的迁移类型中获取到相应的页面,则尝试从其他迁移列表中获取,作为补救
		page = __rmqueue_fallback(zone, order, migratetype);
	return page;
}

zone:内存区,order:阶数,migratetype:迁移类型

  • __rmqueue会调用 __rmqueue_smallest 来获取页面,如果页面分配失败,则使用备用队列
    __rmqueue_fallback
__rmqueue_smallest

遍历给定迁移类型的空闲列表,并从空闲列表中删除最小可用页

  • __rmqueue_smallest会对zone->freeare中的数据进行遍历,zone->freearea按阶数进行管理,__rmqueue_smallest则从当前阶数向最大阶数进行遍历,找到满足页面请求的最佳区域。
    • 获取 zone->freeare[order] 下的空闲页面链表区域area
    • 判断这片区域下对应migratetype的页面是否还有,如果没有则找下一个阶
    • 如果找到,则将该页面取出,并对area统计计数进行更新。
    • 进行expand(遍历给定的迁移类型的空闲列表,并从空闲列表中删除最小可用页)
      for (current_order = order; current_order < MAX_ORDER; ++current_order) {
          area = &(zone->free_area[current_order]);//找到该zone下对应阶数的空闲内存区域area
          if (list_empty(&area->free_list[migratetype]))//如果该内存区域下,没有对应迁移类型的数据,则跳过
          	continue;
      
          page = list_entry(area->free_list[migratetype].next,
          					struct page, lru);
          list_del(&page->lru);//获取到后,从lru链表中移除该内存块
          rmv_page_order(page);//将该页从伙伴系统中移除
          area->nr_free--;//该区域的空余量-1
          __mod_zone_page_state(zone, NR_FREE_PAGES, - (1UL << order));
          //可能存在分配的页面阶数不高,但是area的阶数高,这是就需要使用expand进行分裂
          expand(zone, page, order, current_order, area, migratetype);
      	return page;
      }
      
  • 走到这一步,基本上就获取到页面了,但是还需要做后续的处理,例如当freeare的阶数高,但是需要的阶数并不高,则需要做一些分裂。
    static struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,int migratetype){
    	unsigned int current_order;
    	struct free_area * area;
    	struct page *page;
    	//根据阶数进行遍历
    	for (current_order = order; current_order < MAX_ORDER; ++current_order) {
    		area = &(zone->free_area[current_order]);//找到该zone下对应阶数的空闲内存区域area
    		if (list_empty(&area->free_list[migratetype]))//如果该内存区域下,没有对应迁移类型的数据,则跳过
    			continue;
    		page = list_entry(area->free_list[migratetype].next,
    							struct page, lru);
    		list_del(&page->lru);//获取到后,从lru链表中移除该内存块
    		rmv_page_order(page);//将该页从伙伴系统中移除
    		area->nr_free--;//该区域的空余量-1
    		__mod_zone_page_state(zone, NR_FREE_PAGES, - (1UL << order));
    		//可能存在分配的页面阶数不高,但是area的阶数高,这是就需要使用expand进行分裂
    		expand(zone, page, order, current_order, area, 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)
zone:提取页面的区域
page:对应的page
low:预期需要的分配阶
high:该内存取自的分配阶
area:空闲区域
migratetype:迁移类型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UkuUeiZo-1686223159933)(img/mm_8.png)]

  • 指定的阶数够大,期望阶数不大,则需要进行分裂
    分裂的方法:
    • area中向下走一步到低一个阶的区域,然后size取半
      将后一半内存块放进该area中,然后依次遍历直到获取到low阶的页面,遍历过程中page没动,但通过size在切割page,到对应的free_list中去。
      while (high > low) {
          //area阶数向后退一步
          area--;
          high--;//要放回的链表阶
          size >>= 1;//将size向后移位,并将后半部分放入area对应的空闲队列中
          VM_BUG_ON(bad_range(zone, &page[size]));
          list_add(&page[size].lru, &area->free_list[migratetype]);//这里就相当于将内存页面进行分裂,扩展到对应阶数的area中去
          area->nr_free++;//该阶下的area中的空闲内存量+1
          set_page_order(&page[size], high);
      }
      
    • expand执行结束后,正常流程中,页面分配也就结束了。但还有一个问题没有解决,如果__rmqueue_smallest失败了,该怎么办。为了尽可能地分配内存,Linux会从备选列表中继续尝试分配__rmqueue_fallback。
      static inline void expand(struct zone *zone, struct page *page,
      	int low, int high, struct free_area *area,
      	int migratetype)
      	/*low:是预期的分配阶,high:表示内存取自哪个分配阶*/
      {
      	unsigned long size = 1 << high;
      	//每比较一次order向后退一次
      	while (high > low) {
      		//area阶数向后退一步
      		area--;
      		high--;//要放回的链表阶
      		size >>= 1;//将size向后移位,并将后半部分放入area对应的空闲队列中
      		VM_BUG_ON(bad_range(zone, &page[size]));
      		list_add(&page[size].lru, &area->free_list[migratetype]);//这里就相当于将内存页面进行分裂,扩展到对应阶数的area中去
      		area->nr_free++;//该阶下的area中的空闲内存量+1
      		set_page_order(&page[size], high);
      	}
      }
      
__rmqueue_fallback

从伙伴系统的备选列表中获取元素
Linux系统中,会根据备选选项的不同,维护了一个备选的迁移类型选项
在对备选内存进行分配的原则是,从大到小遍历,避免碎片,如果分配到的迁移类型不同,则尽可能让获取的内存足够大
如果移动这片内存是可回收的内存块,并且这片内存块中有一半空闲,则修改为目标迁移类型

  • 迁移列表的备选队列

    static int fallbacks[MIGRATE_TYPES][MIGRATE_TYPES-1] = {
      [MIGRATE_UNMOVABLE]   = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE,   MIGRATE_RESERVE },
      [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE,   MIGRATE_MOVABLE,   MIGRATE_RESERVE },
      [MIGRATE_MOVABLE]     = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_RESERVE },
      [MIGRATE_RESERVE]     = { MIGRATE_RESERVE,     MIGRATE_RESERVE,   MIGRATE_RESERVE }, /* Never used */
      };
    
  • __rmqueue_smallest中,通过 从小到大的方式,找到最合适的阶的free_area,然后获取。__rmqueue_fallback同样,对空闲列表进行遍历,只是不同在于__rmqueue_fallback对其他迁移类型的空闲队列进行遍历。

  • __rmqueue_fallback在进行遍历时,会从最大阶开始遍历,这样做的原因是为了避免小碎片过多(从大的开始,这样做即使分裂,剩下的也是大碎片)

    • 遍历备选空闲队列

      • 不到必要时刻,不对MIGRATE_RESERVE的内存对象进行处理
    • 如果迁移类型合适,则将起对应的free_list中删除

    • 若满足以下条件,则修改该片内存的迁移类型

      • 阶数>pageblock_order 或者起始的页面迁移类型是可回收类型MIGRATE_RECLAIMABLE
      • 满足条件,就将备选迁移类型的页面转移到需要的页面的迁移类型队列中
    • 获得相应内存后,就进行expand

    • 如果经过上面的处理后,还是没能实现对页面的获取,就启用紧急备用页面

      __rmqueue_smallest(zone, order, MIGRATE_RESERVE);
      

      static struct page *__rmqueue_fallback(struct zone *zone, int order,
      						int start_migratetype)
      {
      	struct free_area * area;
      	int current_order;
      	struct page *page;
      	int migratetype, i;
      	//在Linux系统中,备用迁移域是有一个搜索顺序的
      	/* Find the largest possible block of pages in the other list */
      	//阶数从大到小进行遍历,这样尽量对大块内存进行分割,避免过多的碎片
      	//内核的分配策略是,如果无法避免分配迁移类型不同的内存块,则分配一个尽可能大的内存块
      	for (current_order = MAX_ORDER-1; current_order >= order;
      						--current_order) {
      		//根据搜索顺序,来获取相应的内存块
      		for (i = 0; i < MIGRATE_TYPES - 1; i++) {
      			migratetype = fallbacks[start_migratetype][i];
      			/* MIGRATE_RESERVE handled later if necessary */
      			if (migratetype == MIGRATE_RESERVE)//应该是不到迫不得己不适用紧急分配内存
      				continue;
      			area = &(zone->free_area[current_order]);//获取area
      			if (list_empty(&area->free_list[migratetype]))
      				continue;
      
      			page = list_entry(area->free_list[migratetype].next,
      					struct page, lru);//从area中获取对应类型的page
      			area->nr_free--;//统计量-1
      
      			/*
      			 * 如果分解一个大内存块,则将所有空闲页移动到优先选用的分配列表。
      			 * 如果内核在备用列表中分配可回收内存块,则会更为积极地取得空闲页的所有权
      			 * pageblock_order就是Linux认为的大内存的界限,如果没有超过这个值,就不会进行所有权迁移
      			 */
      			if (unlikely(current_order >= (pageblock_order >> 1)) ||
      					start_migratetype == MIGRATE_RECLAIMABLE) {
      				unsigned long pages;
      				//这里试图将pageblock_order阶数个页的整个内存块转移到新的迁移列表中(转移迁移列表,但只有空闲页才会移动)
      				pages = move_freepages_block(zone, page,
      								start_migratetype);
      				//如果大内存中有1/2是空闲的,则修改整个内存块的迁移类型(应该是将移动的部分的迁移类型进行修改)
      				//如果只是少部分,应该还是不修改的
      				/* Claim the whole block if over half of it is free */
      				if (pages >= (1 << (pageblock_order-1)))
      					//修改整个内存块的迁移类型
      					set_pageblock_migratetype(page,
      								start_migratetype);
      				migratetype = start_migratetype;
      			}
      			/* Remove the page from the freelists */
      			list_del(&page->lru);//从空闲列表中移除
      			rmv_page_order(page);
      			__mod_zone_page_state(zone, NR_FREE_PAGES,
      							-(1UL << order));
      			if (current_order == pageblock_order)
      				set_pageblock_migratetype(page,
      							start_migratetype);
      			//进行分裂
      			expand(zone, page, order, current_order, area, migratetype);
      			return page;
      		}
      	}
      	/* Use MIGRATE_RESERVE rather than fail an allocation *///如果还是没成功,就只能从紧急分配对象中获取内存
      	return __rmqueue_smallest(zone, order, MIGRATE_RESERVE);
      }
      

内存页面的释放

  • Linux中内存对页面的释放,最终都会调用__free_pages进行处理

    fastcall void __free_pages(struct page *page, unsigned int order){
    	if (put_page_testzero(page)) {//若是单页放回CPU高速缓存,如果多页则放入伙伴系统空闲链表
    		if (order == 0)
    			free_hot_page(page);
    		else
    			__free_pages_ok(page, order);
    	}
    }
    
    • 释放内存页时,会检测pages的阶数
      • 如果阶数等于0,则该页是在cpu的页面队列,使用free_hot_page进行释放,其中最终会调用free_hot_cold_page
      • 如果阶数大于0,则从伙伴系统中对页面进行释放,调用__free_pages_ok实现

    我们先来看看单页释放的过程

单page
free_hot_cold_page

和分配页面相类似,在最开始的时候,并不会马上进行页面分配,而是先对页面做检测

static void fastcall free_hot_cold_page(struct page *page, int cold)
{
	//0阶的释放过程
	struct zone *zone = page_zone(page);//获取该页对应的zone
	struct per_cpu_pages *pcp;
	unsigned long flags;
	if (PageAnon(page))//该页是否是匿名页?
		page->mapping = NULL;
	if (free_pages_check(page))//释放前,对页面状态进行释放
		return;
	if (!PageHighMem(page))//判断该页是否在高端内存
		debug_check_no_locks_freed(page_address(page), PAGE_SIZE);
	arch_free_page(page, 0);//貌似这里会将该页面返回给系统
	kernel_map_pages(page, 1, 0);
	pcp = &zone_pcp(zone, get_cpu())->pcp[cold];//获取冷页链表
	local_irq_save(flags);//锁住cpu
	__count_vm_event(PGFREE);
	list_add(&page->lru, &pcp->list);//应该是将该页加入到pcp的链表中
	set_page_private(page, get_pageblock_migratetype(page));
	pcp->count++;//链表pcp增加
	if (pcp->count >= pcp->high) {//如果pcp中的页面存储数量达到一定限度,就释放到伙伴系统中
		free_pages_bulk(zone, pcp->batch, &pcp->list, 0);//一页,这里阶数就用的0阶
		pcp->count -= pcp->batch;
	}
	local_irq_restore(flags);//恢复
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4LY19wSf-1686223159933)(img/mm_10.png)]

  • 释放前的检测过程
    • 检测
      • 该页是否是匿名页
        • 如果是,就将其映射关系置空
      • 对页面状态进行检测,有效就继续
    • 清理
      • 将该page页面内容清零
      • 并将其重新映射一个虚拟地址上
      • 将该页挂在cpu的冷页链表上
    • 惰性释放
      • 只有一个页的情况下,linux不会立即对其释放,当pcp积累到一定数量上才会进行批处理释放。
free_pages_bulk

这个函数,通过一个循环,将每个页一一释放

static void free_pages_bulk(struct zone *zone, int count,
					struct list_head *list, int order)
/*count:需要回收的数据 list:需要回收的页面order:阶数*/
{
	spin_lock(&zone->lock);
	zone_clear_flag(zone, ZONE_ALL_UNRECLAIMABLE);
	zone->pages_scanned = 0;
	//根据count一直进行遍历
	while (count--) {
		struct page *page;
		VM_BUG_ON(list_empty(list));
		page = list_entry(list->prev, struct page, lru);//从链表上获取该page
		/* have to delete it as __free_one_page list manipulates */
		list_del(&page->lru);//从列表中删除
		__free_one_page(page, zone, order);//将对应页free到伙伴系统中
	}
	spin_unlock(&zone->lock);
}

最终通过调用__free_one_page来实现对页面的释放,多阶的释放也会调用该函数,后面再来了解__free_one_page,先来看看高阶应该怎么处理。

多page
__free_pages_ok

这个函数也只是做一个有效性检测的过程

static void __free_pages_ok(struct page *page, unsigned int order)
{
	unsigned long flags;
	int i;
	int reserved = 0;
	for (i = 0 ; i < (1 << order) ; ++i)
		reserved += free_pages_check(page + i);//判断对应页面是否可以被释放,并进行计数
	if (reserved)
		return;
	if (!PageHighMem(page))
		debug_check_no_locks_freed(page_address(page),PAGE_SIZE<<order);
	arch_free_page(page, order);//将这些页面进行释放
	kernel_map_pages(page, 1 << order, 0);
	local_irq_save(flags);
	__count_vm_events(PGFREE, 1 << order);//统计当前cpu一共释放的页框数
	//获取该page所在zone,
	free_one_page(page_zone(page), page, order);
	local_irq_restore(flags);
}
  • 多page的情况下,也只是对page进行一下合法检测,然后清零后调用free_one_page再进一步释放。 free_one_page中也是直接调用__free_one_page进行处理
__free_one_page

上面将页面都清零了,还需要再做一次处理,看看页面是否还可以继续合并

  • 先对页面做一个检测
    • 是否为复合页
      • 复合页:一个虚拟内存页只映射到一个物理页面。但是,在某些情况下,一个较大的虚拟内存页需要映射到多个物理页面。这时就可以使用复合页来实现。例如,当进程请求大量内存时,操作系统可能会分配一个大的虚拟内存区域,并将其映射到多个物理页面上。这些物理页面组成了一个复合页,被统一管理起来。
      • 所以一个page是复合页,那么就需要破除其的复合页关系
    • 该page索引是不是第一个页框
    • 该page上是否有空洞
  • 通过阶数进行循环,尝试进行合并
    • 找到与当前page同属一个伙伴页块的首地址页框号
    • 判断该页是否能够合并
      • Buddy不在一个空洞中且
      • Buddy在Buddy系统中且
      • 一个页面和它的Buddy具有相同的阶数且
      • 一个页面和它的Buddy在同一个区域内。
    • 如果满足,就将该页进行合并,然后继续循环,直到不能合并为止
  • 将最后合并的页,添加至对应的zone->free_area[order].free_list[migratetype]链表上
    static inline void __free_one_page(struct page *page,
    		struct zone *zone, unsigned int order)
    {
    	unsigned long page_idx;
    	int order_size = 1 << order;
    	//获取迁移类型
    	int migratetype = get_pageblock_migratetype(page);
    
    	if (unlikely(PageCompound(page)))
    		destroy_compound_page(page, order);
    	//获取该页在页块中的索引
    	page_idx = page_to_pfn(page) & ((1 << MAX_ORDER) - 1);
    	//检测1:若释放页不是释放页框的第一个页,则错误
    	VM_BUG_ON(page_idx & (order_size - 1));
    	//检测2:检测是否有空洞
    	VM_BUG_ON(bad_range(zone, page));
    
    	__mod_zone_page_state(zone, NR_FREE_PAGES, order_size);
    	/*
    	释放页块以后,当前页块可能与前后的空闲页块组成更大的空闲页面。存在的话就将空闲页块从伙伴系统中
    	取出来进行合并。循环上述操作,直到无法从伙伴系统中找出合适空闲页块与当前页块进行合并或者order < MAX_ORDER-1为止
    	最后退出循环后,将最终页块添加到伙伴系统对应的空闲链表中
    	*/
    	//大概意思是,可以看看是否还能合并
    	while (order < MAX_ORDER-1) {
    		unsigned long combined_idx;
    		struct page *buddy;
    		//找到与当前页块属于同一个阶的伙伴页块首地址的物理页框号
    		buddy = __page_find_buddy(page, page_idx, order);
    		/*
    		判断释放页块和伙伴页块是否能进行合并操作
    		(a) Buddy不在一个空洞中且
    		(b) Buddy在Buddy系统中且
    		(c) 一个页面和它的Buddy具有相同的阶数且
    		(d) 一个页面和它的Buddy在同一个区域内。
    		*/
    		if (!page_is_buddy(page, buddy, order))
    			break;		/* Move the buddy up one level. */
    		//走到这里应该就是可以合并
    		list_del(&buddy->lru);//将该buddy从列表中删除
    		zone->free_area[order].nr_free--;//当前阶数的空闲数量-1
    		rmv_page_order(buddy);
    		combined_idx = __find_combined_index(page_idx, order);
    		page = page + (combined_idx - page_idx);//计算页地址
    		page_idx = combined_idx;
    		order++;
    	}
    	//上面通过循环的方式,来找到一个块大的页面
    	set_page_order(page, order);
    	list_add(&page->lru,
    		&zone->free_area[order].free_list[migratetype]);//然后将其加入对应阶数的空闲区域中(但是好像上面的方式中,没有考虑迁移类型)
    	zone->free_area[order].nr_free++;
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值