前言
内存管理是内核设计十分重要的一环,0.12 版本的内存管理相对简单,理解比较容易。但 2.6 版本的内存管理使用了额外的很多数据结构,需要花些时间查资料进行理解。本篇内容主要对 2.6 版本内核的内存管理方法进行记录。
内存管理简述
内存管理主要指的物理内存的管理,与用户空间中内存分配无关。如何将物理内存高效且有序地分配是内存管理需要考虑的问题。对于一块物理内存地址,为了有序地进行分配,最先想到的方法是将其按一定大小进行划分,如划分为物理页,每个页4kb。那么每次申请地时候,可以按页进行分配。对于每一页,应该有特殊的结构体 page 与其对应,进行管理。
结构体 page 有很多,因此需要设计一个数组 mem_map 进行 page 的存储。page 上需要维护该物理页的使用状态等。那么,当需要申请一块空闲的物理页时,只需要遍历 page 数组,获取空闲的page,将该结构体的线性地址返回,这是 alloc_page 干的事。
然而,alloc_page 只能够获取 page 结构体地址,实际使用时需要的是对应物理页的起始线性地址。因此需要地址转换方法,将 page 结构体地址 —》物理页物理地址—》物理页线性地址。该过程由 page_address 完成。具体地,page 结构体地址 —》物理页物理地址过程可有,page 对应物理页地址 = (page - mem_map) * 4k + 物理页起始物理地址。对于 物理页物理地址—》物理页线性地址则需要区分低端内存还是高端内存。
低端内存中,物理地址与线性地址有一一对应的永久关系。而高端内存需要根据自行设置的映射关系进行映射。
此外,由于不同类型计算机管理内存方式不同,可分为 UMA 计算机(一致内存访问,uniform memory access)和 NUMA 计算机(非一致内存访问,non-uniform memory access)。其主要区别在于CPU与内存的对应关系,如下图所示(来自《深入Linux内核架构》)
因此,对于每个 CPU 都需要定义一个结构体 node 进行内存管理,称为 pg_data_t。进一步地,又因为内存还分为DMA,低端内存,高端内存,又进一步地划分为内存域,结构体为 zone 。再之后,才具体到每个 zone 中的 物理页 page。
从上到下,内存管理的几个关键数据结构的关系为 pg_data_t -> zone -> mem_map -> page。因此,当需要申请 page 时,需要从 zone 中进行分配,而内存为 zone 分配 page 的过程设计了伙伴系统来执行。结构体之间的关系如下图所示
具体地,伙伴系统的结构比较简单,但其分配效率十分高效,如下图所示。通过最左边的 free_list 数组关联 MAX_ORDER 个链表头,每个链表头对应大小不同的物理页。具体分配策略在此先不展开。
上述内容从最顶层的节点 pg_data_t 介绍到了 page。但实际上,page并不是内核可分配的最小单位,因为一个 page 有4k这么大。为了加速小内存对象的申请分配,内核设计了slab层。在此使用 《简说Linux之slab机制》 做的图,能够很好地说明slab层的设计思想
即,cache_chain 关联了多个不同大小的 kmem_cache,每个 kmem_cache 关联一个 slabs ,slabs 由多个 slab 组成,每个 slab 又对应多个 page,每个 page 按照 kmem_cache 对应大小平分了多个 object 空间。实际上,slab 的实现与 linux 0.12 版本中 内存桶的思想较为类似。 《深入Linux内核架构》中也有相关图示,如下
基于上述内容,我们能够对内核中的内存管理有个大致的了解。接下来,可以从内核源码对此进行求证,进而加深印象
代码验证
整体结构
我们验证一下上图3-3的组织结构
struct bootmem_data;
typedef struct pglist_data {
struct zone node_zones[MAX_NR_ZONES];
//
} pg_data_t;
struct zone {
struct free_area free_area[MAX_ORDER];
} ____cacheline_internodealigned_in_smp;
struct free_area {
struct list_head free_list[MIGRATE_TYPES];
unsigned long nr_free;
};
struct page {
struct list_head lru; /* Pageout list, eg. active_list
};
由上可知,pg_data_t 通过 node_zones 管理 zone ,而 zone 中存在 free_area,该结构是一个 list_head 数组。而 page 中 lru 字段属性为 list_head。因此可以猜测,通过 list_entry(list->prev, struct page, lru) 可以获得 free_area 中第 n 个 list_head 的 page 结构体。
上述也提到分配物理页的函数为 alloc_pages。其通过实现伙伴系统完成 page 的分配。具体地,真正执行分配的函数为 __rmqueue_smallest ,其调用链为
alloc_pages
||
alloc_pages_node
||
__alloc_pages
||
__alloc_pages_nodemask
||
get_page_from_freelist
||
buffered_rmqueue ==== __rmqueue ==== __rmqueue_smallest
该函数会遍历 zone 的 free_list,来获取需要的 page,然后将其更新 expand
static inline
struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
int migratetype)
{
unsigned int current_order;
struct free_area * area;
struct page *page;
/* Find a page of the appropriate size in the preferred list */
for (current_order = order; current_order < MAX_ORDER; ++current_order) {
area = &(zone->free_area[current_order]);
if (list_empty(&area->free_list[migratetype]))
continue;
page = list_entry(area->free_list[migratetype].next,
struct page, lru);
list_del(&page->lru);
rmv_page_order(page);
area->nr_free--;
expand(zone, page, order, current_order, area, migratetype);
return page;
}
return NULL;
}
地址转换
该功能主要通过 page_address 来实现。如前所述,由 page 地址获得物理页的线性地址,需要判断当前页是在高端内存还是低端内存。简单地说,低端内存指的是与内核地址空间存在永久的一一映射关系的内存。因为进程空间中,内核空间只有1G,而实际与内存地址有一一映射关系的仅为 896M。这 896M 与物理内存上 896M 地址为永久映射关系。剩下的需要通过高端内存机制进行映射。不然,1G 内核地址空间如何管理例如 8G 的物理内存。
因此,在 page_address 中需要先通过 pageHighMem 判断是否为高端内存。如果为低端内存,则通过 lowmen_page_address 完成低端内存映射,映射逻辑很简单,只需要先计算出物理页物理地址,再减去一个定值即可获得线性地址。而如果为高端内存映射,则涉及高端内存映射机制。
void *page_address(struct page *page)
{
unsigned long flags;
void *ret;
struct page_address_slot *pas;
if (!PageHighMem(page))
return lowmem_page_address(page);
pas = page_slot(page);
ret = NULL;
spin_lock_irqsave(&pas->lock, flags);
if (!list_empty(&pas->lh)) {
struct page_address_map *pam;
list_for_each_entry(pam, &pas->lh, list) {
if (pam->page == page) {
ret = pam->virtual;
goto done;
}
}
}
done:
spin_unlock_irqrestore(&pas->lock, flags);
return ret;
}
若为高端内存,则只需要从存储了高端内存映射的结构体中寻找与当前物理地址匹配的线性地址即可。对应地,先通过 page_slot 获取存储了高端内存映射的结构体 page_address_slot ,其 lh 为映射所在链表。具体地,page_address_slot 所存储的高端内存映射主要指高端内存中的永久性映射,其通过kmap进行创建(可参考 https://zhuanlan.zhihu.com/p/69329911?ivk_sa=1024320u)
也就意味着,通过 page_address 只能完成低端内存页以及高端内存中的永久性映射页的线性地址映射
同时,可以看到 get_free_pgae 则将 alloc_page 与 page_address 合起来,因此可以用它快速地获得一个空闲 page 对应的线性地址。
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
struct page *page;
/*
* __get_free_pages() returns a 32-bit address, which cannot represent
* a highmem page
*/
VM_BUG_ON((gfp_mask & __GFP_HIGHMEM) != 0);
page = alloc_pages(gfp_mask, order);
if (!page)
return 0;
return (unsigned long) page_address(page);
}
slab 分配
上述提到,物理页并不是内存管理的最小单位,为了减少内部碎片,提出了 slab 分配机制。该机制其实是内存桶的复杂版,主要入口函数为 kmalloc 。其核心思想与内存桶类似,即在物理页上进一步划分连续的相同大小对象,并对此进行管理。整个架构图如下所示
- array_cache: 用于进行 slab 缓存。当需要分配对象时,先从缓存中找,找不到再去 slab 空闲链表中获取。不同 cpu 对应不同 array_cache
- slab :slab 头部由双向链表串联。根据头部能够使用对应页中所分配好的 slab 对象块。其分为 3类 slab ,分别为满 slab,部分可用 slab,以及空闲 slab
- kmem_cache :管理一整个结构。不同类型对象的 kmem_cache 不同。
而每个 kmem_cache 通过双向链表组织
进一步地,我们通过 kmalloc 探究一下 slab 的分配过程。其调用链为
kmalloc === __do_cache_alloc === ____cache_alloc
____cache_alloc 函数中,首先通过 cpu_cache_get 获取到对应 kmem_cache 在当前 CPU 下的 array_cache。并判断是否为空,不为空直接获取 objp。否则要执行 cache_alloc_refill 从具体 slab 结构中获取。
static inline void *____cache_alloc(struct kmem_cache *cachep, gfp_t flags)
{
void *objp;
struct array_cache *ac;
check_irq_off();
ac = cpu_cache_get(cachep);
if (likely(ac->avail)) {
STATS_INC_ALLOCHIT(cachep);
ac->touched = 1;
objp = ac->entry[--ac->avail];
} else {
STATS_INC_ALLOCMISS(cachep);
objp = cache_alloc_refill(cachep, flags);
/*
* the 'ac' may be updated by cache_alloc_refill(),
* and kmemleak_erase() requires its correct value.
*/
ac = cpu_cache_get(cachep);
}
if (objp)
kmemleak_erase(&ac->entry[ac->avail]);
return objp;
}
简单地看下 cache_alloc_refill。大概就是先找到需要被分配的 slab,并将其上对象地址获取,放入缓存 array_cache 中,并返回
static void *cache_alloc_refill(struct kmem_cache *cachep, gfp_t flags)
{
int batchcount;
struct kmem_list3 *l3;
struct array_cache *ac;
int node;
retry:
check_irq_off();
node = numa_node_id();
ac = cpu_cache_get(cachep);
batchcount = ac->batchcount;
if (!ac->touched && batchcount > BATCHREFILL_LIMIT) {
/*
* If there was little recent activity on this cache, then
* perform only a partial refill. Otherwise we could generate
* refill bouncing.
*/
batchcount = BATCHREFILL_LIMIT;
}
l3 = cachep->nodelists[node];
BUG_ON(ac->avail > 0 || !l3);
spin_lock(&l3->list_lock);
/* See if we can refill from the shared array */
if (l3->shared && transfer_objects(ac, l3->shared, batchcount)) {
l3->shared->touched = 1;
goto alloc_done;
}
while (batchcount > 0) {
struct list_head *entry;
struct slab *slabp;
/* Get slab alloc is to come from. */
entry = l3->slabs_partial.next;
if (entry == &l3->slabs_partial) {
l3->free_touched = 1;
entry = l3->slabs_free.next;
if (entry == &l3->slabs_free)
goto must_grow;
}
slabp = list_entry(entry, struct slab, list);
check_slabp(cachep, slabp);
check_spinlock_acquired(cachep);
/*
* The slab was either on partial or free list so
* there must be at least one object available for
* allocation.
*/
BUG_ON(slabp->inuse >= cachep->num);
while (slabp->inuse < cachep->num && batchcount--) {
STATS_INC_ALLOCED(cachep);
STATS_INC_ACTIVE(cachep);
STATS_SET_HIGH(cachep);
ac->entry[ac->avail++] = slab_get_obj(cachep, slabp,
node);
}
check_slabp(cachep, slabp);
/* move slabp to correct slabp list: */
list_del(&slabp->list);
if (slabp->free == BUFCTL_END)
list_add(&slabp->list, &l3->slabs_full);
else
list_add(&slabp->list, &l3->slabs_partial);
}
must_grow:
l3->free_objects -= ac->avail;
alloc_done:
spin_unlock(&l3->list_lock);
if (unlikely(!ac->avail)) {
int x;
x = cache_grow(cachep, flags | GFP_THISNODE, node, NULL);
/* cache_grow can reenable interrupts, then ac could change. */
ac = cpu_cache_get(cachep);
if (!x && ac->avail == 0) /* no objects in sight? abort */
return NULL;
if (!ac->avail) /* objects refilled by interrupt? */
goto retry;
}
ac->touched = 1;
return ac->entry[--ac->avail];
}
那么释放的话,自然是将slab对象指针放回 array_cache 中。调用链为 kfree ⇒ __cache_free。可以看到,主要就是操作 ac,让其放入 objp
static inline void __cache_free(struct kmem_cache *cachep, void *objp)
{
//省略部分
if (likely(ac->avail < ac->limit)) {
STATS_INC_FREEHIT(cachep);
ac->entry[ac->avail++] = objp;
return;
} else {
STATS_INC_FREEMISS(cachep);
cache_flusharray(cachep, ac);
ac->entry[ac->avail++] = objp;
}
}
更进一步地,像伙伴系统实现原理,slab的初始化等就不进行展开。至此,关于内存管理梳理完成。