电子说
0 CPU页帧缓存概念
在前一节中,我们学习了buddy伙伴关系系统,它适用于申请连续的大块物理内存;而有些时候,经常需要申请和释放单个页帧。但是,如果使用伙伴关系系统,需要查表、进行合并等操作,效率不高。为了提高性能,每个内存ZONE区都提供了一个per-CPU变量,CPU页帧缓存。每个CPU页帧缓存都包含一些预分配好的页帧,满足本地CPU发起的单个页帧请求。
实际上,每个内存ZONE区和每个CPU都有2个缓存:一个是热缓存,它存储页帧,其内容可能包含在CPU的硬件缓存中;另一个是冷缓存。
如果内核或用户进程在分配后立即写入页帧,那么从热缓存中获取页帧将有利于系统性能。实际上,每次访问页帧的某个内存位置,都会导致硬件Cache中替换其它页帧的某一行(Cache-line),当然,除非硬件Cache已经包含刚刚访问的“热”页帧中内存位置的一行。
相反,如果要用DMA操作填充页帧,则从冷缓存中取页帧是很方便的。在这种情况下,不涉及CPU,也不会修改硬件Cache的任何行。从冷缓存中取页帧可以为其他类型的内存分配请求保留热页帧。
CPU页帧缓存的数据结构是per_cpu_pageset类型的数组,其存储在内存ZONE描述符中的pageset成员中,如下面的代码所示:
struct zone { /* ... */ struct per_cpu_pageset pageset[NR_CPUS]; /* ... */ }
数组个数与CPU个数相关,其中的每个数组元素又包含2个per_cpu_pages描述符成员:一个是热缓存;另一个是冷缓存。而per_cpu_pages数据类型的成员如下表所示:
struct per_cpu_pages { int count; /* 缓存中的页帧数量 */ int low; /* 阈值下限,用于缓存补充 */ int high; /* 阈值上限,需要清空缓存 */ int batch; /* 需从缓存中添加或减少的页帧数 */ struct list_head list; /* 缓存中页帧描述符列表,即内存页列表 */ };
内核使用两个阈值(low和high)监控冷/热缓存的大小:如果页帧数量低于阈值,则内核使用伙伴系统分配一定数量的单个页帧(batch);否则,页帧数量超过阈值上限,内核将缓存中的页帧释放到伙伴系统中(batch)。batch、low和high的值,具体依赖于内存ZONE区的页帧数量。
1 通过CPU页帧缓存分配页帧
buffered_rmqueue()函数在给定的内存ZONE区中分配页帧。它利用CPU页帧缓存来处理单个页帧请求。
Linux v2.6.11内核源码实现如下所示(文件位置:/mm/page_alloc.c):
static struct page * buffered_rmqueue(struct zone *zone, int order, int gfp_flags) { unsigned long flags; struct page *page = NULL; int cold = !!(gfp_flags & __GFP_COLD); if (order == 0) { struct per_cpu_pages *pcp; pcp = &zone->pageset[get_cpu()].pcp[cold]; local_irq_save(flags); if (pcp->count <= pcp->low) pcp->count += rmqueue_bulk(zone, 0, pcp->batch, &pcp->list); if (pcp->count) { page = list_entry(pcp->list.next, struct page, lru); list_del(&page->lru); pcp->count--; } local_irq_restore(flags); put_cpu(); } if (page == NULL) { spin_lock_irqsave(&zone->lock, flags); page = __rmqueue(zone, order); spin_unlock_irqrestore(&zone->lock, flags); } if (page != NULL) { BUG_ON(bad_range(zone, page)); mod_page_state_zone(zone, pgalloc, 1 << order); prep_new_page(page, order); if (gfp_flags & __GFP_ZERO) prep_zero_page(page, order, gfp_flags); if (order && (gfp_flags & __GFP_COMP)) prep_compound_page(page, order); } return page; }
输入参数分别是内存ZONE区的描述符的地址(zone)、内存分配请求大小(2^order)和分配标志gfp_flags。如果在gfp_flags中设置了__GFP_COLD标志,则应从冷缓存中获取页帧,否则应从热缓存中获取页帧(此标志仅对单个页帧请求有意义)。该函数基本上执行以下操作:
如果order不等于0,则页帧缓存不能使用,函数直接跳转到第4步。
检查由__GFP_COLD标志标识的内存ZONE区域的CPU缓存是否必须被补充(per_cpu_pages的count ≤ low)。在本例中,它执行以下子步骤:
重复调用__rmqueue()函数,从伙伴系统中分配batch个页帧。
将分配的页帧描述符插入到缓存的列表中。
更新count变量(将新分配的页帧数量加上)。
如果count > 0,从缓存列表中取一个页帧,然后跳转到第5步。(CPU页帧缓存可能是空的,在第2步的__rmqueue()没有申请到页帧时就会发生)
到这儿,如果内存请求没有被满足,调用__rmqueue()申请从伙伴系统中分配所请求页帧。
如果内存请求被满足,初始化该页帧(第1个)的页描述符:清除某些标志、设置private为0,设置页帧引用计数器为1。另外,如果设置了__GPF_ZERO,将申请的内存清零。
返回页帧(第1个)的描述符,失败返回NULL。
2 通过CPU页帧缓存释放页帧
从CPU页帧缓存中释放页帧,使用free_hot_page()和free_cold_page()函数。它们都是free_hot_cold_page()的封装函数,如下所示(文件位置:/mm/page_alloc.c):
static void fastcall free_hot_cold_page(struct page *page, int cold) { struct zone *zone = page_zone(page); struct per_cpu_pages *pcp; unsigned long flags; arch_free_page(page, 0); kernel_map_pages(page, 1, 0); inc_page_state(pgfree); if (PageAnon(page)) page->mapping = NULL; free_pages_check(__FUNCTION__, page); pcp = &zone->pageset[get_cpu()].pcp[cold]; local_irq_save(flags); if (pcp->count >= pcp->high) pcp->count -= free_pages_bulk(zone, pcp->batch, &pcp->list, 0); list_add(&page->lru, &pcp->list); pcp->count++; local_irq_restore(flags); put_cpu(); }
free_hot_cold_page()接受的参数是待释放页帧的描述符地址page,表示热缓存还是冷缓存的标志cold。
执行的步骤如下:
根据页帧,获取page->flags标志。
根据cold标志获取对应页帧缓存的描述符per_cpu_pages地址。
检查缓存是否不足:如果count ≥ high,调用free_pages_bulk()函数。该函数会重复调用__free_pages_bulk()函数释放指定的页帧到伙伴系统中。
将该页帧添加到缓存列表中,增加count计数。
应该注意的是,在Linux v2.6内核中,没有任何页帧被释放到冷缓存中:内核总是假设释放的页帧相对于硬件缓存来说是热的。当然,这并不意味着冷缓存是空的:当达到低阈值时,缓存由buffered_rmqueue()补充。
3 移除__GFP_COLD
虽然我们前边分析了基于冷热缓存的CPU页帧缓存,但是,从v4.14版本以后的内核中已经移除,参考patch。Patches 1-4是与移除冷缓存最相关的部分;Patches 5-8是可选的,因为它们都是删除无用但也不影响性能的代码。
free_hot_cold_page的大多数调用者用户都声称被释放的页是热缓存的。唯一的例外是页回收代码,因为在不久的将来可能会释放足够多的页,因此CPU的本地页帧缓存列表将被回收,热缓存信息将丢失。由于没有人真正关心被释放到分配器的页的热信息,所以省略该参数即可。
审核编辑:刘清
全部0条评论
快来发表一下你的评论吧 !