MIT 6.S081 Lab7 Locks 锁优化

317 阅读5分钟

Lab地址:pdos.csail.mit.edu/6.828/2021/…

git代码地址:github.com/cardchoosen…

Lab: Locks

Memory allocator

xv6中的kalloc实现非常原始,所有空闲页面集合在一个长链表中,且只有一把大锁来控制该链表结构的访问控制,当有多个进程在不同CPU上同时频繁调用kalloc和kfree时,对该锁的抢占竞争会非常频繁,导致严重的性能、资源浪费。

在Makefile中添加该测试的用户程序:

UPROGS=\
    ...
    $U/_kalloctest\

执行kalloctest可以看到如下输出,对kmem的锁的fetch-and-add 次数高达83375,是全局的锁中抢占最激烈的,我们的工作就是在不同cpu上同时调用kalloc/kfree时避免这种对锁的抢占。预期降kmem:#fetch-and-add降为0.

$ kalloctest
start test1
test1 results:
--- lock kmem/bcache stats
lock: kmem: #fetch-and-add 83375 #acquire() 433015
lock: bcache: #fetch-and-add 0 #acquire() 1260
--- top 5 contended locks:
lock: kmem: #fetch-and-add 83375 #acquire() 433015
lock: proc: #fetch-and-add 23737 #acquire() 130718
lock: virtio_disk: #fetch-and-add 11159 #acquire() 114
lock: proc: #fetch-and-add 5937 #acquire() 130786
lock: proc: #fetch-and-add 4080 #acquire() 130786
tot= 83375
test1 FAIL

思路是:将集合在一起的长链表,分成每个CPU上独占一份,且为每一份空闲链表单独加锁,也就是说将该结构拆解,拆解成与CPU相同粒度,当某个进程在某个CPU上执行需要使用kmem时,会先关闭中断以执行cpuid()调用获取当前CPU ID,并根据id获取其freelist的锁,并在该freelist上分配一个页面返回,同时打开中断。这里有个问题,如果在一个CPU上分配了太多页面时,该CPU对应的freelist会耗尽,此时需要从其他CPU上窃取一些page到当前CPU,这步操作在本实验的提交代码中是存在死锁问题的(需要额外分配一个窃取行为锁来解决)。

kalloc.c

struct
{
  struct spinlock lock;
  struct run *freelist;
} kmem[NCPU];

char *kemem_lock_names[NCPU] = {
    "kmem_cpu_0",
    "kmem_cpu_1",
    "kmem_cpu_2",
    "kmem_cpu_3",
    "kmem_cpu_4",
    "kmem_cpu_5",
    "kmem_cpu_6",
    "kmem_cpu_7",
};

void kinit()
{
  for (int i = 0; i < NCPU; i++)
    initlock(&kmem[i].lock, kemem_lock_names[i]);
  freerange(end, (void *)PHYSTOP);
}

void kfree(void *pa)
{
  struct run *r;

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

  // Fill with junk to catch dangling refs.
  memset(pa, 1, PGSIZE);

  r = (struct run *)pa;

  push_off();
  int cpu = cpuid();

  acquire(&kmem[cpu].lock);
  r->next = kmem[cpu].freelist;
  kmem[cpu].freelist = r;
  release(&kmem[cpu].lock);

  pop_off();
}

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

  push_off();

  int cpu = cpuid();

  acquire(&kmem[cpu].lock);

  if (!kmem[cpu].freelist)
  { // no free list left for current cpu
    int steal_left = 64;
    for (int i = 0; i < NCPU; i++)
    {
      if (i == cpu)
        continue;
      acquire(&kmem[i].lock);
      struct run *rr = kmem[i].freelist;
      while (rr && steal_left)
      {
        kmem[i].freelist = rr->next;
        rr->next = kmem[cpu].freelist;
        kmem[cpu].freelist = rr;
        rr = kmem[i].freelist;
        steal_left--;
      }
      release(&kmem[i].lock);
      if (steal_left == 0)
        break; // done stealing
    }
  }
  r = kmem[cpu].freelist;
  if (r)
    kmem[cpu].freelist = r->next;
  release(&kmem[cpu].lock);

  pop_off();

  if (r)
    memset((char *)r, 5, PGSIZE); // fill with junk
  return (void *)r;
}

再次编译运行kalloctest:

$ kalloctest
start test1
test1 results:
--- lock kmem/bcache stats
lock: kmem_cpu_0: #test-and-set 0 #acquire() 44598
lock: kmem_cpu_1: #test-and-set 0 #acquire() 195829
lock: kmem_cpu_2: #test-and-set 0 #acquire() 192591
lock: bcache: #test-and-set 0 #acquire() 1262
--- top 5 contended locks:
lock: virtio_disk: #test-and-set 130002 #acquire() 114
lock: proc: #test-and-set 59124 #acquire() 191351
lock: proc: #test-and-set 33601 #acquire() 191295
lock: proc: #test-and-set 33526 #acquire() 191353
lock: proc: #test-and-set 32191 #acquire() 191294
tot= 0
test1 OK
start test2
total free number of pages: 32499 (out of 32768)
.....
test2 OK

Buffer cache (hard)

If multiple processes use the file system intensively, they will likely contend for bcache.lock, which protects the disk block cache in kernel/bio.c. bcachetest creates several processes that repeatedly read different files in order to generate contention on bcache.lock; its output looks like this (before you complete this lab):

如果多个进程密集地使用文件系统,它们很可能会争夺 bcache.lock,这个锁用于保护 kernel/bio.c 中的磁盘块缓存。bcachetest 创建了几个进程,这些进程反复读取不同的文件,以便在 bcache.lock 上产生争用;在你完成这个实验之前,它的输出如下:

$ bcachetest
start test0
test0 results:
--- lock kmem/bcache stats
lock: kmem: #fetch-and-add 0 #acquire() 33035
lock: bcache: #fetch-and-add 16142 #acquire() 65978
--- top 5 contended locks:
lock: virtio_disk: #fetch-and-add 162870 #acquire() 1188
lock: proc: #fetch-and-add 51936 #acquire() 73732
lock: bcache: #fetch-and-add 16142 #acquire() 65978
lock: uart: #fetch-and-add 7505 #acquire() 117
lock: proc: #fetch-and-add 6937 #acquire() 73420
tot= 16142
test0: FAIL
start test1
test1 OK

Modify the block cache so that the number of acquire loop iterations for all locks in the bcache is close to zero when running bcachetest. Ideally the sum of the counts for all locks involved in the block cache should be zero, but it's OK if the sum is less than 500. Modify bget and brelse so that concurrent lookups and releases for different blocks that are in the bcache are unlikely to conflict on locks (e.g., don't all have to wait for bcache.lock). You must maintain the invariant that at most one copy of each block is cached. When you are done, your output should be similar to that shown below (though not identical). Make sure usertests still passes. make grade should pass all tests when you are done.

修改块缓存,以便在运行 bcachetest 时,bcache 中所有锁的获取循环迭代次数接近零。理想情况下,块缓存中涉及的所有锁的计数总和应为零,但如果总和小于 500 也可以。修改 bget 和 brelse,以便在 bcache 中对不同块的并发查找和释放不太可能在锁上发生冲突(例如,不必都等待 bcache.lock)。必须保持每个块最多只有一个副本被缓存的不变性。完成后,你的输出应与下面显示的类似(尽管不完全相同)。确保 usertests 仍然通过。

$ bcachetest
start test0
test0 results:
--- lock kmem/bcache stats
lock: kmem: #fetch-and-add 0 #acquire() 32954
lock: kmem: #fetch-and-add 0 #acquire() 75
lock: kmem: #fetch-and-add 0 #acquire() 73
lock: bcache: #fetch-and-add 0 #acquire() 85
lock: bcache.bucket: #fetch-and-add 0 #acquire() 4159
lock: bcache.bucket: #fetch-and-add 0 #acquire() 2118
lock: bcache.bucket: #fetch-and-add 0 #acquire() 4274
lock: bcache.bucket: #fetch-and-add 0 #acquire() 4326
lock: bcache.bucket: #fetch-and-add 0 #acquire() 6334
lock: bcache.bucket: #fetch-and-add 0 #acquire() 6321
lock: bcache.bucket: #fetch-and-add 0 #acquire() 6704
lock: bcache.bucket: #fetch-and-add 0 #acquire() 6696
lock: bcache.bucket: #fetch-and-add 0 #acquire() 7757
lock: bcache.bucket: #fetch-and-add 0 #acquire() 6199
lock: bcache.bucket: #fetch-and-add 0 #acquire() 4136
lock: bcache.bucket: #fetch-and-add 0 #acquire() 4136
lock: bcache.bucket: #fetch-and-add 0 #acquire() 2123
--- top 5 contended locks:
lock: virtio_disk: #fetch-and-add 158235 #acquire() 1193
lock: proc: #fetch-and-add 117563 #acquire() 3708493
lock: proc: #fetch-and-add 65921 #acquire() 3710254
lock: proc: #fetch-and-add 44090 #acquire() 3708607
lock: proc: #fetch-and-add 43252 #acquire() 3708521
tot= 128
test0: OK
start test1
test1 OK
$ usertests
  ...
ALL TESTS PASSED
$

Please give all of your locks names that start with "bcache". That is, you should call initlock for each of your locks, and pass a name that starts with "bcache".

Reducing contention in the block cache is more tricky than for kalloc, because bcache buffers are truly shared among processes (and thus CPUs). For kalloc, one could eliminate most contention by giving each CPU its own allocator; that won't work for the block cache. We suggest you look up block numbers in the cache with a hash table that has a lock per hash bucket.

There are some circumstances in which it's OK if your solution has lock conflicts:

  • When two processes concurrently use the same block number. bcachetest test0 doesn't ever do this.
  • When two processes concurrently miss in the cache, and need to find an unused block to replace. bcachetest test0 doesn't ever do this.
  • When two processes concurrently use blocks that conflict in whatever scheme you use to partition the blocks and locks; for example, if two processes use blocks whose block numbers hash to the same slot in a hash table. bcachetest test0 might do this, depending on your design, but you should try to adjust your scheme's details to avoid conflicts (e.g., change the size of your hash table).

bcachetest's test1 uses more distinct blocks than there are buffers, and exercises lots of file system code paths.

Here are some hints:

  • Read the description of the block cache in the xv6 book (Section 8.1-8.3).
  • It is OK to use a fixed number of buckets and not resize the hash table dynamically. Use a prime number of buckets (e.g., 13) to reduce the likelihood of hashing conflicts.
  • Searching in the hash table for a buffer and allocating an entry for that buffer when the buffer is not found must be atomic.
  • Remove the list of all buffers (bcache.head etc.) and instead time-stamp buffers using the time of their last use (i.e., using ticks in kernel/trap.c). With this change brelse doesn't need to acquire the bcache lock, and bget can select the least-recently used block based on the time-stamps.
  • It is OK to serialize eviction in bget (i.e., the part of bget that selects a buffer to re-use when a lookup misses in the cache).
  • Your solution might need to hold two locks in some cases; for example, during eviction you may need to hold the bcache lock and a lock per bucket. Make sure you avoid deadlock.
  • When replacing a block, you might move a struct buf from one bucket to another bucket, because the new block hashes to a different bucket. You might have a tricky case: the new block might hash to the same bucket as the old block. Make sure you avoid deadlock in that case.
  • Some debugging tips: implement bucket locks but leave the global bcache.lock acquire/release at the beginning/end of bget to serialize the code. Once you are sure it is correct without race conditions, remove the global locks and deal with concurrency issues. You can also run make CPUS=1 qemu to test with one core.

请为你的所有锁赋予以 “bcache” 开头的名称。也就是说,你应该为每个锁调用 initlock,并传递一个以 “bcache” 开头的名称。

在块缓存中减少竞争比在 kalloc 中更棘手,因为 bcache 缓冲区在进程(以及 CPU)之间真正共享。对于 kalloc,可以通过为每个 CPU 分配自己的分配器来消除大部分竞争;但这在块缓存中不起作用。我们建议你使用一个哈希表在缓存中查找块编号,该哈希表每个哈希桶有一个锁。

在某些情况下,如果你的解决方案存在锁冲突是可以的:

  • 当两个进程同时使用相同的块编号时。bcachetest 的 test0 从不这样做。
  • 当两个进程同时在缓存中未命中,并需要找到一个未使用的块进行替换时。bcachetest 的 test0 从不这样做。
  • 当两个进程同时使用在你用于划分块和锁的任何方案中冲突的块时;例如,如果两个进程使用的块的块编号哈希到哈希表中的同一个槽。bcachetest 的 test0 可能会这样做,具体取决于你的设计,但你应该尝试调整你的方案的细节以避免冲突(例如,更改哈希表的大小)。

bcachetest 的 test1 使用比缓冲区更多的不同块,并执行大量文件系统代码路径。

以下是一些提示:

  • 阅读 xv6 书籍中关于块缓存的描述(第 8.1 - 8.3 节)。
  • 可以使用固定数量的桶,并且不要动态调整哈希表的大小。使用质数个桶(例如,13)以减少哈希冲突的可能性。
  • 在哈希表中查找缓冲区并在未找到缓冲区时为该缓冲区分配一个条目必须是原子操作。
  • 删除所有缓冲区的列表(bcache.head 等),而是使用它们最后一次使用的时间(即,使用 kernel/trap.c 中的 ticks)为缓冲区添加时间戳。有了这个改变,brelse 就不需要获取 bcache 锁,并且 bget 可以根据时间戳选择最近最少使用的块。
  • 在 bget 中序列化驱逐是可以的(即,当在缓存中查找未命中时,bget 中选择一个缓冲区进行重用的部分)。
  • 在某些情况下,你的解决方案可能需要持有两个锁;例如,在驱逐期间,你可能需要持有 bcache 锁和每个桶一个锁。确保避免死锁。
  • 当替换一个块时,你可能会将一个 struct buf 从一个桶移动到另一个桶,因为新块哈希到不同的桶。你可能会遇到一个棘手的情况:新块可能哈希到与旧块相同的桶。在这种情况下确保避免死锁。

一些调试提示:实现桶锁,但在 bget 的开头 / 结尾保留全局 bcache.lock 的获取 / 释放以序列化代码。一旦你确定在没有竞争条件的情况下它是正确的,删除全局锁并处理并发问题。你也可以运行 make CPUS = 1 qemu 以使用一个核心进行测试。

分析

这一个实验比较棘手,这里把所有相关文字都给出,帮助理解。省流:xv6的文件的磁盘块缓存系统,和先前的内存块一样,只使用了一把大锁,这样在多个进程同时使用文件系统时,会在bcache.lock上发生严重的锁竞争,无法同时申请或释放内存。

锁竞争优化的思路:

  1. 资源只在必要时共享
  2. 资源共享时,减小临界区大小

思路1对应前一个实验,bcache需要一直保持资源共享,无法拆解成cpu粒度,所以这里选择思路2,也就是减小锁的粒度,用更精细的锁降低出现竞争的次数。

struct {
  struct spinlock lock;
  struct buf buf[NBUF];

  // Linked list of all buffers, through prev/next.
  // Sorted by how recently the buffer was used.
  // head.next is most recent, head.prev is least.
  struct buf head;
} bcache;

xv6的缓存设计使用双向链表存储所有的块缓存,每次尝试获取一个blockno时会持有链表的锁,遍历链表,若块已在缓存中则直接返回,若不存在则选取一个最久未使用且引用计数为0的块作为缓存返回。显然,这里需要修改双向链表的结构。

新的改进方案使用一个blockno - buf 映射的哈希表,在每个哈希桶上单独加锁。这样在多个进程访问缓存时,只有在同时命中相同哈希桶时才会出现锁竞争。当桶中空闲的buf不足时,从其他桶中获取buf。

这个想法很eazy,但是具体实现时会出现很多问题,最主要的就是死锁,如果只是为了过关,这里的问题可能并不会在bcachetest中被暴露出来,但是如果放在一个长时间运行的系统中,肯定是会出错的,这里我们尝试解决。

新方案存在的问题

struct {
  struct buf buf[NBUF];
  struct spinlock eviction_lock;

  // Hash map: dev and blockno to buf
  struct buf bufmap[NBUFMAP_BUCKET];
  struct spinlock bufmap_locks[NBUFMAP_BUCKET];
} bcache;

新的方案在bcache中定义哈希表bufmap,并为每个桶分配一个bufmap_lock,在bget中,首先在blockno对应桶中扫描缓存是否存在,若不存在,则会在其他桶中寻找一个最近最久未使用的引用为0的buf块,进行缓存驱逐并移动到blockno对应的哈希桶中,作为blockno的缓存返回。那么bget大致的代码逻辑如下:

buf bget(dev, blockno) {
  key := hash(dev, blockno);
  // 获取 key 桶的锁
  acquire(bufmap_locks[key]);
  // 查找 blockno 的缓存是否存在,若是直接返回,若否继续执行
  if(b := look_for_blockno_in(bufmap[key])) {
    b->refcnt++
    release(bufmap_locks[key]);
    return b;
  }
  // 查找可驱逐缓存 b
  least_recently := NULL;
  for i := [0, NBUFMAP_BUCKET) { // 遍历所有的桶
    acquire(bufmap_locks[i]);    // 获取第 i 桶的锁
    b := look_for_least_recently_used_with_no_ref(bufmap[key]);
    // 如果找到未使用时间更长的空闲块
    if(b.last_use < least_recently.last_use) {  
      least_recently := b;
    }
    release(bufmap_locks[i]);   // 查找结束后,释放第 i 桶的锁
  }
  b := least_recently;
  // 驱逐 b 原本存储的缓存(将其从原来的桶删除)
  evict(b);
  // 将 b 加入到新的桶
  append(bucket[key], b);
  release(bufmap_locks[key]); // 释放 key 桶的锁
  // 设置 b 的各个属性
  setup(b);
  return b;
}

很明显,这里会遇到和前一个实验类似的问题-死锁,以及一个潜在的问题。

Q1:潜在的问题-待驱逐的buf在锁被释放后可能被引用

在进行缓存驱逐的时候,每扫描一个桶都会尝试获取该桶的锁,但是每扫描完一个桶后又释放了该桶的锁。在这个遍历查找的过程中,释放锁的那一瞬间,就不能保证那个桶是绝对空闲的了。因为在释放后到驱逐这一段操作之间,可能有另一个进程调用bget也获取了这个buf,使这个buf引用不为0,此时前一个驱逐操作就不安全了。

解决方法就是,在扫描时,在找到最近最久未使用的空闲buf后,不释放该桶的锁,继续持有其对应的桶的锁直到驱逐完成后再释放。

Q2:并发请求可能形成环路造成死锁

假设下面这种情况:

存在2个块b1 b2对应哈希值是1和2,这两个块都没有缓存
-----------------------------------
    CPU1             CPU2
-----------------------------------
  bget(dev,b1)    bget(dev,b2) 
     |                 |
     V                 V
  获取桶1的锁       获取桶2的锁
     |                 |
     V                 V
 缓存不存在,遍历    缓存不存在,遍历
     |                 |
     V                 V
   .....           发现桶1空闲
     |            尝试获取1的锁
     V                 |
 发现桶2空闲             V
尝试获取2的锁           .....
     |                 |
     V                 V
等待2的锁释放       等待1的锁释放
-----------------------------------
此时陷入死锁!

此时CPU1持有锁1尝试获取锁2,CPU2持有锁2尝试获取锁1,形成了环路等待。

死锁的四个条件:

  1. 互斥 - 一个资源在任何时候只属于一个线程
  2. 请求保持 - 线程在拿着一个锁的情况下尝试获取另一个锁
  3. 不剥夺 - 不存在系统外力强制剥夺一个线程已经拥有的资源
  4. 环路等待 - 请求资源的顺序形成一个环

我们需要破坏其中一个条件来解决死锁的困境,接下来依次分析

  • 互斥?在这里互斥是必须的,无法改变
  • 请求保持?
  • 不剥夺?这里如果强制剥夺一方释放锁,那么它的bget操作会失败,导致文件系统调用的路径全部失效,不可行
  • 环路等待?改变访问顺序,使得无论怎么访问都不会出现环,例如强制必须按hash值从小到大获取锁,显然不可行,假设blackno第二个桶,唯一空闲的是第一个桶的buf,那么它将永远获取不到实际可以用的资源,这里造成了浪费。

那么这里就从请求保持的角度来解决。

我们在拿到一个锁之后去申请另一个锁的目的,是为了保证我们的blockno确确实实是cache miss状态。我们破坏请求保持,在获取空闲buf锁前,先释放先前持有的key的桶的锁,在找到并驱逐最近最久未使用的空闲块buf后,再重新获取key的桶锁,并将buf加入桶。

现在,所有调用bget的线程永远都只能持有一个桶锁,那么就不会出现请求保持,从而引起环路了。死锁问题解决,但是引入了新的问题。

Q3:对blockno的重复缓存

由于我们在搜索可驱逐buf前,为了破坏请求保持,释放了key的桶锁(key为请求的blockno对应hash值),知道遍历完成并驱逐空闲buf后才重新获取key桶锁。这里存在一个问题,在释放key桶锁后,我们第一个临界区的保障"查找blockno的缓存是否存在,不存在就继续执行"失效了,这意味着在之后再次获取key桶锁前,另一个CPU完全有可能访问同一个blockno获取key桶锁,也通过了缓存不存在的测试,最后同样进入驱逐+分配阶段,导致一个block在同一个hash桶中出现多份缓存。

那么如何保障同一个block不会出现多份缓存呢?

这个问题有些棘手,目前的限制条件有:

  • 在遍历查找可驱逐空闲buf的过程中,不能持有key的桶锁
  • 在遍历查找可驱逐空闲buf的过程中,不持有key的桶锁,可能会有另一CPU同时发现该block没有缓存,进入查找阶段,最后导致其出现多份缓存

这里的解决方案牺牲了些效率,但是能保证在极端情况下的并发安全:

添加eviction_lock,将驱逐+分配的过程原子化。

先释放桶锁,再获取eviction_lock(这里顺序必须固定,以防止新的死锁)。在获取evction_lock后,马上判断blockno的缓存是否存在,若是直接返回,否则继续执行。

在这种情况下,假设仍然出现先前的情况:有多个线程同时请求同一个blockno,且所有线程都在第一步判断是否已存在缓存中,发现未缓存,都尝试进入evction_lock区域,此时该区域形成一个临界区,每次只有一个线程执行驱逐+分配的过程,其他线程直接返回分配后的缓存。

坏处显而易见,引入了全局的eviction_lock使得驱逐+分配的过程失去了并行性,且每一次cache miss都会多一次额外的遍历开销。由于cache miss本身属于稀有事件,对于miss的块,后续需要从磁盘读取数据,读入的耗时比一个遍历要多好几个数量级,这里的trade off我觉得是可以接受的。

这里的设计属于“乐观锁”,即在冲突发生概率较小的关键区内,不使用独占的互斥锁,而是在提交操作前,检查一下操作数据是否被其他线程修改,如果是,说明冲突发生需要特殊处理。相比较悲观锁,乐观锁可以在冲突概率较低的场景下,降低锁开销以及不必要的线性化。提升并行性。有时候还能避免死锁。

代码变更较长,可参考我的git仓库:github.com/cardchoosen…

编译运行bcachetest

$ bcachetest
start test0
test0 results:
--- lock kmem/bcache stats
lock: kmem_cpu_0: #test-and-set 0 #acquire() 32933
lock: kmem_cpu_1: #test-and-set 0 #acquire() 83
lock: kmem_cpu_2: #test-and-set 0 #acquire() 74
lock: bcache_bufmap: #test-and-set 0 #acquire() 6405
lock: bcache_bufmap: #test-and-set 0 #acquire() 6763
lock: bcache_bufmap: #test-and-set 0 #acquire() 8471
lock: bcache_bufmap: #test-and-set 0 #acquire() 6269
lock: bcache_bufmap: #test-and-set 0 #acquire() 6267
lock: bcache_bufmap: #test-and-set 0 #acquire() 4204
lock: bcache_bufmap: #test-and-set 0 #acquire() 4204
lock: bcache_bufmap: #test-and-set 0 #acquire() 2204
lock: bcache_bufmap: #test-and-set 0 #acquire() 4206
lock: bcache_bufmap: #test-and-set 0 #acquire() 2200
lock: bcache_bufmap: #test-and-set 0 #acquire() 4355
lock: bcache_bufmap: #test-and-set 0 #acquire() 4399
lock: bcache_bufmap: #test-and-set 0 #acquire() 6416
lock: bcache_eviction: #test-and-set 0 #acquire() 84
--- top 5 contended locks:
lock: virtio_disk: #test-and-set 1259278 #acquire() 1137
lock: proc: #test-and-set 169114 #acquire() 452645
lock: proc: #test-and-set 116206 #acquire() 434574
lock: wait_lock: #test-and-set 89364 #acquire() 19
lock: proc: #test-and-set 79832 #acquire() 455092
tot= 0
test0: OK
start test1
test1 OK

实验完成,make grade验证(验证过程可能会有些长,耐心):

== Test running kalloctest == 
$ make qemu-gdb
(68.8s) 
== Test   kalloctest: test1 == 
  kalloctest: test1: OK 
== Test   kalloctest: test2 == 
  kalloctest: test2: OK 
== Test kalloctest: sbrkmuch == 
$ make qemu-gdb
kalloctest: sbrkmuch: OK (12.7s) 
== Test running bcachetest == 
$ make qemu-gdb
(13.8s) 
== Test   bcachetest: test0 == 
  bcachetest: test0: OK 
== Test   bcachetest: test1 == 
  bcachetest: test1: OK 
== Test usertests == 
$ make qemu-gdb
usertests: OK (156.8s) 
== Test time == 
time: OK 
Score: 70/70