内存管理——伙伴系统
图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_mask、ALLOC_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后,再尝试一次页面获取
- 首先将alloc_flags设置为ALLOC_WMARK_MIN,表明内存域急需内存页
图4. rebalance.
- 第三次尝试申请
- 检测线程状态
如果进程标志PF_MEMALLOC,必须分配内存或者当前线程仍在等待释放内存 且没有在中断中
- 检测gfp_mask,该进程允许使用紧急备用链表 __GFP_NOMEMALLOC
- 则将alloc_flag设置为无水印的形式,表示不再有要求,有空闲内存就行
- 检测gfp_mask,该进程允许使用紧急备用链表 __GFP_NOMEMALLOC
- 经过上述处理还是没有获取内存(表明进程获取内存的意愿并没有那么强烈,那么就可以等等)
- 进行一次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
- 则再调用一次,重新设置内存分配标志,这次只要求页面是当前关联cpu即可
- 检测线程状态
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; }
- 设定后,尝试从zone中获取,是否有合适的内存页面可以分配 zone_watermark_ok
- alloc_flag & ALLOC_CPUSET,则必须在指定的cpu上允许
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); } }
- area中向下走一步到低一个阶的区域,然后size取半
__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实现
我们先来看看单页释放的过程
- 释放内存页时,会检测pages的阶数
单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_mask、ALLOC_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后,再尝试一次页面获取
- 首先将alloc_flags设置为ALLOC_WMARK_MIN,表明内存域急需内存页
图4. rebalance.
- 第三次尝试申请
- 检测线程状态
如果进程标志PF_MEMALLOC,必须分配内存或者当前线程仍在等待释放内存 且没有在中断中
- 检测gfp_mask,该进程允许使用紧急备用链表 __GFP_NOMEMALLOC
- 则将alloc_flag设置为无水印的形式,表示不再有要求,有空闲内存就行
- 检测gfp_mask,该进程允许使用紧急备用链表 __GFP_NOMEMALLOC
- 经过上述处理还是没有获取内存(表明进程获取内存的意愿并没有那么强烈,那么就可以等等)
- 进行一次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
- 则再调用一次,重新设置内存分配标志,这次只要求页面是当前关联cpu即可
- 检测线程状态
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; }
- 设定后,尝试从zone中获取,是否有合适的内存页面可以分配 zone_watermark_ok
- alloc_flag & ALLOC_CPUSET,则必须在指定的cpu上允许
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); } }
- area中向下走一步到低一个阶的区域,然后size取半
__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实现
我们先来看看单页释放的过程
- 释放内存页时,会检测pages的阶数
单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++; }