【MTI 6.S081 Lab】locks

news2024/11/13 14:38:10

【MTI 6.S081 Lab】locks

  • Memory allocator (moderate)
    • 实验任务
    • Hint
    • 解决方案
  • Buffer cache (hard)
    • 实验任务
    • Hint
    • 解决方案
      • 数据结构设计
      • 初始化数据结构
      • get
      • relse

本实验前去看《操作系统导论》第29章基于锁的并发数据结构,将会是很有帮助的。

在这个实验室中,您将获得重新设计代码以提高并行性的经验。多核机器上并行性差的一个常见问题是锁的高竞争。提高并行性通常需要更改数据结构和锁定策略,以减少争用。您将为xv6内存分配器和块缓存执行此操作。

Before writing code, make sure to read the following parts from the xv6 book :

  • Chapter 6: “Locking” and the corresponding code.
  • Section 3.5: “Code: Physical memory allocator”
  • Section 8.1 through 8.3: “Overview”, “Buffer cache layer”, and “Code: Buffer cache”

Memory allocator (moderate)

程序user/kaloctest强调xv6的内存分配器:三个进程的地址空间不断增加和缩小,导致对kalloc和kfree的多次调用。kalloc和kfree获得kmem.lock。kalloctest打印(作为“#test and set”)由于试图获取另一个核心已经持有的锁而导致的获取中的循环迭代次数,用于kmem锁和其他一些锁。捕获中的循环迭代次数是锁争用的粗略度量。在你开始实验室之前,kalloctest的输出看起来与此类似:

在这里插入图片描述

您可能会看到与此处显示的计数不同的计数,以及前5个争用锁的不同顺序。

acquire为每个锁维护要为该锁获取的调用计数,以及acquire中的循环尝试但未能设置锁的次数。kalloctest调用一个系统调用,使内核打印kmem和bcache锁(这是本实验的重点)以及5个争用最多的锁的计数。如果存在锁争用,则获取循环迭代的次数将很大。系统调用返回kmem和bcache锁的循环迭代次数之和。

对于这个实验,您必须使用一台带有多个核心的专用空载机器。如果你使用一台正在做其他事情的机器,kalloctest打印的计数将是无稽之谈。你可以使用专用的Athena工作站,或者你自己的笔记本电脑,但不要使用拨号机。

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

实验任务

你的工作是实现每个CPU的空闲列表,并在CPU的空闲清单为空时进行窃取。您必须提供所有以“kmem”开头的锁名称。也就是说,您应该为每个锁调用initlock,并传递一个以“kmem”开头的名称。运行kalloctest查看您的实现是否减少了锁争用。要检查它是否仍然可以分配所有内存,请运行usertests sbrkmuch。您的输出将与下面显示的类似,kmem锁上的争用总数大大减少,尽管具体数字会有所不同。确保usertests-q中的所有测试都通过。Make grade应该表示kalloctests通过。

Hint

  • 您可以使用kernel/param.h中的常量NCPU

  • 让freerange给运行freerange的CPU所有可用内存。

  • 函数cpuid返回当前的核心编号,但只有在中断关闭时调用它并使用其结果才是安全的。您应该使push_off()和pop_off(()来关闭和打开中断。

  • 看看kernel/ssprintf.c中的snprintf函数,了解字符串格式化的想法。不过,可以将所有锁命名为“kmem”。

  • 可以选择使用xv6的竞争检测器运行您的解决方案:

    $ make clean
    $ make KCSAN=1 qemu
    $ kalloctest
    

    kalloctest可能会失败,但你不应该看到任何竞争。如果xv6的竞争检测器观察到竞争,它将沿着以下行打印描述竞争的两个堆栈跟踪:

    在您的操作系统上,您可以通过将回溯剪切并粘贴到addr2line中,将其转换为带有行号的函数名:

    您不需要运行竞争检测器,但您可能会发现它很有帮助。请注意,竞争检测器会显著降低xv6的速度,因此您可能不想在运行用户测试时使用它。

解决方案

void
freerange(void *pa_start, void *pa_end)
{
  char *p;
  p = (char*)PGROUNDUP((uint64)pa_start);
  int id = 0;
  struct run *r;
  memset(p, 1, (char*)pa_end - p);
  for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE) {
    r = (struct run*)p;
    r->next = kmem[id].freelist;
    kmem[id].freelist = r;
  }
}

void *
kalloc(void)
{
  struct run *r;
  push_off();
  int id = cpuid();
  pop_off();
  acquire(&kmem[id].lock);
  if (!kmem[id].freelist) {
    int flag = 1;
    while (flag) {
      flag = 0;
      for (int i = 0; i < NCPU ; ++i) {
        if (i == id) {
          continue;
        }
        if (kmem[i].freelist) { // 先检测一次,避免锁中等待
          // 这一轮中检测到有空闲的,如果没有分到,说明可能是自旋锁等待太久了,且当前及之后的i都被分配了
          // 但是由于在这里等了很久,那么前面的可能有释放了,所以我们可以再次循环一次
          // 如果flag一直等于0,说明没进入这里,那么说明系统的page实在是太少了,因为NCPU的if语句将会执行非常快,时间几乎可以忽略
          // 甚至运行时间没有个kfree的时间长,所以此时说明系统真的是没有多余的page了
          // 当然也会有一定的产生错误的可能,但是此时系统中的page必然很少,甚至于每个cpu上只有几个page
          // 这个错误,我们就不关注了,在这种情况下,系统告诉程序没有内存可分配了,也是合理的
          // 经过这个修改后,kalloctest便没有出错了,不然多运行几次,总会在test3出现错误,说明当时没有可分配的
          // 但是对于test3来说,应该要是有可以分配的。
          flag = 1; 
          acquire(&kmem[i].lock);
          if (kmem[i].freelist) {
            kmem[id].freelist = kmem[i].freelist;
            // steal half of memory
            struct run *fast = kmem[i].freelist; // faster pointer
            struct run *slow = kmem[i].freelist;
            struct run *pre = 0;
            while (fast && fast->next) {
              fast = fast->next->next;
              pre = slow;
              slow = slow->next;
            }
            if (pre == 0) {
              // only have one page
              kmem[i].freelist = 0;
            }
            else {
              kmem[i].freelist = slow;
              pre->next = 0;
            }
            release(&kmem[i].lock);
            break;
          }
          release(&kmem[i].lock);
        }
      }
      if (kmem[id].freelist) {
        break;
      }
    }
  }
  r = kmem[id].freelist;
  if(r) {
    kmem[id].freelist = r->next;
  }
  release(&kmem[id].lock);

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

Buffer cache (hard)

这一半任务独立于前一半任务;无论你是否完成了前半部分,你都可以在这半部分工作(并通过测试)。

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

您可能会看到不同的输出,但bcache锁的测试和设置数量会很高。如果您查看kernel/bio.c中的代码,您会发现bcache.lock保护缓存的块缓冲区列表、每个块缓冲区中的引用计数(b->refcnt)以及缓存块的标识(b->dev和b->blockno)。

实验任务

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

在这里插入图片描述

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

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

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

  • 当两个进程同时使用相同的块号时。bcachetest test0从不执行此操作。
  • 当两个进程同时在缓存中未命中,并且需要找到一个未使用的块来替换时。bcachetest test0从不执行此操作。
  • 当两个进程同时使用冲突的块时,无论您使用什么方案来划分块和锁;例如,如果两个进程使用的块的块号哈希到哈希表中的同一个槽。bcachetesttest0可能会这样做,这取决于您的设计,但您应该尝试调整方案的详细信息以避免冲突(例如,更改哈希表的大小)。

bcachetest的test1使用了比缓冲区更多的不同块,并练习了许多文件系统代码路径。

Hint

  • 阅读xv6手册中关于块缓存的描述(第8.1-8.3节)。
  • 可以使用固定数量的bucket,而不动态调整哈希表的大小。使用素数的桶(例如,13)来减少散列冲突的可能性。
  • 在哈希表中搜索缓冲区并在找不到缓冲区时为该缓冲区分配条目必须是原子的。
  • 删除所有缓冲区的列表(bcache.head等),不要实现LRU。通过此更改,brelse不需要获取bcache锁。在bget中,您可以选择refcnt==0的任何块,而不是最近使用最少的块。
  • 您可能无法以原子方式检查一个已经缓存的buf和如果未缓存找一个未使用的buf;如果缓冲区不在缓存中,则可能需要删除所有锁并从头开始。在bget中串行查找未使用的buf(即,当查找在缓存中未命中时,bget中选择要重用的缓冲区的部分)是可以的。
  • 在某些情况下,您的解决方案可能需要持有两个锁;例如,在驱逐过程中,您可能需要持有bcache锁和每个bucket一个锁。确保避免死锁。
  • 当替换一个块时,您可能会将struct buf从一个bucket移动到另一个bucket,因为新的块会散列到不同的bucket。您可能会遇到一个棘手的情况:新块可能会与旧块哈希到同一个bucket。在这种情况下,一定要避免死锁。
  • 一些调试技巧:实现bucket锁,但将全局bcache.lock获取/释放留在bget的开始/结束处,以序列化代码。一旦您确定它在没有竞争条件的情况下是正确的,就删除全局锁并处理并发问题。您还可以运行makeCPUS=1qemu来使用一个核心进行测试。
  • 使用xv6的竞争检测器来查找潜在的竞争(请参阅上面如何使用竞争检测器)。

解决方案

数据结构设计

哈希桶为素数,有利于block在哈希表中均匀分布。

#define HASH_BUCKETS 13

struct {
  struct spinlock lock;
  struct buf *buf; 
  char name[128];
} hash_buckets[HASH_BUCKETS];

static int
hash(int blockbo) {
  return blockbo % HASH_BUCKETS;
}

初始化数据结构

void
binit(void)
{
  // struct buf *b;

  initlock(&bcache.lock, "bcache");
  for (int i = 0; i < HASH_BUCKETS; ++i) {
    snprintf(hash_buckets[i].name, sizeof(hash_buckets[i].name), "hash_buckets_%d", i);
    initlock(&hash_buckets[i].lock, hash_buckets[i].name);
  }

  for (int i = 0; i < NBUF - 1; ++i) {
    bcache.buf[i].next = &bcache.buf[i+1];
    bcache.buf[i+1].prev = &bcache.buf[i];
  }
  hash_buckets[0].buf = bcache.buf;
}

全部将其放入0号桶,这可能会在开始时造成一些竞争。

get

acquire(&hash_buckets[rdx].lock);这里能获得锁的原因是,不会同时有两个进入这个区域,且b一定不再桶idx中。因为之前我们已经搜索过idx了,里面没有,而我们又是一直有idx的锁,所以不可能从无变成有。同时,rdx中有,那么rdx的锁不可能走到第34行处,所以也不会产生死锁。

获得锁后,再检测一次,避免在break到现在的空隙,别的进程已经更改了这个block的状态。如果状态已经改变,那么就去refind处重新查找

// Look through buffer cache for block on device dev.
// If not found, allocate a buffer.
// In either case, return locked buffer.
static struct buf*
bget(uint dev, uint blockno)
{
  struct buf *b;
  int idx = hash(blockno);
  acquire(&hash_buckets[idx].lock);
  for (b = hash_buckets[idx].buf; b; b = b->next) {
    if(b->dev == dev && b->blockno == blockno){
      b->refcnt++;
      release(&hash_buckets[idx].lock);
      acquiresleep(&b->lock);
      return b;
    }
  }

  // 处理冲突,在空闲中找
  // 首先在当前找
  for (b = hash_buckets[idx].buf; b; b = b->next) {
    if (b->refcnt == 0) {
      // 可以移除,但是由于hash值一样,那么就在此bucket中
      b->dev = dev;
      b->blockno = blockno;
      b->valid = 0;
      b->refcnt = 1;
      release(&hash_buckets[idx].lock);
      acquiresleep(&b->lock);
      return b;
    }
  }

  acquire(&bcache.lock);
refind:
  b = 0;
  for (int i = 0; i < NBUF; ++i) {
    if (bcache.buf[i].refcnt == 0) {
      b = &bcache.buf[i];
      break;
    }
  }
  if (!b) {
    panic("bget: no buffers");
  }
  int rdx = hash(b->blockno);
  // 这里能获得锁的原因是,不会同时有两个进入这个区域,且b一定不再桶idx中
  // 因为之前我们已经搜索过idx了,里面没有,而我们又是一直有idx的锁,所以不可能从无变成有
  // 同时,rdx中有,那么rdx的锁不可能走到第34行处,所以也不会产生死锁
  acquire(&hash_buckets[rdx].lock);
  if (b->refcnt != 0) {	// 获得锁后,再检测一次,避免在break到现在的空隙,别的进程已经更改了这个block的状态
    release(&hash_buckets[rdx].lock);
    goto refind;
  }
  // 从bucket中移除
  if(b->prev) {
    b->prev->next = b->next;
  } 
  if (b->next) {
    b->next->prev = b->prev;
  }
  if (hash_buckets[rdx].buf == b) {
    // 去除第一个
    hash_buckets[rdx].buf = b->next;
  }
  release(&hash_buckets[rdx].lock);
  // 加入idx中
  if(hash_buckets[idx].buf) {
    b->next = hash_buckets[idx].buf;
    hash_buckets[idx].buf->prev = b;
    b->prev = 0;
    hash_buckets[idx].buf = b;
  } else {
    b->next = 0;
    b->prev = 0;
    hash_buckets[idx].buf = b;
  }
  b->valid = 0;
  b->refcnt = 1;
  b->dev = dev;
  b->blockno = blockno;
  release(&bcache.lock);
  release(&hash_buckets[idx].lock);
  acquiresleep(&b->lock);
  return b;
}

relse

在brelse中,首先,b此时别的进程可以用了,所有releasesleep。然后锁住所在的桶,将block的引用减1。此时不用担心b->refcnt会变为负数,因为b->refcnt的更新是原子的,且get后才能release,所以必然是先加然后减,那么就不用担心其变为负数了。

// Release a locked buffer.
// Move to the head of the most-recently-used list.
void
brelse(struct buf *b)
{
  if(!holdingsleep(&b->lock))
    panic("brelse");

  releasesleep(&b->lock);
  int idx = hash(b->blockno);
  acquire(&hash_buckets[idx].lock);
  b->refcnt--;
  release(&hash_buckets[idx].lock);
}

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/812198.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

懒得改变原始对象?JavaScript代理模式教你怎样一步步偷懒!

前言 系列首发gong zhong hao[『非同质前端札记』] &#xff0c;若不想错过更多精彩内容&#xff0c;请“星标”一下&#xff0c;敬请关注gong zhong hao最新消息。 懒得改变原始对象&#xff1f;JavaScript代理模式教你怎样一步步偷懒&#xff01; 何为代理模式 例如&#x…

倍增与ST算法

倍增与ST算法 倍增倍增原理倍增法的局限例题 &#xff1a;国旗计划 (洛谷 P4155)例题题解带注释的代码 ST算法ST算法原理ST算法步骤ST算法应用场合例题 &#xff1a;【模板】ST表 (洛谷 P3865) 倍增 倍增原理 倍增法的局限 例题 &#xff1a;国旗计划 (洛谷 P4155) 例题题解 带…

华为OD机试真题 Java 实现【报文回路】【2023 B卷 100分】,俗称“礼尚往来”

目录 专栏导读一、题目描述二、输入描述三、输出描述四、解题思路1、报文回路2、异常情况&#xff1a;3、解题思路 五、Java算法源码六、效果展示1、输入2、输出 华为OD机试 2023B卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&…

《JavaSE-第二十章》之线程的创建与Thread类

文章目录 什么是进程&#xff1f;什么是线程&#xff1f;为什么需要线程&#xff1f; 基本的线程机制创建线程1.实现 Runnable 接口2.继承 Thread 类3.其他变形 Thread常见构造方法1. Thread()2. Thread(Runnable target)3. Thread(String name)4. Thread(Runnable target, Str…

epoll复用

cli #include <stdlib.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h>// 服务器ip #define IP "192.168.250.100" // 服务器端口 #define PORT 8888int main…

c++11 标准模板(STL)(std::basic_ifstream)(一)

定义于头文件 <fstream> template< class CharT, class Traits std::char_traits<CharT> > class basic_ifstream : public std::basic_istream<CharT, Traits> 类模板 basic_ifstream 实现文件流上的高层输入操作。它将 std::basic_istream…

Flink - souce算子

水善利万物而不争&#xff0c;处众人之所恶&#xff0c;故几于道&#x1f4a6; 目录 1. 从Java的集合中读取数据 2. 从本地文件中读取数据 3. 从HDFS中读取数据 4. 从Socket中读取数据 5. 从Kafka中读取数据 6. 自定义Source 官方文档 - Flink1.13 1. 从Java的集合中读取数据 …

二叉树(C语言)

文章目录 1.树1.1概念1.2相关定义1.3 表示&#xff08;左孩子右兄弟&#xff09; 2.二叉树2.1概念2.2特殊的二叉树1. 满二叉树&#xff1a;2. 完全二叉树&#xff1a; 2.3二叉树的性质2.4练习 3.二叉树的存储结构1. 顺序存储2. 链式存储 4.完全二叉树的代码实现4.1堆的介绍1.堆…

ssm德宏贸易项目java人资企业办公jsp源代码mysql

本项目为前几天收费帮学妹做的一个项目&#xff0c;Java EE JSP项目&#xff0c;在工作环境中基本使用不到&#xff0c;但是很多学校把这个当作编程入门的项目来做&#xff0c;故分享出本项目供初学者参考。 一、项目描述 ssm德宏贸易项目 系统有1权限&#xff1a;管理员 二…

接口自动化测试平台

下载了大神的EasyTest项目demo修改了下<https://testerhome.com/topics/12648 原地址>。也有看另一位大神的HttpRunnerManager<https://github.com/HttpRunner/HttpRunnerManager 原地址>&#xff0c;由于水平有限&#xff0c;感觉有点复杂~~~ 【整整200集】超超超…

查询结果元数据-MetaData对象、数据库工具类的封装、通过反射实现数据查询的封装

六、查询结果元数据-MetaData对象 七、数据库工具类的封装 1、PropertieUtil类 2、DbUtil类 3、DBHepler类 查询&#xff1a; 4、TestDb测试类&#xff1a; 更新&#xff1a; 1&#xff09;插入&#xff1a; 2&#xff09;修改&#xff1a; 3&#xff09;删除&#xff1a; 查…

2024考研408-计算机网络 第二章-物理层学习笔记

文章目录 前言一、通信基础1.1、物理层基本概念1.1.1、认识物理层1.1.2、认识物理层的四种接口特性 1.2、数据通信基础知识1.2.1、典型的数据通信模型及相关术语1.2.2、数据通信相关术语1.2.3、设计数据通信系统要考虑的三个问题&#xff1a;问题1&#xff1a;采用单工通信/半双…

通讯录的实现(超详细)——C语言(进阶)

目录 一、创建联系人信息&#xff08;结构体&#xff09; 二、创建通讯录&#xff08;结构体&#xff09; 三、define定义常量 四、打印通讯录菜单 五、枚举菜单选项 六、初始化通讯录 七、实现通讯的的功能 7.1 增加加联系人 7.2 显示所有联系人的信息 ​7.3 单独查…

【自动化运维】Ansible常见模块的运用

目录 一、Ansible简介二、Ansible安装部署2.1环境准备 三、ansible 命令行模块3.1&#xff0e;command 模块3.2&#xff0e;shell 模块3.3&#xff0e;cron 模块3.4&#xff0e;user 模块3.5&#xff0e;group 模块3.6&#xff0e;copy 模块3.7&#xff0e;file 模块8&#xff…

C++之观察者模式(发布-订阅)

目录 模式简介 介绍 优点 缺点 代码实现 场景说明 实现代码 运行结果 模式简介 观察者模式&#xff08;Observer Pattern&#xff09;&#xff0c;也叫我们熟知的发布-订阅模式。 它是一种行为型模式。 介绍 观察者模式主要关注的是对象的一对多的关系&#xff0c; …

4-3 Working with time series

本文所用数据下载 Data from a Washington, D.C., bike-sharing system reporting the hourly count of rental bikes in 2011–2012 in the Capital Bikeshare system, along with weather and seasonal information. Our goal will be to take a flat, 2D dataset and trans…

搭建网站 --- 快速WordPress个人博客并内网穿透发布到互联网

文章目录 快速WordPress个人博客并内网穿透发布到互联网 快速WordPress个人博客并内网穿透发布到互联网 我们能够通过cpolar完整的搭建起一个属于自己的网站&#xff0c;并且通过cpolar建立的数据隧道&#xff0c;从而让我们存放在本地电脑上的网站&#xff0c;能够为公众互联…

JS——输入输出语法数组的操作

JavaScript输入输出语法 目标&#xff1a;能写出常见的JavaScript输入输出语法 输出语法 语法1&#xff1a; document.write(要输出的内容)作用&#xff1a; 向body内输出内容 注意&#xff1a; 如果输出的内容写的是标签&#xff0c;也会被解析成网页元素 语法2&#xff1a…

2023大同首届信息技术产业峰会举行,共话数字经济新未来

7月28日&#xff0c;“聚势而强共领信创”2023大同首届信息技术产业峰会圆满举行。本次峰会由中共大同市委、大同市人民政府主办&#xff0c;中国高科技产业化研究会国际交流合作中心、山西省信创协会协办&#xff0c;中共大同市云冈区委、大同市云冈区人民政府、诚迈科技&…

一文深入了解Cmk

目录 一、Cmk&#xff08;设备能力指数&#xff09;介绍&#xff1a;二、Cmk&#xff08;设备能力指数&#xff09; 概念&#xff1a;三、Cmk的应用时机&#xff1a;四、Cmk前期准备和要求&#xff1a;五、Cmk测试要求&#xff1a;六、CMK计算公式&#xff1a;七、Cmk实际操作&…