xv6 6.S081 Lab3: alloc
xv6 6.S081 Lab3: alloc
- 写在前面
- 实验介绍
- 开始!
- 任务再描述
- 任务一实现
- 任务二实现
- Buddy Allocator
- Code Thru
- 任务二的实现
alloc代码在这里。另外,本文主要是将我的实验报告搬了下来,因此内容难免偏多,可以一边结合代码、一边结合实验指导书食用。
写在前面
Buddy Allocator是Linux中著名的内存分配器,详情可以参考这里的实验指导书(PS:写得真的非常棒)
实验介绍
本次实验由两个任务构成:
- 利用bd_malloc 实现文件动态分配
- 优化Buddy Allocator
开始!
任务再描述
本次实验任务主要有两个,它们分别是:动态分配文件(后文称为任务一)、改进Buddy Allocator的空间效率(后文称为任务二)。接下来,我们讨论一下为什么要有这两个任务。
首先介绍任务一的由来。在xv6的file.c中定义了一个ftable,如下所示。我们可以很清晰的发现,ftable是通过声明静态file数组来实现文件的分配,这样一来,就导致一个进程只能打开固定数量NFILE的文件,通过阅读源码可知NFILE为100。本任务要求利用Buddy Allocator动态分配文件,这样一来可打开的文件数就能够大于NFILE了。
struct {struct spinlock lock;struct file file[NFILE];
} ftable;
其次介绍任务二。xv6的Buddy Allocator在buddy.c中实现。通过阅读源码,可以发现Buddy Allocator中的每种规格大小的内存块都保留一个比特位,用于标识该块是被占有还是空闲,见下面的代码。这样一来Buddy Allocator的就会使用大量的内存用于保存这些比特位,这导致了Buddy Allocator的空间利用效率变得低下。本任务要求利用如下策略对现有的Buddy Allocator进行优化:只用一个比特位标识一对buddy内存块(两块)的使用情况。例如,对于buddy块B1和B2,这个比特记录了“B1 is free XOR B2 is free”(即,B1空闲异或B2空闲,进一步的,B1、B2其中一个空闲,则该比特为1;B1和B2都空闲或者都被占用,则该比特为0)。这样一来,一旦buddy块中的一个块被分配或者释放,都需要调整这个比特的值。
//每种规格大小的内存块都保留一个比特位for (int k = 0; k < nsizes; k++) {lst_init(&bd_sizes[k].free);sz = sizeof(char)* ROUNDUP(NBLK(k), 8)/8;bd_sizes[k].alloc = p;memset(bd_sizes[k].alloc, 0, sz);p += sz;}
为了便于理解,我们给出下表以说明这个过程:
我们仅分析“B1空闲、B2被占用”的状态,其他场景类似。在该状态下,此比特值为1,这时如果我们释放B2,那么我们就知道B1和B2都处于空闲状态,可以进行合并。按照xv6官方指导书的说法,运用这个优化策略,每一对buddy块就能节省1比特,当xv6利用优化后的Buddy Allocator管理大约128MB的空闲内存时,该方案就可以节省大概1MB的内存。具体的原理我们将在3.2节中进行说明。
任务一实现
任务一的实现较为简单,按照实验指导书的Hints一步一步完成即可实现。相关Hints如下:
- You’ll want to remove line 19 in kernel/file.c, which declares file[NFILE]. Instead, allocate struct file in filealloc using bd_malloc. In fileclose you will free the allocated memory.
- fileclose still needs to acquire ftable.lock because the lock protects f->ref.
于是将file.c中的第19行注释掉:
struct {struct spinlock lock;//struct file file[NFILE];
} ftable;
并将file.c中的filealloc函数改为利用bd_malloc:
struct file*
filealloc(void)
{struct file *f;acquire(&ftable.lock);f = bd_malloc(sizeof(*f));if(f->ref == 0){f->ref = 1;release(&ftable.lock);return f;} release(&ftable.lock);return 0;
}
相应的,在fileclose中利用bd_free释放文件:
void
fileclose(struct file *f)
{struct file ff; acquire(&ftable.lock);if(f->ref < 1)panic("fileclose");if(--f->ref > 0){release(&ftable.lock);return;}ff = *f; f->ref = 0;f->type = FD_NONE;bd_free(f);release(&ftable.lock);if(ff.type == FD_PIPE){pipeclose(ff.pipe, ff.writable);} else if(ff.type == FD_INODE || ff.type == FD_DEVICE){begin_op(ff.ip->dev);iput(ff.ip);end_op(ff.ip->dev);} }
运行结果如下图所示:
任务二实现
对于任务二,xv6指导书基本没有给Hint。相比之下,我在文章开头给出的指导书就详尽许多。在本小节中,我打算先结合Buddy Allocator指导书对buddy.c进行code through,以便理解xv6对Buddy Allocator的实现,从而对其进行优化。接下来的部分我将分为三个部分进行介绍。第一部分介绍Buddy Allocator的基本原理;第二部分介绍xv6中对Buddy Allocator的实现;第三部分介绍任务二的实现。
Buddy Allocator
Buddy Allocator实际上是一种二分分配法,给出如下示意图。我们想要在内存大小为512KB的系统内分配一块大小为65KB的内存。具体细节大家可以参考指导书,绝对够详细。
Buddy Allocator采用链表结构实现这一过程:
下图展示了状态A时的链表结构
下图展示了状态D时的链表结构
Code Thru
这一部分可以看,如果完全懂了Buddy.c的实现原理就可以跳过这部分了。
首先,xv6定义LEAF_SIZE
为16,这表明Buddy Allocator中,最小内存块的大小为16。其次,xv6的buddy.c中定义了一个sz_info
结构体,见下面的代码,它包含3个字段:free
,alloc
以及split
,其中free
为我们所说的“空闲列表”,alloc
为“内存块是否已分配状态”数组,split
为“内存块是否被分割状态”数组。显然,bd_sizes
是记录了所有分级状态的数组,通过bd_sizes
我们可以访问所有的分级状态。
struct sz_info {Bd_list free;char *alloc;char *split;
};
typedef struct sz_info Sz_info;
static Sz_info *bd_sizes;
buddy.c从bd_init(void *base, void *end)
函数开始,该函数的任务是将从base
到end
的内存交给Buddy Allocator管理。其中,bd_init
首先计算了分级状态的总数量,保存在nsizes
内,代码如下:
char *p = (char *) ROUNDUP((uint64)base, LEAF_SIZE);int sz;initlock(&lock, "buddy");bd_base = (void *) p;// compute the number of sizes we need to manage [base, end)nsizes = log2(((char *)end-p)/LEAF_SIZE) + 1;if((char*)end-p > BLK_SIZE(MAXSIZE)) {nsizes++; // round up to the next power of 2}
接下来,bd_init
初始化了所有分级状态,即bd_sizes
, 代码如下。值得注意的是,记录bd_sizes
的内存也是被分配到整个Buddy Allocator应该管理的内存(base
到end
)中的,因此,在Buddy Allocator的初始化完成后,一部分的内存已经被bd_sizes
给占用了,这一段内存被xv6称为meta
bd_sizes = (Sz_info *) p;p += sizeof(Sz_info) * nsizes;memset(bd_sizes, 0, sizeof(Sz_info) * nsizes);// initialize free list and allocate the alloc array for each size kfor (int k = 0; k < nsizes; k++) {lst_init(&bd_sizes[k].free);sz = sizeof(char)* ROUNDUP(NBLK(k), 8)/8;bd_sizes[k].alloc = p;printf("sz:%d\n", sz);memset(bd_sizes[k].alloc, 0, sz);p += sz;}// allocate the split array for each size k, except for k = 0, since// we will not split blocks of size k = 0, the smallest size.
// size 0 不用继续分配split了,因为不可再分了for (int k = 1; k < nsizes; k++) {sz = sizeof(char)* (ROUNDUP(NBLK(k), 8))/8;bd_sizes[k].split = p;memset(bd_sizes[k].split, 0, sz);p += sz;}p = (char *) ROUNDUP((uint64) p, LEAF_SIZE);
在接下来的代码中,meta
部分被bd_mark_data_structures()标记为了已被分配,另外,Buddy Allocator还通过bd_mark_unavailable
标记了一段无效区,这一段区域也会被算入Buddy Allocator已分配的内存里面去,最后bd_init()
通过调用bd_initfree(p,end)
初始化所有分级状态的空闲列表。这个过程如何进行的呢?从我工实验指导书上偷一张图,以表明执行完bd_mark_data_structures()
与bd_mark_unavailable()
后bd_sizes的alloc
和split
的分布情况:
显然,图中黄色背景标记的内存块都应该接入相应的freelist中。从图中,我们可以发现几个有趣的现象:
- 现象一:应该加入到空闲列表中的内存块只出现在每一个size(各分级状态)的两端;
- 现象二:某个内存块应该被加入空闲列表中,当且仅当它未被分配且他的兄弟块(Buddy)已经被分配了;
- 现象三:应该加入到空闲列表中的内存块与size 0(对应16B的分级状态)中已分配的内存块大小之和为整个内存空间的大小;
bd_initfree()
做的工作便是完成freelist的初始化,代码如下。
int
bd_initfree(void *bd_left, void *bd_right) {int free = 0;for (int k = 0; k < MAXSIZE; k++) { // skip max sizeint left = blk_index_next(k, bd_left);int right = blk_index(k, bd_right);free += bd_initfree_pair(k, left);if(right <= left)continue;free += bd_initfree_pair(k, right);}return free;
}
在bd_initfree()
代码中,我们可以看到,bd_initfree从size 0开始,不断向上考察有潜力加入free
的内存块。显然,这个过程与现象一一致,考察只发生在left和right,也就是说,我们只需要考察是否加入左右两端的内存块即可。函数bd_initfree_pair()
是考察函数,其实现代码如下。可以看见,其考察准则和现象二一致,当且仅当当前内存块未被分配且他的兄弟块(Buddy)已经被分配了,我们才会在size k
对应的空闲列表中加入该内存块,当然这里当前内存块与Buddy地位相同,它们互为Buddy,因此在bd_initfree_pair()
中才会有int buddy = (bi % 2 == 0) ? bi+1 : bi-1
。
int
bd_initfree_pair(int k, int bi) {int buddy = (bi % 2 == 0) ? bi+1 : bi-1;int free = 0;if(bit_isset(bd_sizes[k].alloc, bi) != bit_isset(bd_sizes[k].alloc, buddy)) {// one of the pair is freefree = BLK_SIZE(k);if(bit_isset(bd_sizes[k].alloc, bi))lst_push(&bd_sizes[k].free, addr(k, buddy)); // put buddy on free listelselst_push(&bd_sizes[k].free, addr(k, bi)); // put bi on free list}return free;
}
在bd_init()
的最后,xv6贴心地附上了一段检查代码,如下。显然,这一段代码做的事情就是验证现象三。
if(free != BLK_SIZE(MAXSIZE)-meta-unavailable) {printf("free %d %d\n", free, BLK_SIZE(MAXSIZE)-meta-unavailable);panic("bd_init: free mem");}
至此,我们调研了bd_init()
函数、bd_initfree()
函数以及bd_initfree_pair()
函数,接下来,我们将继续调研bd_malloc(uint64 nbytes)
与bd_free(void *p)
函数,以完成Code Thru。
首先介绍bd_malloc(uint64 nbytes)
,其功能为动态分配大小为nbytes
的内存。在这个过程中,Buddy Allocator首先找到刚好大于nbytes
的size fk
,代码如下:
// Find a free block >= nbytes, starting with smallest k possiblefk = firstk(nbytes);for (k = fk; k < nsizes; k++) {if(!lst_empty(&bd_sizes[k].free))break;}if(k >= nsizes) { // No free blocks?release(&lock);return 0;}
接着,它需要查找对应size fk
下是否有空闲块,如果没有,就向上搜索,直到找到第一个具有空闲块的size k
为止。接着,Buddy Allocator将从size k
开始,向下修改bd_sizes
直到size fk
为止,这一过程代码如下:
// Found a block; pop it and potentially split it.char *p = lst_pop(&bd_sizes[k].free);bit_set(bd_sizes[k].alloc, blk_index(k, p));for(; k > fk; k--) {// split a block at size k and mark one half allocated at size k-1// and put the buddy on the free list at size k-1char *q = p + BLK_SIZE(k-1); // p's buddybit_set(bd_sizes[k].split, blk_index(k, p));bit_set(bd_sizes[k-1].alloc, blk_index(k-1, p));lst_push(&bd_sizes[k-1].free, q);}
其中,char *p = lst_pop(&bd_sizes[k].free)
表示我们找到了一个空闲内存块p,接下来要切割它,因此将它从size k
对应的空闲列表中删除;接着通过bit_set(bd_sizes[k].alloc, blk_index(k, p))
将内存块p
在size k
中所对应的“是否被分配状态”标记为1,表明已被分配。char *q = p + BLK_SIZE(k-1)
找到了p
在size k-1
中的Buddy块q
;接下来通过bit_set(bd_sizes[k].split, blk_index(k, p))
,bit_set(bd_sizes[k-1].alloc, blk_index(k-1, p))
与lst_push(&bd_sizes[k-1].free, q)
分别将p
在size k
中的“是否被分割的状态”标记为1,将p
在size k-1
中的“是否被分配的状态”标记为1,并将Buddy块q
移入size k-1
的空闲列表内。重复这个过程,直到k
刚好大于fk
为止。结合实验指导书的描述,我们可以更容易地理解这个过程。
接下来,我们要讨论bd_free(void *p)
。这个函数的功能是释放起始地址为p
的内存块。在下面的代码中,我们可以看到bd_free
是如何考察Buddy的:
int bi = blk_index(k, p);int buddy = (bi % 2 == 0) ? bi+1 : bi-1;bit_clear(bd_sizes[k].alloc, bi); // free p at size kif (bit_isset(bd_sizes[k].alloc, buddy)) { // is buddy allocated?break; // break out of loop}
下面的代码描述了bd_free
是如何合并空闲块的:
q = addr(k, buddy);lst_remove(q); // remove buddy from free listif(buddy % 2 == 0) {p = q;}// at size k+1, mark that the merged buddy pair isn't split// anymorebit_clear(bd_sizes[k+1].split, blk_index(k+1, p));
这里还涉及到一点细节问题:
- 在合并中,通过
if(buddy % 2 == 0)
语句保证地址p
始终指向第一个Buddy块 - 通过
bit_clear(bd_sizes[k+1].split, blk_index(k+1, p))
置在size k+1
中内存块p对应的“是否被分割”状态为0,表明下层块已被合并,上层块不再被分割
至此,我们完成了所有必要的Code Thru
任务二的实现
在实验任务再描述一节中,我们提到了一种优化策略,并且以表格的方式将此策略罗列了出来。其核心思想在于:利用一个比特位表示一对buddy块的alloc
。那么,这个策略为什么可行呢?是否有考虑过这样一个问题:如果采用这种优化策略,那么一对buddy块全部空闲或全部被占用时比特位的值都应该是0,此时我们又该如何判别呢?
答案是我们不需要判别。事实上,我们需要考虑清楚这样一个事情:在xv6未经优化的Buddy Allocator中,我们为什么要记录这个alloc
值,alloc
用在哪些地方?通过阅读源码,可以发现,用到alloc
的地方只有两处:
- 在
bd_initfree_pair
中,考察边界内存块是否应该加入free
; - 在
bd_free
中,考察buddy块是否为空闲状态;
在第一处中,当且仅当两个buddy块一个空闲一个被占用才能将其中一个加入free
,显然,这只需要异或一下即可;在第二处中,可以这么来理解:传入的内存块p一定是被占用的,我们将其释放掉之后,如果其buddy块被占用,那么XOR为1,否则XOR为0,同样可以用XOR进行判断。因此,上述优化策略是完全可行的。下面,我们给出实现方案。
首先,调整bd_init()
中分配的alloc
数组的大小。此时我们仅需要原来一半大小的数组,这里需要注意的是,为了保证分子是16的整数倍ROUNDUP(NBLK(k), 16)是必要的。
for (int k = 0; k < nsizes; k++) {lst_init(&bd_sizes[k].free);sz = sizeof(char)* ROUNDUP(NBLK(k), 16) / 8; //改成16,保证是偶数对sz /= 2;//printf("sz:%d, block:%d, after round: %d, char size:%d\n", sz, NBLK(k),ROUNDUP(NBLK(k), 8), sizeof(char));bd_sizes[k].alloc = p;memset(bd_sizes[k].alloc, 0, sz);p += sz;}
接着,编写mutual_bit_flip()
函数,以实现一对Buddy公用一个比特位的操作,同时,相应的,编写mutual_bit_get(
)函数,以获取Buddy的公用比特位。
/* 将公用buddy的bit用一个来表示 */
void mutual_bit_flip(char *array, int index) {index /= 2;if(bit_isset(array, index)){bit_clear(array, index);}else{bit_set(array, index);}
}int mutual_bit_get(char *array, int index){index /= 2;return bit_isset(array, index);
}
修改bd_mark()
,使用优化策略初始化meta
部分:
for(; bi < bj; bi++) {if(k > 0) {// if a block is allocated at size k, mark it as split too.bit_set(bd_sizes[k].split, bi);}/*** Change* bit_set(bd_sizes[k].alloc, bi); */ mutual_bit_flip(bd_sizes[k].alloc, bi);}
修改bd_initfree_pair
函数,使之通过mutual_bit_get()
来判断是否应该将某个内存块加入空闲列表。这里用到了一个技巧,通过判断if(bi == left)
即可决定究竟是将buddy块加入空闲列表还是将bi内存块加入空闲列表。为什么可以这样呢?回顾我们在Code Thru中观察到的现象一:应该加入到free
中的内存块只出现在每一个size
的两端。再者,我们观察传入的bd_initfree()
函数的参数:p
与bd_end
,它们分别表示meta
段末尾地址、无效内存的起始地址,接下来,我们继续借用我工的图,重绘以标记p
与bd_end
的位置,如下图所示(红色代表p,蓝色代表bd_end):
接着,我们观察传入bd_initfree_pair
的参数,通过阅读源码可知,为:left = blk_index_next(k, p)
与right = blk_index(k, bd_end)
。其中left
代表的是在相应size k
对应的内存块p
的后一块,right
代表的就是相应size k
的bd_end
对应的内存块。值得注意的是,在left
对应的块不是bd_end
对应的块的情况下(如size 3),其应该是空闲的,而right对应的块永远都是已被分配的。先来考察size 3的情况,由于size 3
中仅有的一对buddy都被分配了,因此它们谁也不应该加入到free
中;再来考察非size 3的情况,由于left
对应的块永远为空闲,因此其buddy一定被占用(因为mutual_bit_get(bd_sizes[k].alloc, bi)
),我们应该将left
块加入到free
中,而right
对应的块永远被分配,因此其buddy一定为空闲(同样因mutual_bit_get(bd_sizes[k].alloc, bi))
,我们应该将其buddy加入free
。修改后的bd_initfree_pair
如下:
if(mutual_bit_get(bd_sizes[k].alloc, bi)){free = BLK_SIZE(k);printf("size %d, bd_initfree_pair ", k); if(bi == left) {printf(" bi is free \n"); lst_push(&bd_sizes[k].free, addr(k, bi));}else{printf(" buddy is free \n"); lst_push(&bd_sizes[k].free, addr(k, buddy));}}
接下来,我们修改bd_malloc()
。修改方式较为简单,只需要将所有的bit_set
改为mutual_bit_flip
即可。代码如下:
char *p = lst_pop(&bd_sizes[k].free);/*** Change:* bit_set(bd_sizes[k].alloc, blk_index(k, p)); */mutual_bit_flip(bd_sizes[k].alloc, blk_index(k,p));for(; k > fk; k--) {// split a block at size k and mark one half allocated at size k-1// and put the buddy on the free list at size k-1char *q = p + BLK_SIZE(k-1); // p's buddybit_set(bd_sizes[k].split, blk_index(k, p));/*** Change* bit_set(bd_sizes[k-1].alloc, blk_index(k-1, p)); */mutual_bit_flip(bd_sizes[k-1].alloc, blk_index(k-1, p));lst_push(&bd_sizes[k-1].free, q);}
接着,我们对bd_free
如法炮制,注意,原理写在了本小节开头:
int bi = blk_index(k, p);int buddy = (bi % 2 == 0) ? bi+1 : bi-1;/*** Change* bit_clear(bd_sizes[k].alloc, bi); */ // free p at size kmutual_bit_flip(bd_sizes[k].alloc, bi); // free p/** * Change* bit_isset(bd_sizes[k].alloc, buddy) *//** * p已经被释放了,此时mutual_bit_get()* 如果是1,则说明buddy被占用了,否则空闲 * */if (mutual_bit_get(bd_sizes[k].alloc, bi)) { // is buddy allocated?break; // break out of loop}
OK,make grade
运行,测试通过,起飞✈
xv6 6.S081 Lab3: alloc相关推荐
- 6.S081 Lab3 page tables
6.S081 Lab3 page tables 未完成 文章目录 6.S081 Lab3 page tables 未完成 1. Print a page table ([easy](https://p ...
- xv6 6.S081 Lab1: util
xv6 6.S081 Lab1: util 写在前面 实验介绍 开始! sleep pingpong Primes Find Xargs 拖了这么久,终于稍微有时间填坑了.今天介绍xv6的第一个实验u ...
- xv6 6.S081 Lab5: cow
xv6 6.S081 Lab5: cow 写在前面 实验介绍 开始! cow代码在这里.完成了lazy后,cow的实现就非常明了了-- 写在前面 经典写在前面
- xv6 6.S081 Lab8: fs
xv6 6.S081 Lab8: fs 写在前面 实验介绍 开始! Large File Symbolic links fs代码在这里.我的妈呀,终于要写完了,xv6的file system考察难度并 ...
- xv6 6.S081 Lab4: lazy
xv6 6.S081 Lab4: lazy 写在前面 实验介绍 开始! 打印页表 实现Lazy Allocation 修改sbrk() 实现Lazy Allocation 完善Lazy Allocat ...
- xv6 6.S081 Lab7: Lock
xv6 6.S081 Lab7: Lock 写在前面 实验介绍 开始! Memory Allocator Buffer Cache lock代码在这里.本次实验理解起来简单,做起来也容易 写在前面 老 ...
- MIT6.S081 Lab3 Page tables
lab1.2不是太难,lab 3太变态了,github上记一下代码,源代码地址 :https://github.com/CodePpoi/mit-lab 参考博客 : https://blog.csd ...
- 6.s081 lab3
lab3 页表初始化过程: 物理页是一组由run结构体保存的,每个runmain函数调用kinit,初始化物理页.kinit调用了freerange.把内核对应的物理页全部释放掉,加入到freelis ...
- MIT6.S081 Lab3: page tables
Print a page table 接收一个pagetable_t并把它指向的页表打印. 在kernel/def.h中增加函数声明void vmprint(void)并在kernel/vm.c中定义 ...
最新文章
- redis的导入导出需要特别注意的地方
- 021Python路--单例设计模式
- 不用AJAX实现前台JS调用后台C#方法(小技巧)
- 是什么让美国网站拒绝欧洲访问?- GDPR 带来的数据安全思考
- python对象传递_Python参数传递对象的引用原理解析
- (最短路径算法整理)dijkstra、floyd、bellman-ford、spfa算法模板的整理与介绍
- Win10/Server2016镜像集成离线补丁
- java对象调用方法,java 对象调用
- Java毕设项目电商后台管理系统计算机(附源码+系统+数据库+LW)
- 【资源分享】Dll Injector(DLL注入器)
- 海思isp图像处理芯片_最新海思芯片3559A的功能简介
- 微火上线ai绘画小程序搭建系统,ai绘画小程序源码触手可及
- python中内置数学函数详解和实例应用之三角函数_初级阶段(二)
- java面试宝典(综合版)
- Python爬虫框架Scrapy入门(三)爬虫实战:爬取长沙链家二手房
- 483g路由器连接服务器无响应,TP-LINK企业路由器设置 TP-LINK TL-R483 Wan口设置图文教程...
- kubectl logs 常用命令
- Linux下的terminal多窗口开启及切换
- (三)UPF之Domain Coverage Relationship(Cover、Equivalent、Independent)
- 微服务项目实战技术点汇总:“尚硅谷的谷粒在线教育” 一、教师管理模块