xv6 6.S081 Lab3: alloc

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的内存。具体细节大家可以参考指导书,绝对够详细。
图1

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 k
  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;
    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 size
    int 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 free
    free = BLK_SIZE(k);
    if(bit_isset(bd_sizes[k].alloc, bi))
      lst_push(&bd_sizes[k].free, addr(k, buddy));   // put buddy on free list
    else
      lst_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 possible
  fk = 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-1
   char *q = p + BLK_SIZE(k-1);   // p's buddy
   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);
 }

其中,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 k
    if (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 list
    if(buddy % 2 == 0) {
      p = q;
    }
    // at size k+1, mark that the merged buddy pair isn't split
    // anymore
    bit_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-1
    char *q = p + BLK_SIZE(k-1);   // p's buddy
    bit_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 k
    mutual_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运行,测试通过,起飞✈

在这里插入图片描述

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值