操作系统-xv6 lab的四道题

关于操作系统xv6 lab2020的几道题目

1.Lab: Multithreading

1.1实验背景

1.1.1寄存器

进程在cpu中运行过程中进行切换时,它不会直接把整个进程进行保存和加载,而是通过寄存器的保存和加载实现的,这里我们讨论几种寄存器。

ra寄存器,它保存着程序进入处的值,把进程或者线程比作房子,则寄存器可以理解为进程或线程的“门”。例如是包括进程开始的函数指针。线程/进程切换时可从该处进入程序运行。

sp寄存器时栈顶寄存器,进程初始分配内存是以栈的形式从下往上分配,sp寄存器存放栈顶地址,这样进程/线程在切换时可通过栈顶寄存器找到原来的栈即后面在线程的结构体中的stack属性。

caller-save和callee-save是两种不同保存策略的寄存器,caller-save是由调用进程进行寄存器保存,callee-save由被调用者保存。

1.2实验过程

这里我们需要做的是在线程切换时完成寄存器的切换,寄存器记录了进程的一些重要的值,保存起来后才能借助这些被保留的寄存器来恢复进程,通用的寄存器有栈顶寄存器sp,记录程序处地址的ra寄存器…所以我们需要定义其结构体如下(与kernel/proc.h中内核的上下文一致),将其定义在user/uthread.c中,非必要说明,一下内容均在该c文件中修改。

struct context {
  uint64 ra;//程序入口
  uint64 sp;//栈顶寄存器

  // 被调用者保存的寄存器,s0-s11记录线程的一些指令和操作
  uint64 s0;
  uint64 s1;
  uint64 s2;
  uint64 s3;
  uint64 s4;
  uint64 s5;
  uint64 s6;
  uint64 s7;
  uint64 s8;
  uint64 s9;
  uint64 s10;
  uint64 s11;
};

在线程结构中加入属性上下文,有利于上下文跟线程的一一对应

struct thread {
  char       stack[STACK_SIZE]; //线程的栈
  int        state;             //有三种状态 FREE, RUNNING, RUNNABLE 

  struct context    c;//上下文属性,记录线程的寄存器
};

当调用函数生成线程时,我们需要对应设置ra的值和sp的值,其中一个是进程的进入点,一个时栈顶值,所以我们需要将进入点函数的指针赋给t的ra寄存器,而sp应该是从t的栈初始值出发,再加上栈大小,因为栈是从下向上的,栈顶地址应更高,如下

thread_create(void (*func)())
{
  struct thread *t;

  for (t = all_thread; t < all_thread + MAX_THREAD; t++) {
    if (t->state == FREE) break;
  }
  t->state = RUNNABLE;//修改线程状态
  
  t->c.ra = (uint64)func;//分配函数进入点
  t->c.sp = (uint64)t->stack + STACK_SIZE;//指向栈顶
}

其中fun是线程需要运作的内存,以一个线程b为例

thread_b(void)
{
  int i;
  printf("thread_b started\n");
  b_started = 1;//b开始
  while(a_started == 0 || c_started == 0)
    thread_yield();
  
  for (i = 0; i < 100; i++) {
    printf("thread_b %d\n", i);
    b_n += 1;
    thread_yield();//线程暂停,从正在运行变为可运行,进行线程的调度
  }
  printf("thread_b: exit after %d\n", b_n);

  current_thread->state = FREE;//线程结束
  thread_schedule();//调用schedule进行调度,线程切换
}

然后注意最后调用thread_schedule这时意味着当前线程已经状态空闲,其他线程可以开始工作,查找新的线程进行线程切换,其定义如下,线程调度时会一直循环知道找到可运行的线程,同时thread_yield也会将线程从运行调为可运行进行调度。

void 
thread_schedule(void)
{
  struct thread *t, *next_thread;

  //擦找可运行的线程
  next_thread = 0;
  t = current_thread + 1;//运行下一个线程
  for(int i = 0; i < MAX_THREAD; i++){
    if(t >= all_thread + MAX_THREAD)//遍历完没找到可运行的线程
      t = all_thread;//从头再找
    if(t->state == RUNNABLE) {//找到可运行的线程
      next_thread = t;
      break;
    }
    t = t + 1;
  }

  if (next_thread == 0) {//如果一直找不到
    printf("thread_schedule: no runnable threads\n");
    exit(-1);
  }
	
  if (current_thread != next_thread) {         //进行线程的切换
    next_thread->state = RUNNING; //线程状态从可运行变为运行中
    t = current_thread; //因为后面仍需要这个进行的寄存器信息,所以需要中间变量t
    current_thread = next_thread;//更新当前进程
    
    thread_switch((uint64)(&t->c),(uint64)(&next_thread->c));//调用进行调度函数进行上下文的切换
  } else
    next_thread = 0;
}

void 
thread_yield(void)
{
  current_thread->state = RUNNABLE;//调为可运行
  thread_schedule();//调度
}

thread_switch完成了上下文的切换,它是用汇编语言写的,这样运行的更快,进程切换的时间损耗就没那么大,其定义在user/thread_switch.S中


.text

/*
     保存旧寄存器,加载新的上下文
     */

.globl thread_switch
thread_switch:
    //保存旧的上下文(寄存器)信息
	sd ra, 0(a0)
	sd sp, 8(a0)
	sd s0, 16(a0)
	sd s1, 24(a0)
	sd s2, 32(a0)
	sd s3, 40(a0)
	sd s4, 48(a0)
	sd s5, 56(a0)
	sd s6, 64(a0)
	sd s7, 72(a0)
	sd s8, 80(a0)
	sd s9, 88(a0)
	sd s10, 96(a0)
	sd s11, 104(a0)
	//将新的上下文信息加载到寄存器中
	ld ra, 0(a1)
	ld sp, 8(a1)
	ld s0, 16(a1)
	ld s1, 24(a1)
	ld s2, 32(a1)
	ld s3, 40(a1)
	ld s4, 48(a1)
	ld s5, 56(a1)
	ld s6, 64(a1)
	ld s7, 72(a1)
	ld s8, 80(a1)
	ld s9, 88(a1)
	ld s10, 96(a1)
	ld s11, 104(a1)
	
	ret    /*进从ra处入新的线程*/

将旧寄存器的值保存换入新的内容,这时cpu运行的寄存器的值就变成新的进程,完成了切换,注意到callee-saved寄存器的值是我们需要保存的,而不是caller-saved,因为caller-saved会被保存在线程的栈中,不需要通过context保存。

1.3实验结果

运行uthread可以看到线程之前可以进行互相切换。结果如下

在这里插入图片描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RuruNizs-1615893999816)(C:\Users\16677\AppData\Roaming\Typora\typora-user-images\image-20210123213238602.png)]

2.Lab: locks/Memory allocator

2.1实验背景

xv6的内存分配器allocator它只使用了一个单独的链表保存可用物理内存,每个节点是一页的大小,通过头插法将内存进行扩充,其定义在kernel/kalloc.c中如下:

struct run {
  struct run *next;//节点
};

struct {
  struct spinlock lock;//自旋锁保护内存
  struct run *freelist;
} kmem;

其中run结构代表了每个可运行的页表即内存结构kmem中的列表freelist的节点,其地址指向可分配页表的地址,收lock锁的保护,避免一个cpu获取内存时与另一个cpu产生冲突。

分配器对内存的分配kinit定义如下:

void
kinit()
{
  initlock(&kmem.lock, "kmem");//给锁命名并初始化
  freerange(end, (void*)PHYSTOP);//清洗可用物理内存并一一存进freelist中
}

freerange通过对从kernel使用的后一个地址开始到phystop(即RAM)每一页调用kfree实现,kfree是将内存释放并添加进空闲内存链表freelist的函数,它通过将页表每一个地址赋值为1,以此避免新的线程或进程使用了旧的线程存放的值,然后通过节点头插入链表中使得内存可以被cpu中新的程序使用,因为地址从低到高分配并且是通过头插法进入链表中的,所以优先使用的总是高地址。在free中,freelist的扩充需要获取锁即调用acquire,以避免多cpu同时使用freelist产生冲突,可能的一种冲突是两个cpu同时free的时候造成freelist的表头指向错误,即如下:

在这里插入图片描述

这个时候就有页表被浪费了。

在内存分配方面,allocator使用kalloc进行分配,它首先判断freelist的表头是否为空,然后从表中获得内存,并将内存地址赋值为5即101,有效且可写,然后返回地址,如果无可分配地址返回的会是空指针。这里对freelist的操作同样需要获取和释放锁。

2.2实验过程

从背景我们可以知道多于多个cpu使用同一个列表,锁要被频繁的获取和释放,并且一个cpu在获取锁时如果锁被占用,必须等待锁被释放才能操作,cpu的效率被减低,所以我们需要减少这种冲突的发生,比较容易想到的就是给每个cpu都配自己的可用内存列表和各自的锁。即kmem定义多个,如下(该题代码均在kernel/kalloc.c中修改):

struct run {
  struct run *next;//指向运行的下一个节点
};

struct {
  struct spinlock lock;//锁
  struct run *freelist;
} kmem[NCPU];//通过NCPU获取cpu的个数

首先先初始化每个kmem:

void
kinit()
{
    int i;
    for(i = 0;i < NCPU;i++){
        char lockname[8];
        snprintf(lockname,6,"kmem%d",i);//保证锁名为kmem加上对应序号,即都是kmem开头
        initlock(&kmem[i].lock, lockname);//初始化锁
    }
    init_freerange(end, (void*)PHYSTOP);
}

原先的freerange是将空闲内存释放后放进一个freelist,由于这里是多个freelist,我们采用轮流分配的方式,比如如果有4个cpu,对于可用列表,分配顺序是0,1,2,3,0,1,2,3,0,1…通过取余的方式实现,如下:

void init_freerange(void *pa_start, void *pa_end){
  int order = 0;
  char *p;
  p = (char*)PGROUNDUP((uint64)pa_start);//取页表边界
  for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE){
      order ++;//轮到谁,需要取余
      init_kfree(p, (order % NCPU));//轮流分配
  }
}

其中init_free是将cpu释放的内存放进对应的freelist中,init_free定义如下:

void
init_kfree(void *pa, int cpu_order)
{
  //cpu_order是传入的指定cpu序号
  struct run *r;

  if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
    panic("kfree");


  memset(pa, 1, PGSIZE);//将地址赋值为1表示空闲并删除清理掉旧值

  r = (struct run*)pa;

  acquire(&kmem[cpu_order].lock);
  r->next = kmem[cpu_order].freelist;//头插法
  kmem[cpu_order].freelist = r;
  release(&kmem[cpu_order].lock);
}

须在文件开头对函数进行声明:

void init_freerange(void *pa_start, void *pa_end);
init_kfree(void *pa, int cpu_order);

声明原先的freerange和kfree也要从单个的kmem修改成kmem[order],即在对应的freelist和锁上进行操作,其中order是当前正在运行的cpu,可通过如下形式获取:

push_off();//防止中断的发生
int cpu_order = cpuid();//获取当前正在运行的cpuid
pop_off();

注意到我们需要利用push_off,pop_off来避免获取id和发生中断影响结果,而为什么不直接用freerange和kfree给kinit离的freelist分配内存却另外写新的函数是因为初始化时我们要将所有RAM内存分配给全部cpu,cpuid是要不断切换的,而当前运行的cpu只有一个。同时我们使用的轮流分配在多数情况下,即cpu的个数是2的指数个时,各cpu上所能获取的内存大小是一样的,我们也可以一开始将内存都放进同一个cpu,其他cpu的freelist就没有初始有内存,当他们需要内存时,会向那个占用所有内存的cpu“窃取”(这是我们下面所要讨论的),这种初始分配模式下我们可以不需要新定义的init_freerange和init_kfree,而只需要freerange和kfree。但是这种情况下,所有内存被分到同一个freelist,在前期其他cpu的freelsit还未“窃取”到足够的可用内存时,效果跟未进行改进时一样的,多个cpu频繁地会在一个锁上等待,产生冲突。

接下来讨论kalloc,当需要cpu需要内存时,它在自己的freelist上获取内存,如果没有,它通过遍历在其他cpu上查找是否有空闲内存,有的话取出,这种方式在各自cpu有充足内存时无需跟其他cpu竞争,因为有“窃取”的情形,所以还是需要上锁,同时cpuid我们还是通过cpuid()获取,需要关闭中断干扰,如下:

void *
kalloc(void)
{
  struct run *r;

  push_off();//防止中断干扰
  int cpu_order = cpuid();//获取当前正在运行的cpuid
  pop_off();

  acquire(&kmem[cpu_order].lock);
  r = kmem[cpu_order].freelist;
  if(r)//先从自己对应的freelsit找
    kmem[cpu_order].freelist = r->next;
  release(&kmem[cpu_order].lock);

  int i;
  if(!r)//如果cpu对应的freelist没有空闲内存,在其他cpu对应freelist找内存
    for(i = 0;i < NCPU;i++)
        if(i != cpu_order){
            acquire(&kmem[i].lock);
            r = kmem[i].freelist;
            if(r){
                kmem[i].freelist = r->next;
                release(&kmem[i].lock);
                break;//找到后不用继续循环,释放锁退出
            }
            release(&kmem[i].lock);
        }

  if(r)
    memset((char*)r, 5, PGSIZE); // 更改标识符101,表示已经被占用
  return (void*)r;
}

这里我们使用“偷”的方式,既然想到“偷”,是不是也可以用“借”呢?这两种方式的区别就是偷是不用还的,体现在一个cpu在窃取了其他cpu链表上的内存使用后,它是释放到自己的链表中,而没有“还给”被它窃取的cpu,这样可能会出现某个cpu的内存很快被偷和用完,又需要往其他cpu偷,而偷的过程是容易产生冲突的。但是“还”的存在两个问题,一是“还”的话需要记录它从哪个cpu借的,增加了内存开销,还有就是“还”同样需要对别的cpu的链表进行操作(插入节点),这个时候也要获取别的cpu的锁,也会产生冲突,这个冲突不一定比被“偷”完的cpu“回偷”产生冲突少,因此这里不做实现。

2.3实验结果

未对文件进行修改时,结果如下,其中acquire时对应单词开头的锁合计申请acquire次数,而fetch-and-add是获取锁失败进入循环重新申请的次数,用fetch来衡量冲突,可见对用kmem的锁433016中由241937次失败,超过一半,显然冲突比较激烈。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JGXm5aEG-1615893999825)(C:\Users\16677\AppData\Roaming\Typora\typora-user-images\image-20210123214609712.png)]

修改文件后结果如下,fetch-and-add变为0,冲突极大地解决了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vzPfKUig-1615893999829)(C:\Users\16677\AppData\Roaming\Typora\typora-user-images\image-20210123215835253.png)]

同时usertests也通过了测试。

3.Lab: locks/Buffer cache

3.1实验背景

3.1.1 哈希表和哈希桶

哈希表一种表示数据的方式,它可以降低数据搜索的次数,具体做法是用一个数据表示数据,然后从数据的属性中选择一个唯一标识的属性以为作为key值,输入指定的哈希映射函数f,得到
h a s h v a l u e = f ( k e y ) hashvalue = f(key) hashvalue=f(key)
hashvalue作为数据在哈希表的数组中的位置,这样我们搜索数据的时候就不用从头到尾或者从中间不断地将其他数据和要搜索的数据进行比对,从而能够更快地而且不干扰其他不相关数据进行搜索。但是f(key)很难单调地映射到数组的下标取值范围内,因此会出现多个key值映射到同一个位置上,成为哈希冲突。解决哈希冲突的方法可以通过哈希桶算法实现,在该算法中,它将映射到同一位置的所有数据构成一个双向链表,哈希表数组中的每个元素存储对应表头,对应的链表称为哈希桶,这样在搜索数据时仍需要对比,但是对比的范围从所有数据收缩至每个哈希桶,冲突域降低。

通常情况下桶的个数会选择质数,而映射函数为
h a s h v a l u e = k e y % M , M 为 桶 的 个 数 hashvalue = key \% M,M为桶的个数 hashvalue=key%MM
这样hashvalue除余求得的结果可以更加均匀地放在各个桶而不致使单个桶的链表长度过大。

3.1.2 xv6中的缓存机制

在xv6的文件系统中,从下往上可分为7层,其中cache缓存属于第二层,缓存层可以缓存磁盘块并同步访问它们,缓存的主要工作有两个,一个是同步访问磁盘块并保证同一时间每个磁盘块只有一个拷贝在内存中并且只有一个线程能同时使用它,另一个是使经常访问的内存可以在缓存中找到其拷贝而不必进入磁盘,因为磁盘的访问会比较慢。xv6的缓存是通过一个双向链表实现的,每个节点用结构体struct buf表示

struct buf {
  int valid;   // 判断节点是否有磁盘内存的拷贝,或者说缓存是否从磁盘中读取过数据
  int disk;    // 判断缓存是否更改过磁盘,即磁盘是否从缓存读取国数据
  uint dev;    // 识别数据的媒体信息
  uint blockno;  //识别数据块
  struct sleeplock lock; 
  uint refcnt;  //有多少个进程/线程在申请/等待使用这个数据
  struct buf *prev; // LRU cache list
  struct buf *next;
  uchar data[BSIZE]; //数据
};

其中查询数据通过dev和blockno实现,判断缓存是否空闲用refcnt,当它为0时说明已经没有线程等待它,则可以赋予新的磁盘数据和信息。在xv6中当缓存通过bread或bwrite被查询到并使用后,该缓存会被移至表头,当缓存上的线程通过brelse不断释放数据使refcnt为0时,该缓存会被放置到链表的表头,表示最近一个使用过并释放完的缓存块,因此当数据块在缓存中没有找到时,会在表尾开始找最久没被使用的空闲的缓存进行数据缓存。由于在查询过程中需要比对每个缓存的block和dev,以及找到后需要对refcnt进行修改,因为对整个双向链表进行上锁保护,这就造成了当多个进程同时访问数据时形成冲突,多个对锁的acquire被拒绝,如下我们可以看到:

这就是我们所要解决的问题,如何在数据的访问过程中减少锁的冲突。

3.2实验过程

我们考虑使用哈希桶的方式减少冲突。事实上第二题我们我们同样使处理锁的冲突,在第二题中我们使用的方法是为各个cpu单独分配一个内存表,也是一个哈希桶算法,这里每个cpu的链表就是一个桶,桶的个数是cpu的个数,key值就是所在cpu序号,哈希函数是等值映射。但是在缓存中这样的算法并不太使用,因为缓存与内存不一样,内存只在一个cpu的一个进程内分配使用,而缓存是在多个进程中共享同样也可以在多个cpu共享的,因为不能使用cpu序号作为key值,我们考虑使用数据块序号blockno作为key值,选择质数13作为哈希桶个数,哈希表描述如下:
k e y = b l o c k M = 13 h a s h v a l u e = k e y % M key = block\\M = 13\\hashvalue = key \%M key=blockM=13hashvalue=key%M
则用代码重新定义kernel/bio.c的缓存表如下:

#define NBUCKET 13 //定义桶的大小
struct {
  struct buf buf[NBUF];//初始的缓存值
  struct buf hashbucket[NBUCKET];
  struct spinlock hashlock[NBUCKET];
} bcache;

int HashV(int key){//哈希函数
    return key % NBUCKET;
}

首先对哈希表进行初始化,通过binit实现,将buf[NBUF]中的缓存快一一放到哈希表中,由于初始的缓存块blockno的值均被设为0,直接放入第一个桶即可,但为使代码更具解释性,仍通过b的blockno进行哈希映射,如下:

void
binit(void)
{
  struct buf *b;
  int i;
  for(i = 0; i < NBUCKET;i++){
    
    char lockname[8];
    snprintf(lockname,6,"bcache%d",i);//保证锁名为bcache加上对应序号,即都是bcache开头
    initlock(&bcache.hashlock[i], lockname);
    // 为每个桶生成双向链表
    bcache.hashbucket[i].prev = &bcache.hashbucket[i];
    bcache.hashbucket[i].next = &bcache.hashbucket[i];
  }
  
  for(b = bcache.buf; b < bcache.buf+NBUF; b++){
    int hashvalue = HashV(b->blockno);
    b->next = bcache.hashbucket[hashvalue].next;//头插法将b插入第一通中
    b->prev = &bcache.hashbucket[hashvalue];
    initsleeplock(&b->lock, "buffer");//初始化缓存的睡眠锁
    bcache.hashbucket[hashvalue].next->prev = b;
    bcache.hashbucket[hashvalue].next = b;
  }
}

再之后在bget中修改取缓存的方式,对应改为符合哈希桶的,bget是在bread和bwrite中调用以查找对应缓存的函数,它会返回上锁的缓存,如果在缓存找不到,它可以空闲缓存中生成一个匹配所需要磁盘数据的缓存块,更改后的哈希缓存搜索bget如下:

// 在缓存中查找在媒体dev的数据块,如果没找到,就分配一个缓存,返回上锁的缓存
static struct buf*
bget(uint dev, uint blockno)
{
  struct buf *b;
  
  int hashvalue = HashV(blockno);
  acquire(&bcache.hashlock[hashvalue]);

  // 查看缓存中是否存在对应数据块
  for(b = bcache.hashbucket[hashvalue].next; b != &bcache.hashbucket[hashvalue]; b = b->next){
    if(b->dev == dev && b->blockno == blockno){
      b->refcnt++;
      release(&bcache.hashlock[hashvalue]);//释放锁
      acquiresleep(&b->lock);//将对应的块上锁,避免进程同时更改数据以及一个进程结束后通知需要该数据的其他排队进程
      return b;
    }
  }

  // 如果没找到,从桶表尾查找空闲的缓存进行分配
  for(b = bcache.hashbucket[hashvalue].prev; b != & bcache.hashbucket[hashvalue]; b = b->prev){
    if(b->refcnt == 0) {
      b->dev = dev;//修改块
      b->blockno = blockno;
      b->time_stamp = ticks;
      b->valid = 0;
      b->refcnt = 1;

      release(&bcache.hashlock[hashvalue]);
      acquiresleep(&b->lock);
      return b;
    }
  }
  panic("bget: no buffers");
    
}

这里我们需要给buf加入时间戳属性time_stamp,同时利用ticks(在kernel/trap.c文件中定义赋值)由于ticks是外部变量需要引入,有:

extern uint ticks;

它反应当前运行的时间戳,利用该属性可以通过查找最小时间戳(在当前桶的最小,不是整体缓存的最小)的方式,在上面的两次遍历的第一次遍历时查找是否有refcnt为0的缓存块更新最小时间戳,当遍历结束后没找到缓存可直接用最小时间戳对应的缓存进行生成,就不用再进行遍历也可以获取最久没被使用的缓存块,如下:

// 在缓存中查找在媒体dev的数据块,如果没找到,就分配一个缓存,返回上锁的缓存
static struct buf*
bget(uint dev, uint blockno)
{
  struct buf *b;
  
  int hashvalue = HashV(blockno);
  acquire(&bcache.hashlock[hashvalue]);

  uint min_time_stamp = -1;//最小时间戳,无符号整型中-1是最大值
  struct buf *min_bcache = 0;//最小时间戳对应的缓存块
    
  // 查看缓存中是否存在对应数据块
  for(b = bcache.hashbucket[hashvalue].next; b != &bcache.hashbucket[hashvalue]; b = b->next){
    if(b->dev == dev && b->blockno == blockno){
      b->refcnt++;
      release(&bcache.hashlock[hashvalue]);//释放锁
      acquiresleep(&b->lock);//将对应的块上锁,避免进程同时更改数据以及一个进程结束后通知需要该数据的其他排队进程
      return b;
    }
    //对最小时间戳进行更新
    if(b->refcnt == 0 && b ->time_stamp < min_time_stamp){
        min_time_stamp = b ->time_stamp;
        min_bcache = b;
    }
  }
  b = min_bcache;
  // 如果没找到,利用最小时间戳的缓存进行分配进行分配
  if(b != 0){//最小时间戳更新后b才不会等于0,此时成功在当前桶找到空心的缓存
    b->dev = dev;//修改块
    b->blockno = blockno;
    b->valid = 0;
    b->refcnt = 1;

    release(&bcache.hashlock[hashvalue]);
    acquiresleep(&b->lock);
    return b;
  }
  panic("bget: no buffers");
    
}

当所在桶没有空闲的缓存时,可以从其他的桶进行窃取,注意到利用空闲的缓存分配时,其blockno会被修改,这时候它映射到的哈希值可能会有变化,需要把他移出当前的哈希桶并放入对应的哈希桶中。故在bget的panic之前添加如下代码:

//如果在当前桶找不到,去其他桶偷
  int i;
  for(i = 0;i < NBUCKET;i++){
    if(i == hashvalue)continue;//当前桶已经找过,跳过,如果没有这一步会再次获取该桶的锁,由于前面获取的还没释放,会陷入死锁
    acquire(&bcache.hashlock[i]);
    for(b = bcache.hashbucket[i].prev; b != & bcache.hashbucket[i]; b = b->prev){
      if(b->refcnt == 0) {
        b->dev = dev;
        b->blockno = blockno;
        b->time_stamp = ticks;
        b->valid = 0;
        b->refcnt = 1;

        b->next->prev = b->prev;//将缓存块从当前桶移出
        b->prev->next = b->next;
        acquire(&bcache.hashlock[hashvalue]);
        //被引用的缓存移到对应桶

        b->next = bcache.hashbucket[hashvalue].next;//移入
        b->prev = &bcache.hashbucket[hashvalue];
        bcache.hashbucket[hashvalue].next->prev = b;
        bcache.hashbucket[hashvalue].next = b;
        release(&bcache.hashlock[i]);//释放两把锁,一把是查询所在桶的,一个哈希映射到的桶的
        release(&bcache.hashlock[hashvalue]);
        acquiresleep(&b->lock);
        return b;
      }
    }
    release(&bcache.hashlock[i]);//没有找到也要记得释放锁
  }

然后就是brelse了,我们将缓存进行释放,如果refcnt为0,该缓存空闲了,需要将它移到表头,这里我们允许不使用锁,因为当多个缓存同时被释放时,只需要更新最新的即时间戳最大的即可,如下:

//释放缓存,同时将refcnt为0的缓存移到表头
void
brelse(struct buf *b)
{
  if(!holdingsleep(&b->lock))
    panic("brelse");
  
  releasesleep(&b->lock);
  int hashvalue = HashV(b->blockno);
  b->time_stamp = ticks;
  if(b->time_stamp == ticks){
    b->refcnt--;
    if (b->refcnt == 0) {
      // 将b移到表头
      b->next->prev = b->prev;
      b->prev->next = b->next;
      b->next = bcache.hashbucket[hashvalue].next;
      b->prev = &bcache.hashbucket[hashvalue];
      bcache.hashbucket[hashvalue].next->prev = b;
      bcache.hashbucket[hashvalue].next = b;
    }
  }

}

另外bpin和bunpin也要把锁对应修改如下:

void
bpin(struct buf *b) {
  int hashvalue = HashV(b->blockno);//获取对应的桶
  acquire(&bcache.hashlock[hashvalue]);
  b->refcnt++;
  release(&bcache.hashlock[hashvalue]);
}

void
bunpin(struct buf *b) {
  int hashvalue = HashV(b->blockno);//获取对应的桶
  acquire(&bcache.hashlock[hashvalue]);
  b->refcnt--;
  release(&bcache.hashlock[hashvalue]);
}

3.3实验结果

未修改文件前,运行bcachetest结果如下,冲突值很大,219227甚至远大于成功acquire的值65028

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YWNnT63j-1615893999836)(C:\Users\16677\AppData\Roaming\Typora\typora-user-images\image-20210123225125262.png)]

修改文件后运行结果如下,冲突降低未0,通过测试。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6JVdtKJv-1615893999837)(C:\Users\16677\AppData\Roaming\Typora\typora-user-images\image-20210124003527494.png)]

4.Lab: file system/Large files

4.1实验背景

Inode层是文件系统从下往上的第四层,它主要涉及文件的组织。相关结构有两个,一个是dinode,它是在磁盘上的一块连续区域,另一个是inode,它是dnode在内存上的拷贝。其中inode在xv6中在kernel/file.h定义如下:

// in-memory copy of an inode
struct inode {
  uint dev;           // 媒体序号
  uint inum;          // inode序号
  int ref;            // 等待数据的进程个数
  struct sleeplock lock; // 保护下面的属性
  int valid;          // 是否已

  short type;         // 以下均从dinode拷贝
  short major;
  short minor;
  short nlink;
  uint size;
  uint addrs[NDIRECT+1];
};

而dinode在kernel/fs.h中定义如下:

// 磁盘上的inode
struct dinode {
  short type;           // 判断是文件还是目录还是特殊文件
  short major;          // Major device number (T_DEVICE only)
  short minor;          // Minor device number (T_DEVICE only)
  short nlink;          // 在文件系统中有多少链接该节点
  uint size;            // 文件的大小
  uint addrs[NDIRECT+1];   // 数据块的地址
};

其中addrs数组指向inode的数据的地址,一般NDIRECT为12,即该文件inode直接存放指向12个数据块的地址,而addrs的大小是12+1,还有一个指向另一块间接的inode存放256个地址 ,这样一共有256+12=268个数据块。一个块的大小为BSIZE=1kB,则文件的最大大小为268kB。dinode结构如下(选自xv6-riscv-book第90页):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yNjyTBwk-1615893999839)(C:\Users\16677\AppData\Roaming\Typora\typora-user-images\image-20210123113844598.png)]

Bmap函数是xv6中用于获取数据的入口,它接受一个两个参数,一个是所要查询的indode,一个是逻辑地址dn,

dn是数据块在268个块中的对应位置的序号,如果dn在0-11,则在inode自身的addrs数组可找到其地址,如果dn大于11则在addrs[12]指向的间接块的第dn-12的位置可以找到数据地址,同时bmap对于数据是按需分配的,当检测到地址入口为0时,通过调用balloc分配地址,其过程如下:

static uint
bmap(struct inode *ip, uint bn)
{
  uint addr, *a;
  struct buf *bp;

  if(bn < NDIRECT){//如果小于12,则在addrs数组中查找
    if((addr = ip->addrs[bn]) == 0)//地址为0则通过balloc分配
      ip->addrs[bn] = addr = balloc(ip->dev);
    return addr;
  }
  bn -= NDIRECT;//大于12,在间接块的第bn-12的位置

  if(bn < NINDIRECT){
    // 间接块中查找
    if((addr = ip->addrs[NDIRECT]) == 0)//直接块中间接块的入口为0,则通过balloc分配
      ip->addrs[NDIRECT] = addr = balloc(ip->dev);
    bp = bread(ip->dev, addr);//通过直接块中间接块的入口找到间接块
    a = (uint*)bp->data;
    if((addr = a[bn]) == 0){//如果为0调用balloc分配
      a[bn] = addr = balloc(ip->dev);
      log_write(bp);//log记录操作
    }
    brelse(bp);//调用完bread后需要释放缓存
    return addr;
  }

  panic("bmap: out of range");
}

因为文件最大只能容纳278个数据块,所以本实验我们希望在addrs中牺牲一个直接地址更改成一个二级索引,即指向一个256的间接索引,该块的每一个项都指向一个间接块,其整体结构如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Misjgi7e-1615893999840)(C:\Users\16677\AppData\Roaming\Typora\typora-user-images\image-20210123124745435.png)]

4.2实验过程

需要将kernel/param.h中的FSSIZE的数值更改,因为文件最大由268kB变成了65308kB,所以可将FSSIZE改成200000。不过这一步似乎切换分支后自动完成了。然后因为我们要将直接映射从12改成11然后加一个二级映射,所以修改dinode如下,

// 磁盘上的inode
struct dinode {
  short type;           // 判断是文件还是目录还是特殊文件
  short major;          // Major device number (T_DEVICE only)
  short minor;          // Minor device number (T_DEVICE only)
  short nlink;          // 在文件系统中有多少链接该节点
  uint size;            // 文件的大小
  //修改成NDIRECT+2,2个间接块
  uint addrs[NDIRECT+2];   // 数据块的地址
};

其中NDIRECT从12变成11,同时最大文件MAXFILE也应该对应修改,因为二级映射可对应256*256个块,所以文件最大时直接映射,一级映射和二级映射的求和,如下:

#define NDIRECT 11

#define NINDIRECT (BSIZE / sizeof(uint))
#define NDOUBLE_INDIRECT NINDIRECT*NINDIRECT

// #define MAXFILE (NDIRECT + NINDIRECT) //之前的
#define MAXFILE (NDIRECT + NINDIRECT + NDOUBLE_INDIRECT)//修改后

应为inode时dinode在内存上的拷贝,需要做同样的修改。如下

// in-memory copy of an inode
struct inode {
  uint dev;           // 媒体序号
  uint inum;          // inode序号
  int ref;            // 等待数据的进程个数
  struct sleeplock lock; // 保护下面的属性
  int valid;          // 是否已

  short type;         // 以下均从dinode拷贝
  short major;
  short minor;
  short nlink;
  uint size;
  uint addrs[NDIRECT+2];
};

然后我们需要bmap进行修改,因为之前的bmap只提供有12个直接映射和1个一级映射,现在直接映射减少了,一级映射在数组中的位置对应页提前了以为,这些通过前面对NDIRECT的修改已经完成,只需要添加对2级映射的处理,我们同样提供按需分配,如果数据块所在的间接映射块没有被用到我们不对其分配,只有调用了bmap进行查询的块我们对它和它的一级或二级映射进行分配。在二级映射中我们可将间接块看作一个256*256的二维数组,下一行记录的第一个位置和改行最后一个位置相接,那样bn超出11+256的部分进行除以256的商作为行,余作为列,则可以找到对应的数据块。如下:

static uint
bmap(struct inode *ip, uint bn)
{
  uint addr, *a;
  struct buf *bp;

  if(bn < NDIRECT){//如果小于12,则在addrs数组中查找
    if((addr = ip->addrs[bn]) == 0)//地址为0则通过balloc分配
      ip->addrs[bn] = addr = balloc(ip->dev);
    return addr;
  }
  bn -= NDIRECT;//大于11,在间接块找
  //进入在数组倒数第二位的一级映射中
  if(bn < NINDIRECT){
    // 间接块中查找
    if((addr = ip->addrs[NDIRECT]) == 0)//直接块中间接块的入口为0,则通过balloc分配
      ip->addrs[NDIRECT] = addr = balloc(ip->dev);
    bp = bread(ip->dev, addr);//通过直接块中间接块的入口找到间接块
    a = (uint*)bp->data;
    if((addr = a[bn]) == 0){//如果为0调用balloc分配
      a[bn] = addr = balloc(ip->dev);
      log_write(bp);//调用完bread后需要释放缓存
    }
    brelse(bp);
    return addr;
  }
  bn -= NINDIRECT;//大于11+256,在二级映射里找
  //进入二级映射中
  if(bn < NINDIRECT*NINDIRECT){
    int sigle_direct = bn/NINDIRECT;  //“行”
    int double_direct = bn%NINDIRECT; //“列”
    // 间接块中查找
    if((addr = ip->addrs[NDIRECT+1]) == 0)//间接块的入口为0,则通过balloc分配
      ip->addrs[NDIRECT+1] = addr = balloc(ip->dev);
      
    bp = bread(ip->dev, addr);//通过直接块找到一级映射的间接块
    a = (uint*)bp->data;
    if((addr = a[sigle_direct]) == 0){//如果为0调用balloc分配
      a[sigle_direct] = addr = balloc(ip->dev);
      log_write(bp);
    }
    brelse(bp);//调用完bread后需要释放缓存
    
    bp = bread(ip->dev,addr);//二级映射中找到数据地址
    a = (uint*)bp->data;
    if((addr = a[double_direct]) == 0){//如果为0调用balloc分配
      a[double_direct] = addr = balloc(ip->dev);
      log_write(bp);
    }   
    brelse(bp);//调用完bread后需要释放缓存   
    return addr;
  }
  panic("bmap: out of range");
}

4.3运行结果

修改前,只能写267个块(268-1)

在这里插入图片描述

修改后,可以写65803(65804-1),测试通过。

5.其他

有时候做完实验后切换分支时总z是提示失败,需要在切换前提交,因为对git的不熟悉,前两次实验都是手动保存修改后的文件然后剪切掉的,发现可以利用git stash存放修改完的内存,然后就可以进行分支的切换了,同时还可以利用git status查看是否有需要提交的文件,如果显示工作台为空可直接进行切换。(对git不是很熟悉)

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值