mit6s081 lab8 locks

mit6s081 lab8 locks

一、Memory Allocator(内存分配器)

📘 题目描述

为每个 CPU 实现独立的空闲列表,当 CPU 的空闲列表为空时,可以从其他 CPU 的空闲列表窃取空闲内存

所有的锁都以 "kmem" 开头。你应该为每个锁调用 initlock() 并传入一个以 "kmem" 开头的名称。

通过运行以下命令进行验证:

  • kalloctest:查看锁争用是否显著减少;
  • usertests sbrkmuch:验证是否仍然可以分配所有内存。

输出结果中,尽管具体数值可能不同,但 kmem 锁的总争用次数应显著减少


💡 实现思路

  • 所有空闲内存最初都分配给 CPU0;
  • 当 CPU1 需要内存时,可以窃取 CPU0 的空闲块;
  • 使用完成后,释放的内存会挂回到 CPU1 的空闲列表;
  • 这样 CPU1 下次再分配时就可以直接从自己的空闲列表中获取。

🧱 数据结构定义

为每个 CPU 分配一个独立的空闲链表及其锁:

复制代码
struct {
  struct spinlock lock;
  struct run *freelist;
} kmem[NCPU];

🔧 修改 kinit

只有一个 CPU(通常是 CPU0)会调用该函数。

该函数负责初始化每个 CPU 的锁,并调用 freerange() 将物理内存放入空闲链表中。

复制代码
void
kinit()
{
  char lockname[NCPU];
  for (int i = 0; i < NCPU; ++i) {
    snprintf(lockname, sizeof(lockname), "kmem_%d", i); // 为每个 CPU 的锁命名
    initlock(&kmem[i].lock, lockname); // 初始化锁
  }
  freerange(end, (void*)PHYSTOP); // 将用户空间所有内存加入空闲链表
}

🔩 修改 kfree

获取 CPU ID 时必须关闭中断,保证获取的 ID 正确。

复制代码
void
kfree(void *pa)
{
  struct run *r;

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

  // 用垃圾数据填充,检测悬空引用
  memset(pa, 1, PGSIZE);
  r = (struct run*)pa;

  // 关中断
  push_off();
  int id = cpuid(); // 当前 CPU id
  acquire(&kmem[id].lock);
  r->next = kmem[id].freelist;
  kmem[id].freelist = r;
  release(&kmem[id].lock);
  pop_off(); // 开中断
}

⚙️ 修改 kalloc

当当前 CPU 的空闲列表为空时,尝试从其他 CPU 的空闲列表中窃取内存块

复制代码
void *
kalloc(void)
{
  struct run *r;

  push_off();
  int id = cpuid();
  acquire(&kmem[id].lock);
  r = kmem[id].freelist;

  if(r)
    kmem[id].freelist = r->next;
  else {
    int antid; // 其他 CPU id
    for (antid = 0; antid < NCPU; ++antid) {
      if (antid == id) continue;
      acquire(&kmem[antid].lock);
      r = kmem[antid].freelist;
      if (r) {
        kmem[antid].freelist = r->next;
        release(&kmem[antid].lock);
        break;
      }
      release(&kmem[antid].lock);
    }
  }

  release(&kmem[id].lock);
  pop_off();

  if (r)
    memset((char*)r, 5, PGSIZE); // 填充垃圾数据

  return (void*)r;
}

✅ 测试截图


二、Buffer Cache(缓冲区缓存)

📘 题目描述

修改块缓存,使得运行 bcachetest 时,bcache 中所有锁的 acquire 循环迭代次数接近于 0。

理想情况是所有锁的总争用计数为 0(小于 500 也可接受)。

修改 bget()brelse(),让多个进程能够并发查找和释放缓存块,减少锁竞争。

同时必须保持:

每个磁盘块最多只能缓存一个副本。


💡 优化思路

  • 尽可能减少共享:能独享就独享,比如每 CPU 独立空闲链表。
  • 必须共享时:减少临界区的锁粒度,缩短加锁时间。

⚙️ xv6 原设计

  • 使用双向链表存储所有缓存块;
  • 查找时需遍历整个链表;
  • 若缓存中已有对应 block,则直接返回;
  • 否则选择最近最久未使用且 refcnt == 0 的 buf 进行替换;
  • 整个过程都要持有 bcache.lock,导致严重锁竞争。

🚀 新的改进方案

  • 建立 哈希表(blockno → buf)
  • 每个桶(bucket)配一把独立的自旋锁;
  • 查找 / 插入时仅锁定对应桶;
  • 仅当桶中无空闲 buf 时,再从其他桶"偷取";
  • 大幅减少全局锁竞争。

🧱 结构体修改

复制代码
struct buf {
  int valid;      // 是否从磁盘读取过数据
  int disk;       // 是否为磁盘所有
  uint dev;
  uint blockno;
  struct sleeplock lock;
  uint refcnt;
  uint lastuse;   // *新增*:记录最近一次使用时间(用于 LRU)
  struct buf *next;
  uchar data[BSIZE];
};

🔧 全局结构定义与初始化

复制代码
#define NBUFMAP_BUCKET 13
#define BUFMAP_HASH(dev, blockno) ((((dev)<<27)|(blockno))%NBUFMAP_BUCKET)

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

  // 哈希表:从 dev+blockno 到 buf 的映射
  struct buf bufmap[NBUFMAP_BUCKET];
  struct spinlock bufmap_locks[NBUFMAP_BUCKET];
} bcache;

void
binit(void)
{
  // 初始化哈希桶
  for(int i = 0; i < NBUFMAP_BUCKET; i++) {
    initlock(&bcache.bufmap_locks[i], "bcache_bufmap");
    bcache.bufmap[i].next = 0;
  }

  // 初始化缓存块
  for(int i = 0; i < NBUF; i++){
    struct buf *b = &bcache.buf[i];
    initsleeplock(&b->lock, "buffer");
    b->lastuse = 0;
    b->refcnt = 0;
    b->next = bcache.bufmap[0].next;
    bcache.bufmap[0].next = b;
  }

  initlock(&bcache.eviction_lock, "bcache_eviction");
}

🔍 修改 bget

复制代码
static struct buf*
bget(uint dev, uint blockno)
{
  struct buf *b;
  uint key = BUFMAP_HASH(dev, blockno);

  acquire(&bcache.bufmap_locks[key]);

  // 查找是否已缓存
  for (b = bcache.bufmap[key].next; b; b = b->next) {
    if (b->dev == dev && b->blockno == blockno) {
      b->refcnt++;
      release(&bcache.bufmap_locks[key]);
      acquiresleep(&b->lock);
      return b;
    }
  }

  // 若未命中缓存
  release(&bcache.bufmap_locks[key]);
  acquire(&bcache.eviction_lock);

  // 再次确认是否已有缓存(防止重复创建)
  for (b = bcache.bufmap[key].next; b; b = b->next) {
    if (b->dev == dev && b->blockno == blockno) {
      acquire(&bcache.bufmap_locks[key]);
      b->refcnt++;
      release(&bcache.bufmap_locks[key]);
      release(&bcache.eviction_lock);
      acquiresleep(&b->lock);
      return b;
    }
  }

  // 选择 LRU 块进行替换
  struct buf *before_least = 0;
  uint holding_bucket = -1;

  for (int i = 0; i < NBUFMAP_BUCKET; i++) {
    acquire(&bcache.bufmap_locks[i]);
    int newfound = 0;
    for (b = &bcache.bufmap[i]; b->next; b = b->next) {
      if (b->next->refcnt == 0 && (!before_least || b->next->lastuse < before_least->next->lastuse)) {
        before_least = b;
        newfound = 1;
      }
    }
    if (!newfound)
      release(&bcache.bufmap_locks[i]);
    else {
      if (holding_bucket != -1)
        release(&bcache.bufmap_locks[holding_bucket]);
      holding_bucket = i;
    }
  }

  if (!before_least)
    panic("bget: no buffers");

  b = before_least->next;

  // 若需要移动到目标桶
  if (holding_bucket != key) {
    before_least->next = b->next;
    release(&bcache.bufmap_locks[holding_bucket]);
    acquire(&bcache.bufmap_locks[key]);
    b->next = bcache.bufmap[key].next;
    bcache.bufmap[key].next = b;
  }

  b->dev = dev;
  b->blockno = blockno;
  b->refcnt = 1;
  b->valid = 0;

  release(&bcache.bufmap_locks[key]);
  release(&bcache.eviction_lock);
  acquiresleep(&b->lock);
  return b;
}

🔁 修改 brelsebpinbunpin

复制代码
void
brelse(struct buf *b)
{
  if (!holdingsleep(&b->lock))
    panic("brelse");

  releasesleep(&b->lock);

  uint key = BUFMAP_HASH(b->dev, b->blockno);

  acquire(&bcache.bufmap_locks[key]);
  b->refcnt--;
  if (b->refcnt == 0)
    b->lastuse = ticks;
  release(&bcache.bufmap_locks[key]);
}

void
bpin(struct buf *b) {
  uint key = BUFMAP_HASH(b->dev, b->blockno);
  acquire(&bcache.bufmap_locks[key]);
  b->refcnt++;
  release(&bcache.bufmap_locks[key]);
}

void
bunpin(struct buf *b) {
  uint key = BUFMAP_HASH(b->dev, b->blockno);
  acquire(&bcache.bufmap_locks[key]);
  b->refcnt--;
  release(&bcache.bufmap_locks[key]);
}

🧩 总结

对象 是否在内存中 是否可每 CPU 独立 是否需要全局同步 优化方式
空闲内存(kalloc) ✅ 是 ✅ 可以 每 CPU 独立空闲链表
磁盘缓存(bcache) ✅ 是 ❌ 不行 ✅ 必须 哈希分桶 + 每桶独立锁

结论:

  • 虽然磁盘缓存也使用内存,但它存储的是全局共享的文件系统块数据
  • 因此必须保证数据一致性,不能按 CPU 独立分配;
  • 为提高并发性能,应将全局锁细化为哈希桶锁;
    | ------------ | ----------------- | ---------------- | --------------------- |
    | 空闲内存(kalloc) | ✅ 是 | ✅ 可以 | 否 | 每 CPU 独立空闲链表 |
    | 磁盘缓存(bcache) | ✅ 是 | ❌ 不行 | ✅ 必须 | 哈希分桶 + 每桶独立锁 |

结论:

  • 虽然磁盘缓存也使用内存,但它存储的是全局共享的文件系统块数据
  • 因此必须保证数据一致性,不能按 CPU 独立分配;
  • 为提高并发性能,应将全局锁细化为哈希桶锁;
  • 从而在全局共享一致性的前提下实现高并行度的访问。
相关推荐
CAU界编程小白11 小时前
数据结构系列之堆
数据结构·c
fakerth14 小时前
【OpenHarmony】分布式文件服务模块架构
分布式·架构·操作系统·openharmony
kyle~16 小时前
计算机系统---CPU的进程与线程处理
linux·服务器·c语言·c++·操作系统·计算机系统
fakerth1 天前
【OpenHarmony】应用文件服务模块架构
架构·操作系统·openharmony
轻口味2 天前
Rokid Glasses 移动端控制应用开发初体验-助力业务创新
android·操作系统·app
海棠蚀omo2 天前
Linux操作系统-进程(三)
linux·操作系统
hour_go2 天前
页表 vs. 组相联缓存:内存管理与性能优化的殊途同归
笔记·操作系统·分页·计算机体系结构·tlb·组相联缓存
靠近彗星2 天前
1.5操作系统引导
java·linux·服务器·操作系统
空荡forevere3 天前
《操作系统真象还原》 第十章 输入输出系统
开发语言·c++·操作系统