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 idacquire(&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 idfor (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;
}
🔁 修改 brelse
、bpin
、bunpin
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 独立分配;
- 为提高并发性能,应将全局锁细化为哈希桶锁;
- 从而在全局共享一致性的前提下实现高并行度的访问。