6.1810: Operating System Engineering 2023 <Lab7 lock: Parallelism/locking>

一、本节任务

二、要点 

2.1 文件系统(file system) 

xv6 文件系统软件层次如下:

通过路径树我们可以找到相应的文件:

fd(文件描述符)是进程用来标识其打开的文件的手段,每个进程有自己的文件打开表,并且系统会维护一个全局文件打开表(系统中所有打开的文件都保存在这个全局文件打开表中)。

进程通过 fd 将文件作为一系列字节来访问,每一个 fd 都有一个光标(cursor)来指向文件的当前访问位置:

read() 和 write() 系统调用都会使光标前进: 

也有一些特殊的文件,比如 pipe() 创建出来的缓冲区,其本质是一个文件,不同的进程使用 fd 对缓冲区文件进行读和写,从而实现进程间的通信: 

inode(index node)索引节点用来记录一个文件的详细信息,包括:

  • 记录文件的大小以及数据在磁盘上的存储位置;
  • 记录文件的硬链接数量(和打开 fd 数量);

inode 可以是指磁盘上的 inode,也可能指内存上的 inode 副本(读写 inode 时会先将其读入内存中)。磁盘上的 inode 被打包到一个称为 inode 块的相邻磁盘区域中,每个 inode 大小都一样,所以通过一个索引 i 能很容易找到相应的索引节点,这个索引 i 也被称为 i-number 或 inode number。

address1~12 为直接索引,指向的为数据块,而 indrect 为间接索引,指向的为 indirect block,indirect block 里面的地址才指向数据块,这种方式可以增加文件的最大大小,并且减少 dinode 结构体的大小。 

数据存储的位置? 

磁盘访问是十分低效的,我们可以将 RAM 的一部分作为磁盘的 cache,每次访问磁盘会将连续的部分内容复制到 RAM 上,当再次访问的时候就不需要去访问磁盘,而是访问复制到 RAM 上的内容即可,其替换策略为 LRU(least recently used)。buffer cache 有两个作用:

  1. 同步对磁盘块的访问,以确保只有一个块的副本在内存中,并且一次只有一个内核线程使用该副本;
  2. 缓存常用的块,以便不需要从慢速磁盘上重新读取它们。

相关代码在 bio.c 中,其中主要的接口为 bread() 和 bwrite():

  • bread():获取一个能在内存中读写的磁盘块的副本,使用 buf(buf.h)结构体来管理这个副本;
  • bwrite():后者将修改后的缓冲区写入磁盘上的对应块;
  • brelse():内核线程在使用完后这块缓冲区后需要使用 brelse() 来释放它;

磁盘布局: 

  

xv6 文件系统在磁盘上的结构如下图。xv6将磁盘划分为几个部分,文件系统不使用 block0(它用来保存引导扇区)。block1 称为超级块(superblock),它包含有关文件系统的元数据(如块中的文件系统大小、数据块的数量、inode 的数量和 log 中的块数)。从 block2 开始的块保持 log,log 之后是 inodes,每个块有多个 inode。在这些之后,位图(bitmap)块跟踪数据库的使用情况。其余的块是数据块,每个块要么在 bitmap 中标记为空闲,要么保存文件或目录的内容。超级块由一个称为 mkfs 的单独程序填充,该程序构建一个初始文件系统。 

Loging layer 可以帮助我们实现崩溃恢复(crash recovery)。出现这个问题的原因是,许多文件系统操作涉及对磁盘的多次写入操作,在某个写入操作后的崩溃可能会使磁盘上的文件系统处于不一致的状态。根据磁盘写入的顺序,崩溃可能会使 inode 留下对标记为 free 的块的引用(os 可能会将该 free 块分配给其他文件,这样就导致有两个文件共享同一个块,可能会导致严重的安全问题),也可能会留下已分配但未被引用的块。

xv6 通过一种简单的日志记录形式解决了文件系统操作过程中的崩溃问题。xv6 系统调用不会直接写磁盘上的文件系统数据结构,相反,它在磁盘上的日志中写入了它希望进行的所有磁盘写操作的描述,一旦系统调用在日志中记录了它所有的写入操作,它就会将一个特殊的提交记录写入磁盘,这表明该日志中包含了一个完整的操作。此时,系统调用才开始执行写磁盘上的文件系统数据结构操作。在这些写入操作完成之后,系统调用再将磁盘上的此次日志删除。

如果系统崩溃并重新启动,则文件系统代码将从崩溃中恢复。如果日志被标记为包含完整操作,则恢复代码将写入复制到磁盘文件系统中所属的位置。如果日志未标记为包含完整的操作,则恢复代码将忽略该日志。最后恢复代码再将日志删除。 所以,logging layer 能保证写操作要么执行完,要么不执行,避免出现数据不一致的情况。

系统调用一开始只会写 buffer cache, 而不是磁盘。

当系统调用完成后 —— commit:

  1. 将所有的更新的块写到磁盘的 log 上(在 log 上保存了更新块的副本);
  2. 将磁盘上的 log 设置为 committed;
  3. 然后再将更新的块写到要写的位置;
  4. 删除 log 上的记录;

当崩溃发生,reboot 后:

如果 log 被设置为 committed,将更新的块从磁盘的 log 上写到要写的位置。

三、Lab lock: Parallelism/locking

在多核机器上,锁的争用使得其性能变差,在本实验中,我们会重新设计部分代码以增加并行性。提高并行性通常需要改变数据结构和锁定策略,以减少争用。

3.1 Memory allocator (moderate)

这部分需要为每个 CPU 维护一个 freelist(kernel/kalloc.c),每个 freelist 都有自己的锁。不同 CPU 上的分配和释放可以并行运行,因为每个 CPU 将对不同的 freelist 进行操作。当一个 CPU 上的 freelist 为空,而另外一个 CPU 的 freelist 还有空闲页面时,没有空闲页面的 CPU 必须 “窃取” 另一个 CPU 的 freelist 的一部分。窃取可能会引入锁争用,但争用频率要比单个 freelist 要小很多。

首先将 kmem 变成一个数组,大小为 CPU 的个数: 

struct {
  struct spinlock lock;
  struct run *freelist;
} kmem[NCPU];

在 kinit() 函数中初始化每个结构体的锁:

void
kinit()
{
  for(int i = 0; i < NCPU; i++)
  {
    char name[9] = {0};
    snprintf(name, 8, "kmem-%d", i);
    initlock(&kmem[i].lock, "kmem");
  }

  freerange(end, (void*)PHYSTOP);
}

使用 kfree() 来释放的页面,要注意释放的页面放到当前 CPU 的 freelist 中:

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;

  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() 分配页面时,需要注意的情况就是当前 CPU 的 freelist 里面没有空页面可用,这时候需要去其他 CPU 的 freelist 偷一个页面过来:

static struct run *steal_free_block(int id)
{
  struct run *p = 0;
  for(int i = 0; i < NCPU; i++)
  {
    if(i != id)
    {
      acquire(&kmem[i].lock);
      p = kmem[i].freelist;
      if(p)
      {
        kmem[i].freelist = p->next;
        p->next = 0;
        release(&kmem[i].lock);
        break;
      }
      release(&kmem[i].lock);
    }
  }
  return p;
}

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;
  }

  release(&kmem[id].lock);

  if(!r)
  {
    r = steal_free_block(id);
  }

  pop_off();

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

3.2 Buffer cache (hard)

在 kenel/bio.c 中的 bcache 结构体中的 lock 的竞争十分激烈,每次访问 bcache 结构体都需要请求这个锁,所以需要我们使用一些策略来减少竞争。

首先是 bcache 结构体需要使用 hash 桶 buf_table 来装入不同 blockno 的块,每个桶对应一个 lock: 

#define NBUCKET 13

struct {
  struct spinlock lock[NBUCKET];
  struct buf buf[NBUF];

  // Linked list of all buffers, through prev/next.
  // Sorted by how recently the buffer was used.
  // head.next is most recent, head.prev is least.
  //struct buf head;

  struct buf buf_table[NBUCKET];
} bcache;

初始化每个桶和每个桶对应的锁: 

void
binit(void)
{
  struct buf *b;
  for(int i = 0; i < NBUCKET; i++){
    char name[9] = {0};
    snprintf(name, 8, "bcache-%d", i);
    initlock(&bcache.lock[i], name);

    bcache.buf_table[i].prev = &bcache.buf_table[i];
    bcache.buf_table[i].next = &bcache.buf_table[i];
  }

  int i;
  for(i = 0,b = bcache.buf; i < NBUF && b < bcache.buf+NBUF; i++,b++){
    int index = i % NBUCKET;
    b->next = bcache.buf_table[index].next;
    b->prev = &bcache.buf_table[index];
    initsleeplock(&b->lock, "buffer");
    bcache.buf_table[index].next->prev = b;
    bcache.buf_table[index].next = b;
  }
}

bget 函数会先根据 blockno 找到对应的 hash 桶,若块已被缓存到对应的桶中,则直接返回,若不存在则循环遍历各个桶找到空闲的块(b->refcnt == 0),若该块在其他桶中,则需要把该块放到当前 blockno 对应的桶中:

static struct buf*
bget(uint dev, uint blockno)
{
  struct buf *b;
  int index = blockno % NBUCKET;

  acquire(&bcache.lock[index]);

  // Is the block already cached?
  for(b = bcache.buf_table[index].next; b != &bcache.buf_table[index]; b = b->next){
    if(b->dev == dev && b->blockno == blockno){
      b->refcnt++;
      release(&bcache.lock[index]);
      acquiresleep(&b->lock);
      return b;
    }
  }
  release(&bcache.lock[index]);

  for(int i = index; ; i = (i+1) % NBUCKET){
    acquire(&bcache.lock[i]);
    for(b = bcache.buf_table[i].next; b != &bcache.buf_table[i]; b = b->next){
      if(b->refcnt == 0) {
        // change bucket
        if(i != index){
          b->next->prev = b->prev;
          b->prev->next = b->next;
          release(&bcache.lock[i]);

          acquire(&bcache.lock[index]);
          b->next = bcache.buf_table[index].next;
          b->prev = &bcache.buf_table[index];
          bcache.buf_table[index].next->prev = b;
          bcache.buf_table[index].next = b;
        }

        b->dev = dev;
        b->blockno = blockno;
        b->valid = 0;
        b->refcnt = 1;
        release(&bcache.lock[index]);
        acquiresleep(&b->lock);
        return b;
      }
    }
    release(&bcache.lock[i]);
  }
  panic("bget: no buffers");
}

下面几个函数根据 block 中的 blockno 来得到对应的桶: 

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

  releasesleep(&b->lock);

  int index = b->blockno % NBUCKET;

  acquire(&bcache.lock[index]);
  b->refcnt--;
  release(&bcache.lock[index]);
}

void
bpin(struct buf *b) {
  int index = b->blockno % NBUCKET;
  acquire(&bcache.lock[index]);
  b->refcnt++;
  release(&bcache.lock[index]);
}

void
bunpin(struct buf *b) {
  int index = b->blockno % NBUCKET;
  acquire(&bcache.lock[index]);
  b->refcnt--;
  release(&bcache.lock[index]);
}

最后 kalloctest 和 bcachetest 还有 usertest 全部通过。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++学习笔记(三十三):c++ 宏定义

本节对c的宏定义进行描述。c使用预处理器来对宏进行操作&#xff0c;我们可以写一些宏来替换代码中的问题&#xff0c;c的宏是以#开头&#xff0c;预处理器会将所有的宏先进行处理&#xff0c;之后在通过编译器进行编译。宏简单说就是文本替换&#xff0c;可以替换代码中的任何…

高级分布式系统-第15讲 分布式机器学习--概念与学习框架

高级分布式系统汇总&#xff1a;高级分布式系统目录汇总-CSDN博客 分布式机器学习的概念 人工智能蓬勃发展的原因&#xff1a;“大” 大数据&#xff1a;为人工智能技术的发展奠定了坚实的物质基础。 大规模机器学习模型&#xff1a;具备超强的表达能力&#xff0c;可以解决…

CMU15-445-Spring-2023-Project #3 - 前置知识(lec10-14)

Lecture #10_ Sorting & Aggregation Algorithms Query Plan 数据库系统会将 SQL 编译成查询计划。查询计划是一棵运算符树。 Sorting DBMS 需要对数据进行排序&#xff0c;因为根据关系模型&#xff0c;表中的tuple没有特定的顺序。排序使用 ORDER BY、GROUP BY、JOIN…

如何在Windows 10/11的防火墙中禁止和允许某个应用程序,这里提供详细步骤

想阻止应用程序访问互联网吗&#xff1f;以下是如何通过简单的步骤阻止和允许Windows防火墙中的程序。​ 一般来说&#xff0c;大多数用户永远不需要担心应用程序访问互联网。然而&#xff0c;在某些情况下&#xff0c;你需要限制应用程序访问互联网。 例如&#xff0c;有问题…

高级定时器

本节主要介绍以下内容&#xff1a; 定时器简介 高级定时器功能框图讲解 一、定时器简介 定时器功能 &#xff1a;定时、输出比较、输入捕获、断路输入 定时器分类 &#xff1a;基本定时器、通用定时器、高级定时器 定时器资源 &#xff1a;F103有2个高级定时器、4个通…

Vue学习笔记3--全局事件总线

Vue学习笔记3—全局事件总线 1.全局事件总线可以实现任意组件间通信 X需具备的条件&#xff1a; 所有的组件都要能看见X可以调用$on $off $emitVue.prototype.x {a:1, b:2} 可以被所有组件看见VueComponent.protoype.proto Vue.prototype组件实例对象(vc)可以访问到Vue原型上…

Redis实现全局唯一Id

一、全局唯一ID 每个店铺都可以发布优惠券&#xff1a; 当用户抢购时&#xff0c;就会生成订单并保存到tb_voucher_order这张表中&#xff0c;而订单表如果使用数据库自增ID就存在一些问题&#xff1a; id的规律性太明显 受单表数据量的限制 场景分析&#xff1a;如果我们的…

10.9.2 std::function 代替函数指针 Page182~183

std::function是一个模板类&#xff0c;基本可作为函数指针的代替品&#xff0c;具备更多功能&#xff0c;特别是与函数对象及bind配合使用。使用std::function时&#xff0c;需要添加头文件 #include <functional> 1.定义函数指针 18行&#xff0c;定义了一个函数指针类…

大语言模型面试问题【持续更新中】

自己在看面经中遇到的一些面试题&#xff0c;结合自己和理解进行了一下整理。 transformer中求和与归一化中“求和”是什么意思&#xff1f; 求和的意思就是残差层求和&#xff0c;原本的等式为y H(x)转化为y x H(x)&#xff0c;这样做的目的是防止网络层数的加深而造成的梯…

Visual Studio 2022 成功配置QT5.12.10

目录 下载并安装Visual Studio 2022 Qt5.12.10下载 Qt5.12.10安装 Qt VS Tools for Visual Studio 2022下载 Visual Studio 2022配置 测试 下载并安装Visual Studio 2022 下载社区版并安装&#xff0c;这个比较快。 Qt5.12.10下载 官网下载很慢&#xff0c;还不如百度网…

在visual studio中调试时无法查看std::wstring

1.问题 在调试的时候发现std::wstring类型的变量查看不了&#xff0c;会显示(error)|0&#xff0c;百思不得其解。 2.解决方法 参考的&#xff1a;vs2015调试时无法显示QString变量的值&#xff0c;只显示地址_vs调试qstring的时候如何查看字符串-CSDN博客 在工具/选项/调试…

Linux网络文件共享服务2(基于NFC)

目录 一、初步了解NFS 1、概念 2、工作原理 3、NFS优势和缺点 3.1优点 3.2缺点 二、NFS软件介绍 2.1 NFS共享配置文件格式 2.2 NFS工具 2.2.1 exportfs 2.2.2 showmount 2.2.3 mount.nfs 三、NFS服务部署 1、服务器部署配置 2、客户端配置 3、服务测试 一、初步…

解决英特尔无线网卡WiFi或者蓝牙突然消失问题

winR&#xff0c;输入“devmgmt.msc”&#xff0c;检查设备管理器中的无线网卡驱动是否安装好。 访问https://www.intel.cn/content/www/cn/zh/download/19351/windows-10-and-windows-11-wi-fi-drivers-for-intel-wireless-adapters.html下载对应系统版本的英特尔无线网卡WiFi…

VMware虚拟机忘记密码操作方法

下面已openEuler虚拟机为例&#xff1a; 1、点击重启时&#xff0c;一直按esc&#xff08;鼠标点击一下&#xff0c;确保鼠标在你的虚拟机里面&#xff09; 2、一直到进入到如下页面按e键&#xff08;可能会略有不同&#xff09; 3、按e键后跳转到如下页面 4、在该页面输入 in…

软件测试|使用selenium实现文件上传

简介 文件上传是我们web自动化测试工作中经常使用的场景&#xff0c;selenium同样也是支持我们实现自动化的文件上传操作&#xff0c;本文就来给大家介绍一下selenium如何实现自动化文件上传。 input标签文件上传 一般情况下&#xff0c;文件上传的按钮是一个<input>标…

纯c++简易的迷宫小游戏

一个用c写的黑框框迷宫 适合新手入门学习 也适合大学生小作业 下面附上代码 总体思路 初始化游戏界面&#xff1a;设置迷宫的大小&#xff08;WIDTH和HEIGH&#xff09;&#xff0c;生成迷宫地图&#xff08;map&#xff09;&#xff0c;包括墙壁、空地、起点和终点。显示…

设计模式-委托模式

设计模式专栏 模式介绍模式特点应用场景委托模式在GUI编程场景的应用代码示例Java实现委托模式Python实现委托模式 委托模式在spring中的应用 模式介绍 委托模式是一种软件设计模式&#xff0c;其中一个对象&#xff08;委托对象&#xff09;将某些操作委托给另一个对象&#…

关于queue的两道编程题

在蓝桥网站上面的两道题 https://www.lanqiao.cn/problems/1113/learning/?page1&first_category_id1&problem_id1113 #include <bits/stdc.h> using namespace std; int main() {queue<string> V,N;int m;cin>>m;while(m--){string op;cin>>…

5文件操作

包含头文件<fstream> 操作文件三大类&#xff1a; ofstream : 写文件ifstream &#xff1a;读文件fstream : 读写文件 5.1文本文件 -文件以ascii的形式存储在计算机中 5.1.1写文件 步骤&#xff1a; 包含头文件 #include "fstream"创建流对象 ofs…

Edge浏览器入门

关于作者&#xff1a; CSDN内容合伙人、技术专家&#xff0c; 从零开始做日活千万级APP&#xff0c;带领团队单日营收超千万。 专注于分享各领域原创系列文章 &#xff0c;擅长java后端、移动开发、商业化变现、人工智能等&#xff0c;希望大家多多支持。 目录 一、导读二、概览…