代码在github

这一次实验是要对XV6内部的锁进行优化,减少锁争用,提高系统的性能。

Memory allocator (moderate)

第一个实验是对XV6内核的内存页面分配器进行改进,改进的策略在前面的章节中也讲过了。XV6原本是使用一个空闲页面链表,但是这样就会导致不同CPU上的kallockfree会产生锁争用,内存页面的分配被完全串行化了,降低了系统的性能。

而一个改进策略就是为每个CPU核心分配一个空闲链表,kallockfree都在本核心的链表上进行,只有当当前核心的链表为空时才去访问其他核心的链表。通过这种策略就可以减少锁的争用,只有当某核心的链表为空时才会发生锁争用。

首先定义NCPU个kmem结构体,并在kinit函数中对锁进行初始化。

struct {
struct spinlock lock;
struct run *freelist;
char lock_name[7];
} kmem[NCPU]; void
kinit()
{
for (int i = 0; i < NCPU; i++) {
snprintf(kmem[i].lock_name, sizeof(kmem[i].lock_name), "kmem_%d", i);
initlock(&kmem[i].lock, kmem[i].lock_name);
}
freerange(end, (void*)PHYSTOP);
}

对于kfree函数只需要将释放的页面插入到当前核心对应链表上就行了

void
kfree(void *pa)
{
...
r = (struct run*)pa; push_off();
int id = cpuid(); acquire(&kmem[id].lock);
r->next = kmem[id].freelist;
kmem[id].freelist = r;
release(&kmem[id].lock); pop_off();
}

对于kalloc函数,当在当前核心上申请失败时,就尝试从其他核心上获取页面。使用快慢指针来找到链表的中点,之后将一半的页面移动到当前核心的链表上。

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 {
// alloc failed, try to steal from other cpu
int success = 0;
int i = 0;
for(i = 0; i < NCPU; i++) {
if (i == id) continue;
acquire(&kmem[i].lock);
struct run *p = kmem[i].freelist;
if(p) {
// steal half of memory
struct run *fp = p; // faster pointer
struct run *pre = p;
while (fp && fp->next) {
fp = fp->next->next;
pre = p;
p = p->next;
}
kmem[id].freelist = kmem[i].freelist;
if (p == kmem[i].freelist) {
// only have one page
kmem[i].freelist = 0;
}
else {
kmem[i].freelist = p;
pre->next = 0;
}
success = 1;
}
release(&kmem[i].lock);
if (success) {
r = kmem[id].freelist;
kmem[id].freelist = r->next;
break;
}
}
}
release(&kmem[id].lock);
pop_off(); if(r)
memset((char*)r, 5, PGSIZE); // fill with junk
return (void*)r;
}

实验结果如下:

$ kalloctest
start test1
test1 results:
--- lock kmem/bcache stats
lock: kmem_0: #fetch-and-add 0 #acquire() 77186
lock: kmem_1: #fetch-and-add 0 #acquire() 182362
lock: kmem_2: #fetch-and-add 0 #acquire() 173534
lock: bcache_bucket: #fetch-and-add 0 #acquire() 128
lock: bcache_bucket: #fetch-and-add 0 #acquire() 138
lock: bcache_bucket: #fetch-and-add 0 #acquire() 142
lock: bcache_bucket: #fetch-and-add 0 #acquire() 148
lock: bcache_bucket: #fetch-and-add 0 #acquire() 132
lock: bcache_bucket: #fetch-and-add 0 #acquire() 6
lock: bcache_bucket: #fetch-and-add 0 #acquire() 42
lock: bcache_bucket: #fetch-and-add 0 #acquire() 34
lock: bcache_bucket: #fetch-and-add 0 #acquire() 5916
lock: bcache_bucket: #fetch-and-add 0 #acquire() 32
lock: bcache_bucket: #fetch-and-add 0 #acquire() 242
lock: bcache_bucket: #fetch-and-add 0 #acquire() 128
lock: bcache_bucket: #fetch-and-add 0 #acquire() 128
--- top 5 contended locks:
lock: proc: #fetch-and-add 31954 #acquire() 206502
lock: proc: #fetch-and-add 24395 #acquire() 206518
lock: proc: #fetch-and-add 9306 #acquire() 206501
lock: proc: #fetch-and-add 7463 #acquire() 206481
lock: proc: #fetch-and-add 5209 #acquire() 206480
tot= 0
test1 OK
start test2
total free number of pages: 32493 (out of 32768)
.....
test2 OK

Buffer cache (hard)

这个实验是要对XV6的磁盘缓冲区进行优化。在初始的XV6磁盘缓冲区中是使用一个LRU链表来维护的,而这就导致了每次获取、释放缓冲区时就要对整个链表加锁,也就是说缓冲区的操作是完全串行进行的。

为了提高并行性能,我们可以用哈希表来代替链表,这样每次获取和释放的时候,都只需要对哈希表的一个桶进行加锁,桶之间的操作就可以并行进行。只有当需要对缓冲区进行驱逐替换时,才需要对整个哈希表加锁来查找要替换的块。

使用哈希表就不能使用链表来维护LRU信息,因此需要在buf结构体中添加timestamp域来记录释放的事件,同时prev域也不再需要。

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 list
struct buf *next;
uchar data[BSIZE]; uint timestamp;
};

brelse函数中对timestamp域进行维护,同时将链表的锁替换为桶级锁:

void
brelse(struct buf *b)
{
if(!holdingsleep(&b->lock))
panic("brelse"); releasesleep(&b->lock); int idx = hash(b->dev, b->blockno); acquire(&hashtable[idx].lock);
b->refcnt--;
if (b->refcnt == 0) {
// no one is waiting for it.
b->timestamp = ticks;
} release(&hashtable[idx].lock);
}

定义哈希表的结构体,bcache.lock为表级锁,而hashtable[i].lock为桶级锁:

#define NBUCKET 13
#define NBUF (NBUCKET * 3) struct {
struct spinlock lock;
struct buf buf[NBUF];
} bcache; struct bucket {
struct spinlock lock;
struct buf head;
}hashtable[NBUCKET]; int
hash(uint dev, uint blockno)
{
return blockno % NBUCKET;
}

binit函数中对哈希表进行初始化,将bcache.buf[NBUF]中的块平均分配给每个桶,记得设置b->blockno使块的hash与桶相对应,后面需要根据块来查找对应的桶。

void
binit(void)
{
struct buf *b; initlock(&bcache.lock, "bcache"); for(b = bcache.buf; b < bcache.buf+NBUF; b++){
initsleeplock(&b->lock, "buffer");
} b = bcache.buf;
for (int i = 0; i < NBUCKET; i++) {
initlock(&hashtable[i].lock, "bcache_bucket");
for (int j = 0; j < NBUF / NBUCKET; j++) {
b->blockno = i; // hash(b) should equal to i
b->next = hashtable[i].head.next;
hashtable[i].head.next = b;
b++;
}
}
}

之后就是重点bget函数,首先在对应的桶当中查找当前块是否被缓存,如果被缓存就直接返回;如果没有被缓存的话,就需要查找一个块并将其逐出替换。我这里使用的策略是先在当前桶当中查找,当前桶没有查找到再去全局数组中查找,这样的话如果当前桶中有空闲块就可以避免全局锁。

在全局数组中查找时,要先加上表级锁,当找到一个块之后,就可以根据块的信息查找到对应的桶,之后再对该桶加锁,将块从桶的链表上取下来,释放锁,最后再加到当前桶的链表上去。

这里有个小问题就是全局数组中找到一个块之后,到对该桶加上锁之间有一个窗口,可能就在这个窗口里面这个块就被那个桶对应的本地查找阶段用掉了。因此,需要在加上锁之后判断是否被用了,如果被用了就要重新查找。

static struct buf*
bget(uint dev, uint blockno)
{
// printf("dev: %d blockno: %d Status: ", dev, blockno);
struct buf *b; int idx = hash(dev, blockno);
struct bucket* bucket = hashtable + idx;
acquire(&bucket->lock); // Is the block already cached?
for(b = bucket->head.next; b != 0; b = b->next){
if(b->dev == dev && b->blockno == blockno){
b->refcnt++;
release(&bucket->lock);
acquiresleep(&b->lock);
// printf("Cached %p\n", b);
return b;
}
} // Not cached.
// First try to find in current bucket.
int min_time = 0x8fffffff;
struct buf* replace_buf = 0; for(b = bucket->head.next; b != 0; b = b->next){
if(b->refcnt == 0 && b->timestamp < min_time) {
replace_buf = b;
min_time = b->timestamp;
}
}
if(replace_buf) {
// printf("Local %d %p\n", idx, replace_buf);
goto find;
} // Try to find in other bucket.
acquire(&bcache.lock);
refind:
for(b = bcache.buf; b < bcache.buf + NBUF; b++) {
if(b->refcnt == 0 && b->timestamp < min_time) {
replace_buf = b;
min_time = b->timestamp;
}
}
if (replace_buf) {
// remove from old bucket
int ridx = hash(replace_buf->dev, replace_buf->blockno);
acquire(&hashtable[ridx].lock);
if(replace_buf->refcnt != 0) // be used in another bucket's local find between finded and acquire
{
release(&hashtable[ridx].lock);
goto refind;
}
struct buf *pre = &hashtable[ridx].head;
struct buf *p = hashtable[ridx].head.next;
while (p != replace_buf) {
pre = pre->next;
p = p->next;
}
pre->next = p->next;
release(&hashtable[ridx].lock);
// add to current bucket
replace_buf->next = hashtable[idx].head.next;
hashtable[idx].head.next = replace_buf;
release(&bcache.lock);
// printf("Global %d -> %d %p\n", ridx, idx, replace_buf);
goto find;
}
else {
panic("bget: no buffers");
} find:
replace_buf->dev = dev;
replace_buf->blockno = blockno;
replace_buf->valid = 0;
replace_buf->refcnt = 1;
release(&bucket->lock);
acquiresleep(&replace_buf->lock);
return replace_buf;
}

最后将bpinbunpin的锁替换为桶级锁就行了:

void
bpin(struct buf *b) {
int idx = hash(b->dev, b->blockno);
acquire(&hashtable[idx].lock);
b->refcnt++;
release(&hashtable[idx].lock);
} void
bunpin(struct buf *b) {
int idx = hash(b->dev, b->blockno);
acquire(&hashtable[idx].lock);
b->refcnt--;
release(&hashtable[idx].lock);
}

实验结果如下:

start test0
test0 results:
--- lock kmem/bcache stats
...
lock: bcache_bucket: #fetch-and-add 0 #acquire() 4244
lock: bcache_bucket: #fetch-and-add 0 #acquire() 2246
lock: bcache_bucket: #fetch-and-add 0 #acquire() 4402
lock: bcache_bucket: #fetch-and-add 0 #acquire() 4458
lock: bcache_bucket: #fetch-and-add 0 #acquire() 6450
lock: bcache_bucket: #fetch-and-add 0 #acquire() 6312
lock: bcache_bucket: #fetch-and-add 0 #acquire() 6624
lock: bcache_bucket: #fetch-and-add 0 #acquire() 6634
lock: bcache_bucket: #fetch-and-add 0 #acquire() 12706
lock: bcache_bucket: #fetch-and-add 0 #acquire() 6208
lock: bcache_bucket: #fetch-and-add 0 #acquire() 4360
lock: bcache_bucket: #fetch-and-add 0 #acquire() 4246
lock: bcache_bucket: #fetch-and-add 0 #acquire() 2236
--- top 5 contended locks:
lock: proc: #fetch-and-add 269741 #acquire() 4551132
lock: proc: #fetch-and-add 236112 #acquire() 4551131
lock: proc: #fetch-and-add 186278 #acquire() 4551151
lock: proc: #fetch-and-add 167286 #acquire() 4551164
lock: proc: #fetch-and-add 151922 #acquire() 4551132
tot= 0
test0: OK
start test1
test1 OK

XV6学习(12)Lab lock: Parallelism/locking的更多相关文章

  1. XV6学习笔记(2) :内存管理

    XV6学习笔记(2) :内存管理 在学习笔记1中,完成了对于pc启动和加载的过程.目前已经可以开始在c语言代码中运行了,而当前已经开启了分页模式,不过是两个4mb的大的内存页,而没有开启小的内存页.接 ...

  2. xv6学习笔记(5) : 锁与管道与多cpu

    xv6学习笔记(5) : 锁与管道与多cpu 1. xv6锁结构 1. xv6操作系统要求在内核临界区操作时中断必须关闭. 如果此时中断开启,那么可能会出现以下死锁情况: 进程A在内核态运行并拿下了p ...

  3. xv6学习笔记(4) : 进程调度

    xv6学习笔记(4) : 进程 xv6所有程序都是单进程.单线程程序.要明白这个概念才好继续往下看 1. XV6中进程相关的数据结构 在XV6中,与进程有关的数据结构如下 // Per-process ...

  4. JavaScript学习12 JS中定义对象的几种方式

    JavaScript学习12 JS中定义对象的几种方式 JavaScript中没有类的概念,只有对象. 在JavaScript中定义对象可以采用以下几种方式: 1.基于已有对象扩充其属性和方法 2.工 ...

  5. ThinkPhp学习12

    原文:ThinkPhp学习12 二.输出模板内容      (重点) a.display 1.display中没有参数    $this->display(); 2.可以带参数    $this ...

  6. JavaScript学习12 JS中定义对象的几种方式【转】

    avaScript学习12 JS中定义对象的几种方式 转自:  http://www.cnblogs.com/mengdd/p/3697255.html JavaScript中没有类的概念,只有对象. ...

  7. Asp.NetCore源码学习[1-2]:配置[Option]

    Asp.NetCore源码学习[1-2]:配置[Option] 在上一篇文章中,我们知道了可以通过IConfiguration访问到注入的ConfigurationRoot,但是这样只能通过索引器IC ...

  8. XV6学习笔记(1) : 启动与加载

    XV6学习笔记(1) 1. 启动与加载 首先我们先来分析pc的启动.其实这个都是老生常谈了,但是还是很重要的(也不知道面试官考不考这玩意), 1. 启动的第一件事-bios 首先启动的第一件事就是运行 ...

  9. xv6学习笔记(3):中断处理和系统调用

    xv6学习笔记(3):中断处理和系统调用 1. tvinit函数 这个函数位于main函数内 表明了就是设置idt表 void tvinit(void) { int i; for(i = 0; i & ...

随机推荐

  1. win7-win10 禁用IPV6临时地址

    IPV6临时地址本意是保护设备隐私,但有时候需要暂时禁用的情景下指令 netsh interface ipv6 set privacy state=disable 启用则修改最后的状态值为enable ...

  2. 用python+sklearn(机器学习)实现天气预报数据 模型和使用

    用python+sklearn机器学习实现天气预报 模型和使用 项目地址 系列教程 0.前言 1.建立模型 a.准备 引入所需要的头文件 选择模型 选择评估方法 获取数据集 b.建立模型 c.获取模型 ...

  3. TCP连接的建立与释放(超详细)

    前言:在计算机网络协议中,TCP只是其中一个,然而在网络使用中,TCP也是最离不开的协议之一,它的重要性毋庸置疑,最最重要的是,面试的重点就是它啊,呜呜~~,今天我们一起来看下TCP的连接建立与释放, ...

  4. Memcached、Redis、Mongodb比较

    Memcached(内存Cache) Memcached 是一个高性能的分布式内存对象缓存系统.通过在内存里维护一个统一的巨大的hash表,它能够用来存储各种格式的数据,包括图像.视频.文件以及数据库 ...

  5. 【栈和队列】5、队列概述与数组队列的基本实现 - Java

    3-5 数组队列 简单记录 - bobo老师的玩转算法系列–玩转数据结构 - 栈和队列 队列Queue 队列也是一种线性结构 相比数组,队列对应的操作是数组的子集 只能从一端(队尾)添加元素,只能从另 ...

  6. 【Docker】CentOS7 上无网络情况下安装

    自建虚拟机,但是连接不上网络,只能通过下载rpm包进行安装docker 环境:CentOS 7.3.1611 x64 rpm镜像下载地址用的阿里的https://mirrors.aliyun.com/ ...

  7. 【Linux】Linux下如何分区及如何格式化

    环境:CentOS7.1 磁盘大小是1.8T 将磁盘/dev/sda分一个分区,分区类型为xfs fdisk /dev/sda n    --创建新分区 p  --创建分区类型为主分区 1  --主分 ...

  8. 【Linux】扩大swap分区

    今天安装oracle的时候,提示我swap分区过小.需要最少3g以上 但是安装系统了,想要扩大swap分区怎么办呢 下面来介绍如何扩大swap分区 按步骤介绍 Red Hat linux 如何增加sw ...

  9. 敏捷史话(四):敏捷是人的天性 —— Arie van Bennekum

    敏捷是人的天性,是你与生俱来的东西.面对敏捷,Arie van Bennekum 下了这样一个结论. 但这并不意味着人们只能通过天赋获得敏捷,对于想要学习敏捷的人来说,敏捷绝不是仅仅靠学习僵化的框架. ...

  10. [Usaco2005 Dec]Scales 天平

    题目描述 约翰有一架用来称牛的体重的天平.与之配套的是N(1≤N≤1000)个已知质量的砝码(所有砝码质量的数值都在31位二进制内).每次称牛时,他都把某头奶牛安置在天平的某一边,然后往天平另一边加砝 ...