Lab7:Locks

本 lab 的任务是优化 xv6 锁,以减少锁竞争。
我们将会重新设计代码以降低锁竞争,提高多核机器上系统的并行性,需要修改数据结构和锁的策略。

详细要求及提示见链接
https://pdos.csail.mit.edu/6.1810/2021/labs/lock.html

参考文章:
MIT6.S081 2021 locks
[mit6.s081] 笔记 Lab8: Locks | 锁优化

阅读指路:
• xv6book Chapter 6: “Locking” and the corresponding code.
• xv6book Section 3.5: “Code: Physical memory allocator”
• xv6book Section 8.1 through 8.3: “Overview”, “Buffer cache layer”, and “Code: Buffer cache”
kernel/kalloc.c
kernel/bio.c

Memory allocator (moderate)

在未修改前,所有内存块由一个锁管理,若有多个进程并发地获取内存,则会造成非常多的锁等待。 用户程序 /kalloctest 给xv6的内存分配器加压:三个进程增加和缩小它们的地址空间,导致对kalloc和kfree的多次调用,形成锁的竞争。

$ 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

目标:

kalloctest中锁争用的根本原因是kalloc()只有一个空闲列表,由一个锁保护
要消除锁争用,必须重新设计内存分配器,以避免单一的锁和列表。
基本思想是为每个CPU维护一个空闲列表,每个列表都有自己的锁。不同CPU上的分配和释放可以并行运行,因为每个CPU将在不同的列表上运行。
主要的挑战将是处理这样的情况:一个CPU的空闲列表是空的,但另一个CPU的列表有空闲内存;在这种情况下,一个CPU必须“窃取”另一个CPU的空闲列表的一部分。这种行为可能会引发锁竞争,但希望这种情况不会经常发生。

Hints

  • 可以使用常量 NCPUkernel/param.h
  • freerange() 函数将所有空闲资源分配给正在运行freerange的CPU
  • cpuid 函数返回当前的CPU编号,但是在调用前必须关闭中断,用函数 push_off()pop_off() 实现中断的开关
  • PS:snprintf() 函数 (kernel/sprintf.c)可以给出锁的统一格式的命名

主要工作:

在 kernel/kalloc.c 中实现:

struct {struct spinlock lock;struct run *freelist;
} kmem[NCPU]; // 【每个CPU分配一个kmem锁】#define MAX_NUM_PAGES 100  // 【最大“偷窃”页数设为100】

在knit中对每个锁初始化:

void
kinit()
{char name[10];for (int i = 0; i < NCPU; i++) {snprintf(name, 10, "kmem-%d", i);initlock(&kmem[i].lock, name);}freerange(end, (void*)PHYSTOP);
}// Let freerange give all free memory to the CPU running freerange.【无需修改】
void
freerange(void *pa_start, void *pa_end)
{char *p;p = (char*)PGROUNDUP((uint64)pa_start);for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)kfree(p);
}
// Free the page of physical memory pointed at by v,
// which normally should have been returned by a
// call to kalloc().  (The exception is when
// initializing the allocator; see kinit above.)
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;/*My Code:*/push_off();int cpu = cpuid();pop_off();// 空闲页加入对应CPU的空闲页链表acquire(&kmem[cpu].lock);r->next = kmem[cpu].freelist;kmem[cpu].freelist = r; // 维护链表(从头部插入)release(&kmem[cpu].lock);
}

kalloc函数(分配一个物理页),对每个CPU的空闲内存单独管理,没有空闲内存时到其他CPU“偷窃”

// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
void *
kalloc(void)
{struct run *r;// 原代码:// acquire(&kmem.lock);// r = kmem.freelist;// if(r)//   kmem.freelist = r->next;// release(&kmem.lock);push_off();int cpu = cpuid();pop_off();acquire(&kmem[cpu].lock);r = kmem[cpu].freelist;if(r)kmem[cpu].freelist = r->next;release(&kmem[cpu].lock);if(r == 0)r = get_others_pages(cpu);  // 【如果当前CPU的空闲页链表为空,到其他CPU的空闲页中取】if(r)memset((char*)r, 5, PGSIZE); // fill with junkreturn (void*)r;
}

偷窃其他CPU的空闲内存:

void * get_others_pages(int cpu){int count = 0;struct run *start = 0;struct run *end = 0;for(int i = 0; i < NCPU; i++){if(i == cpu)continue;acquire(&kmem[i].lock);start = kmem[i].freelist;end = kmem[i].freelist;if(!start){  // 这个CPU也没有空闲内存release(&kmem[i].lock);continue;}// 链表向后,直到结束或者达到100页while(end && count < MAX_NUM_PAGES){end = end->next;count++;}if(end){ kmem[i].freelist = end->next; // 后面还有空闲内存,freelist接在后面end->next = 0;}else kmem[i].freelist = 0;release(&kmem[i].lock);acquire(&kmem[cpu].lock);kmem[cpu].freelist = start->next;  // cpu的空闲内存freelist从start开始release(&kmem[cpu].lock);break;}return (void*) start;
}

Buffer cache (hard)

多个进程同时使用文件系统的时候,bcache.lock 上会发生严重的锁竞争。bcache.lock 锁用于保护磁盘区块缓存,在原本的设计中,由于该锁的存在,多个进程不能同时操作(申请、释放)磁盘缓存。

你的目标是,修改cache管理策略,降低锁冲突。

We suggest you look up block numbers in the cache with a hash table that has a lock per hash bucket.

  • 可以用固定数量的buckets,并且哈希表不需要动态变化,用一个质数(例如13)可以减少hash值的冲突

  • 在哈希表中查找一个buffer,以及为新的buffer分配一个entry的操作必须是原子的

  • 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
    .

  • bget() 函数中serialize eviction 连续驱逐是可行的 (i.e., the part of bget that
    selects a buffer to re-use when a lookup misses in the cache).

  • 有时候需要同时持有两个锁,例如在eviction过程中需要持有bcache lock和一个bucket lock,这种情况下注意避免死锁

  • 当转移一个block(将一个buffer从一个bucket转移到另一个bucket时),需要注意处理block仍然hash到相同的bucket的情况,需要避免死锁

  • xv6 book Chaper8 部分内容:

Buffer cache缓存磁盘块,并同步访问它们,确保一个块只能同时被内核中的一个进程访问。

8.2 Buffer cache layer
buffer缓存有两项工作:
(1) 同步访问磁盘块,以确保磁盘块在内存中只有一个buffer缓存,并且一次只有一个内核线程能使用该buffer缓存;
(2) 缓存使用较多的块,这样它们就不需要从慢速磁盘中重新读取(代码见bio.c)

buffer缓存的主要接口包括bread和bwrite,bread返回一个在内存中可以读取和修改的块副本buf,bwrite将修改后的buffer写到磁盘上相应的块。
内核线程在使用完一个buffer后,必须通过调用brelse释放它。
buffer缓存为每个buffer的都设有sleeplock(在struct buf内定义),以确保每次只有一个线程使用buffer(从而使用相应的磁盘块);bread 返回的buffer会被锁定,而brelse释放锁。

buffer缓存有固定数量的buffer来存放磁盘块,这意味着如果文件系统需要一个尚未被缓存的块,buffer缓存必须回收一个当前存放其他块的buffer。buffer缓存为新块寻找最近使用最少的buffer【LRU替换策略】,因为最近使用最少的buffer是最不可能被再次使用的buffer。

主要工作:

  • kernel/buf.h 中完成:
struct buf {int valid;   // has data been read from disk?int disk;    // does disk "own" buf?uint dev;uint blockno;struct sleeplock lock;uint refcnt;struct buf *prev; // LRU cache liststruct buf *next;uchar data[BSIZE];uint lastuse_time;  // 【上次使用时间的时间戳】int owner;   // 【当前归属的bucket】
};
  • kernel/bio.c 中完成:

首先定义NBUCKET表示管理的bucket个数,并且定义hash映射(磁盘块号blockno到bucket号)

#define NBUCKET 13
#define BUCKET_HASH(dev, blockno) (blockno % NBUCKET)

bcache结构体中加入bucket(buffer cache的管理单位,将一堆buf数组组织在一起成为bucket)
以及 bucket_lock 作为每个bucket的互斥锁

struct {struct buf buf[NBUF];struct spinlock lock;struct buf bucket[NBUCKET];struct spinlock bucket_locks[NBUCKET];
} bcache;

binit 函数:初始化所有 bucket 以及 buffer

void
binit(void)
{initlock(&bcache.lock, "bcache_lock");// 【初始化每个bucket】char name[20];for(int i = 0; i < NBUCKET; i++) {snprintf(name, 20, "bucket_lock_%d", i);initlock(&bcache.bucket_locks[i], name);bcache.bucket[i].next = 0;}
//【初始化每个buffer】for(int i = 0; i < NBUF; i++){struct buf *b = &bcache.buf[i];initsleeplock(&b->lock, "buffer");b->lastuse_time = 0;b->refcnt = 0;// 【初始:将所有buffer装到bucket[0]】b->next = bcache.bucket[0].next;bcache.bucket[0].next = b;b->owner = 0;}
}

bget 函数 (给定磁盘块,在buffer cache上分配一个buffer进行缓存)

static struct buf*
bget(uint dev, uint blockno)
{uint index = BUCKET_HASH(dev, blockno);   // 【找到归属于哪个bucket管理】acquire(&bcache.bucket_locks[index]); // 获取对应bucket的锁// Is the block already cached?// 【在对应的bucket里面找,是否已经缓存了这个块】struct buf *b = bcache.bucket[index].next;while(b){if(b->dev == dev && b->blockno == blockno){b->refcnt++;release(&bcache.bucket_locks[index]);acquiresleep(&b->lock);return b;}b = b->next;}// Not cached.没有被缓存// 我们需要从所有buckets中, 用最近最久未使用策略(LRU), 寻找合适的buffer来缓存这个块,// 这表明我们需要获取它们对应的bucket_lock.// 【占有一个bucket_lock时, 尝试再获取其他bucket锁是很不安全的, 很容易循环等待造成死锁, // 所以我们首先释放当前获取的bucket锁】release(&bcache.bucket_locks[index]);// 【获取大锁lock】acquire(&bcache.lock);// 【获取大锁后, 再检查一遍是否已缓存(可能有其他进程所做的操作)// 防止出现同一个块缓存在多个buffer的问题】b = bcache.bucket[index].next;while(b){if(b->dev == dev && b->blockno == blockno){acquire(&bcache.bucket_locks[index]);b->refcnt++;release(&bcache.bucket_locks[index]);release(&bcache.lock);acquiresleep(&b->lock);return b;}b = b->next;}// Not Cached.仍然没有被缓存// 【此时需要采用LRU机制选择对应的buffer】:struct buf *before_ans = 0;uint min_lastuse_time = 0xffffffff;uint owner = -1;for(int i = 0; i < NBUCKET; i++){acquire(&bcache.bucket_locks[i]); // 获取bucket_lock[i]int flag = 0; // 标记b = &bcache.bucket[i];while(b->next) {if(b->next->refcnt == 0 && before_ans == 0){  //【还没找到一个buffer(before_ans==0), 此时碰到一个空闲buffer, 暂时作为before_ans】before_ans = b;min_lastuse_time = b->next->lastuse_time;flag = 1;}else if(b->next->refcnt == 0 && b->next->lastuse_time < min_lastuse_time) {//【已经找到buffer, 但是碰到空闲buffer, 满足lastuse_time更小, 应该用这个】before_ans = b;min_lastuse_time = b->next->lastuse_time;flag = 1;}b = b->next;}if(flag) {  // 【bucket[i]找到新的buffer】if(owner != -1)   // 有暂时的owner release(&bcache.bucket_locks[owner]);  // 【释放之前的owner bucket的锁】//【要注意一直持有的是目前最新的owner的锁!!!】owner = i;    // 设置新的owner} else {  // 【bucket[i]没有找到新的buffer: 释放buck_lock[i] (记得在循环一开始先获得了锁) 】release(&bcache.bucket_locks[i]);}}if(before_ans == 0) { // 最终都没有找到一个bufferpanic("bget: No buffer.");}struct buf *ans = before_ans->next;if(owner != index) { // 【找到的buffer不在当前的bucket[index]】// buffer从原来的bucket[owner]移除:before_ans->next = ans->next;release(&bcache.bucket_locks[owner]);// buffer添加到bucket[index]:acquire(&bcache.bucket_locks[index]);ans->next = bcache.bucket[index].next;bcache.bucket[index].next = ans;}// buffer重置:ans->dev = dev;ans->blockno = blockno;ans->refcnt = 1;ans->valid = 0;ans->owner = index;release(&bcache.bucket_locks[index]);  // 释放bucket[index]锁//【无论owner等不等于index,执行到此处都只持有bucket[index]的锁】release(&bcache.lock);   // 释放大锁acquiresleep(&ans->lock);return ans;
}

brelse 函数:

// Release a locked buffer.
void
brelse(struct buf *b)
{// 【必须持有大锁lock】if(!holdingsleep(&b->lock))panic("brelse");
// 唤醒 buffer lockreleasesleep(&b->lock);uint index = BUCKET_HASH(b->dev, b->blockno); // 找到block归属于哪个bucketacquire(&bcache.bucket_locks[index]);b->refcnt--;if (b->refcnt == 0) { //没有进程引用这块buffer, 则为空闲状态, lastuse_time设为当前时钟b->lastuse_time = ticks;}release(&bcache.bucket_locks[index]);
}

最后加入两个维护block的refcnt的函数:

void
bpin(struct buf *b) {uint index = BUCKET_HASH(b->dev, b->blockno);acquire(&bcache.bucket_locks[index]);b->refcnt++;release(&bcache.bucket_locks[index]);
}void
bunpin(struct buf *b) {uint index = BUCKET_HASH(b->dev, b->blockno);acquire(&bcache.bucket_locks[index]);b->refcnt--;release(&bcache.bucket_locks[index]);
}

XV6 Lab7:Locks相关推荐

  1. XV6 lab3:Trap

    Lab3:Traps 参考文章: 6.S081 & 操作系统内核 操作系统MIT6.S081:Lab4->Trap Xv6操作系统的系统调用通过 trampoline 来实现用户空间和内 ...

  2. XV6 Lab2:Page Tables

    Lab2:Page Tables 本 lab 的任务是理解 xv6 页表的实现. 参考文章: xv6实验课程–页表(2021) 6.S081-2021FALL-Lab3:pgtbl MIT6.S081 ...

  3. [emerg] 30766#0: unknown directive rtmp in /usr/local/etc/nginx/nginx.conf:16

    https://www.jianshu.com/p/99fd7d447f41 查看全文 http://www.taodudu.cc/news/show-6390979.html 相关文章: VS调试出 ...

  4. 常用jdk的命令行工具:_jhsdb:JDK 9的新工具

    常用jdk的命令行工具: 我喜欢在分析性能和基于Java的应用程序的其他问题的早期步骤中使用JDK随附的命令行工具,并在诸如jcmd , jps , jstat , jinfo , jhat和jmap ...

  5. jhsdb:JDK 9的新工具

    我喜欢在分析性能和其他基于Java的应用程序的其他问题的早期步骤中使用JDK随附的命令行工具,并在诸如jcmd , jps , jstat , jinfo , jhat和jmap , jrunscri ...

  6. Draconian,自由或保姆状态:Java,C#,C,C ++,Go和Rust中的并发意识形态

    为什么我们需要并发 (Why we need Concurrency) Once, there was a good old time when clock speed doubled every 1 ...

  7. MIT操作系统实验lab1(pingpong案例:附代码、详解)

    1.题目描述:在xv6上实现pingpong程序,即两个进程在管道两侧来回通信.父进程将"ping"写入管道,子进程从管道将其读出并打印<pid>:received p ...

  8. 第15章:存储引擎和表类型#innodb的性能调整

    原贴:http://dev.mysql.com/doc/refman/5.1/zh/storage-engines.html#innodb-tuning 第15章:存储引擎和表类型 目录 15.1. ...

  9. JVM系列之:你知道Jhsdb整合的故障处理工具

    本文为<深入学习 JVM 系列>第二十一篇文章 Jhsdb 是 JDK9 引入的新的命令行工具,它有 clhsdb.debugd.hsdb.jstack.jmap.jinfo.jsnap ...

最新文章

  1. 从面试官角度观察到的程序员工资瓶颈,同时给出突破瓶颈的建议
  2. [Swift]LeetCode901. 股票价格跨度 | Online Stock Span
  3. 【git】git入门之把自己的项目上传到github
  4. 《Node.js区块链开发》一3.5 亿书对DPoS机制的改进
  5. WCF - 基础介绍
  6. PAT Basic 1048
  7. SQLite中的高级SQL
  8. linux内核合并dtb文件,c – 如何修改内核DTB文件
  9. linux的shell脚本语法与C语言不同之一
  10. 一文捋清项目里的各种配置,看了必懂!
  11. curl命令详解_命令行学习(一)基础命令
  12. (二)开源IT资产管理系统--OCS(win)客户端代理安装
  13. DO447诠释红帽Ansible Tower架构
  14. U盘文件夹被隐藏能够解决方法
  15. CAD中属性编辑操作——对象属性
  16. 怎么用wps抽签_“excle 如何摇号“如何参与摇号?
  17. leetcode-017-297. 二叉树的序列化与反序列化
  18. Line软件zhuce分析
  19. 大脑是如何编码外界各种信息的?
  20. 牢骚:各种奇奇怪怪的问题。。。

热门文章

  1. 嵌入式课程设计linux,嵌入式系统课程设计--基于U盘的linux操作系统的构建
  2. 信息论与编码:C语言实现lz78算法
  3. 基于Keras实现鸢尾花分类
  4. 现实版终结者?杀手机器人预计2017年问世
  5. 假如我国国民生产总值的年增长率为7%, 计算10年后我国国民生产总值与现在相比增长多少百分比
  6. 分布式系统CAP理论深入探索和分析
  7. 可以投放广告的博客-大海软件(含Google广告)
  8. 经济法基础——第一章第一节、法律基础
  9. USACO 2020 December Contest, BronzeProblem 2. Daisy Chains题解
  10. ArcGIS API for Silverlight部署本地地图服务