lab 2: Memory Manage(working)
新的环境好像不支持arm架构了,总是会在make build
触发错误
exec chbuild not found
. 我们于是只能使用utm平台+qemu模拟amd64架构的ubuntu系统来进行运行。
首先我们还是先进行make build
来获得我们想要的环境。
1 Buddy System
练习题1: 完成
kernel/mm/buddy.c
中的split_chunk
、merge_chunk
、buddy_get_pages
、 和buddy_free_pages
函数中的 LAB 2 TODO 1 部分,其中 buddy_get_pages 用于分配指定阶大小的连续物理页,buddy_free_pages 用于释放已分配的连续物理页。
我们首先先来了解一下我们需要完成的任务。
- 从内存中获取自由链表的情况。
- 在分配相对应的链表时,根据阶次进行相对应地拆分,同时修改自由链表的填写情况。
- 释放已经分配的页,并将他们合成。
我们首先阅读一下kernel/mm/buddy.c
的代码。
第一部分:初始化init_buddy()
在这一部分,我们首先初始化了物理内存池的相关数据。例如起始地址,起始页表,起始内存大小等。这里我们的每块伙伴页表的大小等常量的定义位于kernel/include/mm/buddy.h
中。
这里页表的最大阶次规定为14,并且其伙伴页表的最大大小为0x1000, 因此我们的伙伴页表阶次为12.
接下来需要了解我们所需要的各个结构体组成。我们在kernel/include/mm/buddy.h
和kernel/include/common/list.h
里面可以找到相关的叙述。
page结构体
- 节点(list_head)
- 分配状态(int)
- 阶次(int)
- ...
free_list结构体
- free_list(list_head)
- 自由页表的个数(nr_free)
物理线程池
- 起始地址(虚拟地址)
- 地址大小
- 页表相关数据
- 内存锁
- 自由链表(不同阶次的)
- 物理页表数目
list_head结构体
- 前向指针
- 后向指针
- 初始化双向链表
- 增加,添加,删除,清零,寻找下一个容器,列表下标,简单迭代器...
其中list_head里有一些宏定义十分有意思。我们之后会来看一看。
1.1 获得未分配的页表
首先我们先要分配伙伴链表。这时我们需要
- 寻找空闲链表。
- 从空闲链表去除伙伴块。
- 检查伙伴块阶次并根据情况进行分裂。
因此,首先我们先完成buddy_get_pages()
部分。在这一部分中,我们要为伙伴系统分配空闲的物理页,并将其放入到自由链表中。
struct page *buddy_get_pages(struct phys_mem_pool *pool, int order)
{int cur_order;struct list_head *free_list;struct page *page = NULL;if (unlikely(order >= BUDDY_MAX_ORDER)) {kwarn("ChCore does not support allocating such too large ""contious physical memory\n");return NULL;}lock(&pool->buddy_lock);/* LAB 2 TODO 1 BEGIN *//** Hint: Find a chunk that satisfies the order requirement* in the free lists, then split it if necessary.*//* BLANK BEGIN */// UNUSED(cur_order);// UNUSED(free_list);for (cur_order = order; cur_order < BUDDY_MAX_ORDER; cur_order++){// 检查是否具有空闲链表。if(pool -> free_lists[cur_order].nr_free != 0) {// 获取对应阶次的空闲链表头。free_list = &(pool->free_lists[cur_order].free_list);// 获取这个链表节点所对应的页表头。检查这一条链表是否为空。if (!list_empty(free_list)){/*** 强行从free_list->next作为其成员,计算出page结构的首地址* 然后分配一段新的内存空间给这个页表。* 因此我们需要将list_head->next,也就是第一个页表节点传入,计算出其在page中的偏移量* 然后让编译器进行类型转换,将包含有链表指向的地址的一段地址解释成为新的结构体。*/ page = list_entry(free_list->next, struct page, node);// 分配空间之后其余的值均为随机值需要进行更改。page -> allocated = 0;page -> order = cur_order;page -> pool = pool;break;}}}if (page == NULL)goto out;// 分配了页表,删除掉自由链表内的该值。减少改阶次下的链表数。list_del(&page -> node);pool -> free_lists[cur_order].nr_free -= 1;// 必要的情况下分裂链表。page = split_chunk(pool, order, page);page -> allocated = 1;/* BLANK END *//* LAB 2 TODO 1 END */
out: __maybe_unusedunlock(&pool->buddy_lock);return page;
}
1.2 分裂部分
接下来我们要完成分裂链表部分,采用递归进行。注意每次我们都需要减少order后获得新的伙伴块,直到获得的伙伴块和我们需要的order相同。
我们需要自行不断减少页表的order数,通过get_buddy_chunk()
的方式,从当前的页表中获得其子页(伙伴页),直到符合条件为止。
采用递归的方式进行分裂,我们可以得到:
__maybe_unused static struct page *split_chunk(struct phys_mem_pool *__maybe_unused pool,int __maybe_unused order,struct page *__maybe_unused chunk)
{/* LAB 2 TODO 1 BEGIN *//** Hint: Recursively put the buddy of current chunk into* a suitable free list.*//* BLANK BEGIN */int cur_order = chunk -> order;if (cur_order == order)return chunk;else{// 分裂链表。chunk->order -= 1;struct list_head *free_list = &(pool -> free_lists[chunk->order].free_list);struct page *buddy_chunk;buddy_chunk = get_buddy_chunk(pool, chunk);buddy_chunk -> allocated = 0;buddy_chunk -> order = chunk -> order;buddy_chunk -> pool = chunk -> pool;buddy_chunk -> slab = chunk -> slab;list_add(&(buddy_chunk -> node), free_list);pool -> free_lists[buddy_chunk->order].nr_free += 1;return split_chunk(pool, order, chunk);} /* BLANK END *//* LAB 2 TODO 1 END */
}
接下来完成释放页表部分。只需要将页表标记成空闲后,尝试合并页表块,将合并的页表块加入到对应order的自由链表之中。
void buddy_free_pages(struct phys_mem_pool *pool, struct page *page)
{int order;struct list_head *free_list;lock(&pool->buddy_lock);/* LAB 2 TODO 1 BEGIN *//** Hint: Merge the chunk with its buddy and put it into* a suitable free list.*//* BLANK BEGIN */// UNUSED(free_list);// UNUSED(order);page->allocated = 0;page = merge_chunk(pool, page);list_add(&page->node, &pool->free_lists[page->order].free_list);pool->free_lists[page->order].nr_free += 1;/* BLANK END *//* LAB 2 TODO 1 END */unlock(&pool->buddy_lock);
}
递归式合并伙伴。只需要每次获取chunk直到无法获取更高阶次的为止。注意检查阶次是否到达合并顶点,是否被分配或伙伴页表为空的情况。
/* The most recursion level of merge_chunk is decided by the macro of* BUDDY_MAX_ORDER. */
__maybe_unused static struct page * merge_chunk(struct phys_mem_pool *__maybe_unused pool,struct page *__maybe_unused chunk)
{/* LAB 2 TODO 1 BEGIN *//** Hint: Recursively merge current chunk with its buddy* if possible.*//* BLANK BEGIN */if (chunk->order == BUDDY_MAX_ORDER - 1) return chunk;struct page *buddy = get_buddy_chunk(pool, chunk);if (buddy == NULL || buddy->allocated == 1 || buddy->order != chunk->order) return chunk;else {list_del(&buddy->node);--pool->free_lists[buddy->order].nr_free;if (chunk > buddy) chunk = buddy;chunk->order += 1;return merge_chunk(pool, chunk);}/* BLANK END *//* LAB 2 TODO 1 END */
}
伙伴系统完成。
其中一个很重要的宏定义函数完成了通过结构体成员构造新结构体的方式。即我们有:
list_entry(ptr, type, field);
// 第一个参数是我们需要的结构体成员,第二个是我们想要转换成的类型,
// 第三个是在想要转换成为的类型中的成员。
// 本质上计算了成员与结构体开始地址的差值,
// 并告诉编译器当前所需要的成员所在的结构体的首地址。
// 宏定义如下:
#define list_entry(ptr, type, field) \container_of(ptr, type, field)#define container_of(ptr, type, field) \((type *)((void *)(ptr) - (void *)(&(((type *)(0))->field))))
2. SLAB分配器
完成
kernel/mm/slab.c
中的choose_new_current_slab
、alloc_in_slab_impl
和free_in_slab
函数中的 LAB 2 TODO 2 部分,其中alloc_in_slab_impl
用于在 slab 分配器中分配指定阶大小的内存,而free_in_slab
则用于释放上述已分配的内存。
在SLAB分配时,我们需要先观察其具有的结构体。
我们可以发现,SLAB指针(slab_pointer,也就是我们第一个要完成函数的传入指针类型)包含着一个指针和一个list_head成员。结合课上知识,第一个为当前slab分配器的头指针current,第二个则是所谓的partial指针,其中含有所有拥有空闲块。因此,我们需要获得partial指针的next,也就是第一个空闲的SLAB。这样我们就有:
static void choose_new_current_slab(struct slab_pointer * __maybe_unused pool)
{/* LAB 2 TODO 2 BEGIN *//* Hint: Choose a partial slab to be a new current slab. *//* BLANK BEGIN */// 检查是否为空。if(list_empty(&pool -> partial_slab_list))pool -> current_slab = NULL;else{// 选择一个没有分配的物理页块,返回并从partial中删除。pool -> current_slab = list_entry(pool -> partial_slab_list.next, struct slab_header, node);list_del(pool -> partial_slab_list.next);}/* BLANK END *//* LAB 2 TODO 2 END */
}
接下来需要分配指定阶次大小的内存。首先,我们可以看到在函数中,首次分配内存时我们需要初始化我们的每个slab,形成固定大小的slab块并且组织好slab的free_list. free_list是一个链表结构,每个指针存储着指向下一个没有被分配的物理内存页。将slab的header槽中的free_list_head转换成slab_slot_list类型的一个指针后,其成员next_free就是下一个未被分配的物理页。我们具有以下的图示结构:(有点难理解)
接下来我们阅读我们的函数。首先看函数给我们前面的部分:
static void *alloc_in_slab_impl(int order)
{struct slab_header *current_slab;struct slab_slot_list *free_list;void *next_slot;UNUSED(next_slot);// ...
}
这一部分给出了我们需要的几个变量。首先,current_slab自然无需多言是我们需要分配的slab。其次,free_list是一个slab_slot_list类型,有:
/* Each free slot in one slab is regarded as slab_slot_list. */
struct slab_slot_list {void *next_free;
};
结合slab_header的free_list_head,我们可以发现,free_list_head可以转换成slab_slot_list类型。而这个free_list_head有next_free指针,指向的正好是下一个自由的slot。要注意的是,每一个slab的第一个slot在本系统中不存储实际的物理页,相当于是头指针,因此计算当前free_slot的数目时需要减去1。
这样我们有:
/* LAB 2 TODO 2 BEGIN *//** Hint: Find a free slot from the free list of current slab.* If current slab is full, choose a new slab as the current one.*//* BLANK BEGIN */// 首先,我们先寻找当前slab的free_list. 也就是slab_header中的free_list_head.// 注意类型的转换。从注释中可以得到答案。free_list = (struct slab_slot_list*) current_slab -> free_list_head;next_slot = free_list -> next_free;current_slab -> free_list_head = next_slot;current_slab -> current_free_cnt--;// 检查是否slab已满。注意slab_header的第一个slot用于储存头信息!因此需要-1if (current_slab -> current_free_cnt == 0) choose_new_current_slab(&slab_pool[order]); // 分配新的slab/* BLANK END *//* LAB 2 TODO 2 END */
接下来就是free_in_slab. 我们要进行的是释放这个slot,并将其放回free_list.
struct page *page;struct slab_header *slab;struct slab_slot_list *slot;int order;slot = (struct slab_slot_list *)addr;page = virt_to_page(addr);if (!page) {kdebug("invalid page in %s", __func__);return;}slab = page->slab;order = slab->order;lock(&slabs_locks[order]);try_insert_full_slab_to_partial(slab);
首先从物理地址转换成slot,这样我们就可以操作其next_free指针指向下一个离他最近的free_slot.随后,判断是否是一个全满的slab释放出空slot,将其插入partial_list(用于指明当前内存池中的具有空闲槽的slab)中。
接下来,理论上我们应该去寻找离我们最近的free_slot,让我们的next_free指向它。但是我们并没有一个插入链表的接口。因此我们可以用另一种方法,让这个新的slot成为我们的slab新的free_list_head,让原先的free_list_head指向我们的slot。如下图所示:
这样我们就有:
/* LAB 2 TODO 2 BEGIN *//** Hint: Free an allocated slot and put it back to the free list.*//* BLANK BEGIN */slot -> next_free = slab -> free_list_head;slab -> free_list_head = (void*) slot;slab -> current_free_cnt += 1;UNUSED(slot);/* BLANK END *//* LAB 2 TODO 2 END */
这样我们完成了简单的slab。
3. kmalloc
完成
kernel/mm/kmalloc.c
中的_kmalloc
函数中的 LAB 2 TODO 3 部分,在适当位置调用对应的函数,实现 kmalloc 功能
根据提示,首先我们直接调用alloc_in_slab()
功能,分配slab给我们的addr。当此时的size与当前slab的size相比过大时,我们则需要重新从伙伴系统中获得新的物理页,再转换成slab分配器,获得我们的地址。
if (size <= SLAB_MAX_SIZE) {/* LAB 2 TODO 3 BEGIN *//* Step 1: Allocate in slab for small requests. *//* BLANK BEGIN */UNUSED(addr);UNUSED(order);addr = alloc_in_slab(size, real_size);/* BLANK END */
#if ENABLE_MEMORY_USAGE_COLLECTING == ONif(is_record && collecting_switch) {record_mem_usage(*real_size, addr);}
#endif} else {/* Step 2: Allocate in buddy for large requests. *//* BLANK BEGIN */order = size_to_page_order(size);addr = _get_pages(order, is_record);/* BLANK END *//* LAB 2 TODO 3 END */}
这样我们完成了实验二第一部分,应该获得30分。
待续...