xv6 6.S081 Lab3: alloc

  • 写在前面
  • 实验介绍
  • 开始!
    • 任务再描述
    • 任务一实现
    • 任务二实现
      • Buddy Allocator
      • Code Thru
      • 任务二的实现

alloc代码在这里。另外,本文主要是将我的实验报告搬了下来,因此内容难免偏多,可以一边结合代码、一边结合实验指导书食用。

写在前面

Buddy Allocator是Linux中著名的内存分配器,详情可以参考这里的实验指导书(PS:写得真的非常棒)

实验介绍

本次实验由两个任务构成:

  1. 利用bd_malloc 实现文件动态分配
  2. 优化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个字段:freealloc以及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)函数开始,该函数的任务是将从baseend的内存交给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应该管理的内存(baseend)中的,因此,在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的allocsplit的分布情况:

显然,图中黄色背景标记的内存块都应该接入相应的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首先找到刚好大于nbytessize 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))将内存块psize k中所对应的“是否被分配状态”标记为1,表明已被分配。char *q = p + BLK_SIZE(k-1)找到了psize 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)分别将psize k中的“是否被分割的状态”标记为1,将psize 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()函数的参数:pbd_end,它们分别表示meta段末尾地址、无效内存的起始地址,接下来,我们继续借用我工的图,重绘以标记pbd_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 kbd_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相关推荐

  1. 6.S081 Lab3 page tables

    6.S081 Lab3 page tables 未完成 文章目录 6.S081 Lab3 page tables 未完成 1. Print a page table ([easy](https://p ...

  2. xv6 6.S081 Lab1: util

    xv6 6.S081 Lab1: util 写在前面 实验介绍 开始! sleep pingpong Primes Find Xargs 拖了这么久,终于稍微有时间填坑了.今天介绍xv6的第一个实验u ...

  3. xv6 6.S081 Lab5: cow

    xv6 6.S081 Lab5: cow 写在前面 实验介绍 开始! cow代码在这里.完成了lazy后,cow的实现就非常明了了-- 写在前面 经典写在前面

  4. xv6 6.S081 Lab8: fs

    xv6 6.S081 Lab8: fs 写在前面 实验介绍 开始! Large File Symbolic links fs代码在这里.我的妈呀,终于要写完了,xv6的file system考察难度并 ...

  5. xv6 6.S081 Lab4: lazy

    xv6 6.S081 Lab4: lazy 写在前面 实验介绍 开始! 打印页表 实现Lazy Allocation 修改sbrk() 实现Lazy Allocation 完善Lazy Allocat ...

  6. xv6 6.S081 Lab7: Lock

    xv6 6.S081 Lab7: Lock 写在前面 实验介绍 开始! Memory Allocator Buffer Cache lock代码在这里.本次实验理解起来简单,做起来也容易 写在前面 老 ...

  7. MIT6.S081 Lab3 Page tables

    lab1.2不是太难,lab 3太变态了,github上记一下代码,源代码地址 :https://github.com/CodePpoi/mit-lab 参考博客 : https://blog.csd ...

  8. 6.s081 lab3

    lab3 页表初始化过程: 物理页是一组由run结构体保存的,每个runmain函数调用kinit,初始化物理页.kinit调用了freerange.把内核对应的物理页全部释放掉,加入到freelis ...

  9. MIT6.S081 Lab3: page tables

    Print a page table 接收一个pagetable_t并把它指向的页表打印. 在kernel/def.h中增加函数声明void vmprint(void)并在kernel/vm.c中定义 ...

最新文章

  1. redis的导入导出需要特别注意的地方
  2. 021Python路--单例设计模式
  3. 不用AJAX实现前台JS调用后台C#方法(小技巧)
  4. 是什么让美国网站拒绝欧洲访问?- GDPR 带来的数据安全思考
  5. python对象传递_Python参数传递对象的引用原理解析
  6. (最短路径算法整理)dijkstra、floyd、bellman-ford、spfa算法模板的整理与介绍
  7. Win10/Server2016镜像集成离线补丁
  8. java对象调用方法,java 对象调用
  9. Java毕设项目电商后台管理系统计算机(附源码+系统+数据库+LW)
  10. 【资源分享】Dll Injector(DLL注入器)
  11. 海思isp图像处理芯片_最新海思芯片3559A的功能简介
  12. 微火上线ai绘画小程序搭建系统,ai绘画小程序源码触手可及
  13. python中内置数学函数详解和实例应用之三角函数_初级阶段(二)
  14. java面试宝典(综合版)
  15. Python爬虫框架Scrapy入门(三)爬虫实战:爬取长沙链家二手房
  16. 483g路由器连接服务器无响应,TP-LINK企业路由器设置 TP-LINK TL-R483 Wan口设置图文教程...
  17. kubectl logs 常用命令
  18. Linux下的terminal多窗口开启及切换
  19. (三)UPF之Domain Coverage Relationship(Cover、Equivalent、Independent)
  20. 微服务项目实战技术点汇总:“尚硅谷的谷粒在线教育” 一、教师管理模块

热门文章

  1. arduino安装+esp32+esp8266安装
  2. harbor搭建-主从复制
  3. 【2】C++语法与数据结构之MFC_CList学生管理系统_链表内排序_函数指针
  4. BZOJ 1002 1003 1007 被屠记录
  5. 中国己内酰胺市场前景策略分析与投资调研评估报告2022年版
  6. orign绘制双排Stack Column
  7. live2d在vue中的运用
  8. 华为核心交换机HW_S7706添加静态路由
  9. python解决跨域_Python | 跨域
  10. Dining (匹配,最大流)