NJU OSLAB 速通记录
Day -9
假期搞了一堆光说不练的事情,结果眼看着 OSlab 的 HardDDL 就在眼前了,胡适之啊胡适之,你不能再坐视不理了!
于是我就这样决定开始速通我的操作系统实验(含三个大实验和三个小实验)。
不妨从第一个实验 kalloc
开始。实验要求实现一个内存分配器,允许多处理器并发申请/释放内存。分配的内存一定按照(能包含它的最小的)二的次幂对齐。
我们先考虑单线程/上把大锁的情况。
脑子里一下出现一个区间二叉树的结构,仔细想想太复杂了,原因如下。
- 逻辑太复杂了,不利于维护。
- 没法做到 in-place,即元数据结构没法随所在空间节点的分配而删除。
然后发现不必维护整棵树,只需维护所有极大未分配子树即可。发现它们一定是无交的,于是可以做到 in-place。
那么怎么常数时间拉出一个指定大小的未分配子树呢?
考虑把所有极大未分配子树按照大小分组,每个插个链表就完事了,每次先看对应大小的有没有,如果没有就递归分配一个更大一级的,把更大一级的拆成两半。
看上去非常之完美!
大约只需要定义如此数据结构:
// 最大允许分配内存 16 MiB
#define MAXSCALE 24
// In place 的数据结构
typedef struct span_t
{
uint8_t scale; // 规模
span_t *next_span; // 下一个区间
} span_t;
// 链表
span_t *span_heap[MAXSCALE + 1];
对于小到连这个数据结构都装不下的空间,我们的选择是……摆烂。我们只把空间划分到 16B 即可,小内存直接“当作”大内存来分配。
In-place 意味着分配内存后我们将丢失所有的元数据,那么还原时就会遇到很大的问题:free 只提供一个头指针,如何确定它是哪一块内存?
因此我们需要对每个分配出去的头指针记录下分配的单元的规模,以便还原。
这是个困难的事情,因为 lab 只给了 1M 的静态数据区,没法存下这么大的数组。我们只能使用动态的 Trie/Radix 树来存下这些值。
这时候又麻烦了,Trie/Radix 怎么分配新内存……?必须创建一个约 32 KB 大小的缓冲区,每次要用了递归调用 kalloc
就新分配一段空间。
在归还时,必须检查兄弟子树是否未被分配,如果未被分配的话需要归并。
现在考虑多处理器,且不能用一把大锁锁上整个链表,同时还得支持某个处理器分配的内存在另外一个处理器释放,听着就麻烦。
讲义说大于一页的内存分配是较为罕见的,所以我们令大于一页的内存全部按老办法分配。
小内存和整页内存频繁分配,我们可以考虑一次取走一个较大的 Span 给本处理器,用完了再申请;如果归还导致本处理器的空闲内存块较多较大,那么就归还。
这样就引入了一个单处理器的内存池,我们仍然可以仿照大处理器的结构同构地构造单处理器的池。
太困了,明天再说吧。
另外值得一提的是按照讲义做了一个简易的测试框架,其实就是把 pmm.c
和一些其他源文件链接起来了,并重定向了它的头文件,使得这个模块可以被直接在 linux 的运行时下调用测试。
Day -8
一个处理器可能会释放其他处理器分配的内存,意味着可能具有跨池操作,更糟的是会有两个处理器同时读写同一个池的数据,当然这可以通过对每个池设置一个锁来解决。
然而更恶心的是,处理器还会同时读写全局 Radix 树,因为它们可以 free 全局任何位置的内存。这直接令对 Span 的分配和释放操作代价变得极大。
Free 的时候读取是不免的了(要找到是哪个 Span),我们考虑尽量减少对 Radix 树的写入。
但是即使如此,Radix 树还是需要加锁,这大概是因为 Radix 树的结构过于复杂,我们还是希望采用线性的数据结构。
可能的 Span 头指针数目太多了,有没有一种可能只分配大的 Span,而让小的 Span 的地址自动向大的 Span 对齐呢?
这显然是可以的,于是我们把 Span 的最小大小定在 128 KiB,所有小于 128 KiB 的内存分配都一次拿一整个 Span 进行分配,这样整个内存中至多存在大约 32Ki 个 Span,可以直接开一个数组记录头数据,包括其规模大小以及其所属 CPU。
用这个方法可以规避两个实现问题:
- 单 CPU 的缓存获取和回收问题。由于 Span 够大,单个 CPU 内只需存放最小大小的 Span 即可,这样可以把单 CPU 的缓存设计的极简单,回收只需要设置单一数值门槛即可。
- 可以免设全局数组的锁。由于保证测试用例不存在数据竞争(同时 Free/Malloc 地址)那么全局数组的某一个单元在任意时刻也不存在数据竞争(如果是整块分配显然不存在竞争,如果是零散分配,由于一个 Span 只可能属于单个 CPU,写全局数组的情况仅限于整体分配和整体归还,这两种情况下其在全局数组的所属单元肯定不可能有人在读,所以仍不存在竞争)。