【MIT 6.S081】2020, 实验记录(8),Lab: locks

news2024/9/26 3:22:52

目录

    • Task 1:Memory allocator (moderate)</font>
    • Task 2:Buffer cache (hard)</font>

Task 1:Memory allocator (moderate)

这个任务就是练习将一把大锁拆分为多个小锁,同时可以更加深入地理解 memory allocator 运行的原理。

task 的内容是:原来的 memory allocator(kernel/kalloc.c)在分配内存和释放内存时只有一把大锁(也就是原来代码中的 kmem.lock),一把大锁带来的问题就是锁竞争的问题严重。为了减少锁的竞争,现在需要将这把大锁拆成多个小锁,每个 CPU 都有一把锁和一个 freelist,当需要分配内存时,优先从本 CPU 自己所对应的 freelist 中分配,当自己的 freelist 为空时,才会去其他 CPU 的 freelist 中寻找空闲的 page。

所以我们需要做的就是,为每个 CPU 设置一个 lock,并将原来的 freelist 拆分成多个,并分给所有 CPU 来用,从而提高并发度。

这里我采用的思路是,假如一共有 NCPU 个 CPU,那按照内存页号平分给这些 CPU。比如有 15 个内存页,那 0 ~ 4 就分给第一个 CPU 的 freelist,5 ~ 9 就分给第二个 CPU 的 freelist,10 ~ 14 就分给第三个 CPU 的 freelist。这样,根据一个物理地址,我们就知道这是应该放在哪个 freelist 中。

代码实现过程(所有修改均在 kernel/kalloc.c 中):

首先需要为每个 CPU 分配一个 lock 和 freelist,所以将之前的结构体 kmem 改为了一个数组:

kmems
这样 i 号 CPU 就对应 kmems[i]

然后我们在 kinit() 中初始化各个 lock:

kinit
为了平分所有内存页,我们应该知道每个 CPU 能分到多少个内存页,也就是总内存页的数量除以 CPU 的数量,用一个变量 freelist_size 来表示:

freelist_size
然后在 freerange() 函数中遍历所有内存 page 时计算出 freelist_size

freerange
有了 freelist_size,那我们就可以根据一个物理地址计算出这个内存属于哪个 freelist 了,这个转换的逻辑封装为函数:

// 计算物理地址 pa 应该由哪个 kmem 的 freelist 来管
int kmem_number(void* pa) {
  return ( (uint64)pa - (uint64)end ) / PGSIZE / freelist_size;
}

修改 kfree() 函数的实现,在归还某个内存 page 时,需要先计算出这个 page 属于哪个 freelist,然后再将其加入到那个 freelist 中:

kfree 实现

接下来修改 kalloc() 的实现,在这里我们分配时,优先从当前 CPU 自己的 freelist 中寻找一个空闲 page,当自己没有空闲 page 时,需要再从其他人手中找一个出来,我们将这个寻找存在空闲 page 的 freelist 的逻辑封装为 find_freelist() 函数,它寻找含有空闲 page 的 freelist 所在的 kmem:

struct Kmem*
find_freelist()
{
  int cpu_id = cpuid();
  struct Kmem* kmem = kmems + cpu_id;

  // 先检查自己的 freelist 是否能够分配
  acquire(&kmem->lock);
  if (kmem->freelist) {
    return kmem;
  }
  release(&kmem->lock);

  // 如果自己的 freelist 空了的话,就从通过**遍历**来从其他人那里找
  for (int i = 0; i < NCPU; i++) {
    if (i == cpu_id) {
      continue;
    }
    kmem = kmems + i;
    acquire(&kmem->lock);
    if (kmem->freelist) {
      return kmem;
    }
    release(&kmem->lock);
  }

  return 0;
}

如果没找到就返回 0。

注意 find_freelist() 在实现时有个大坑!因为它在遍历各个 CPU 所对应的 freelist 时需要加锁,这种依次加锁很可能产生死锁的问题,为了规避死锁,我们应该按一定的顺序来依次加锁解锁,不可以产生交叉(比如一个进程先加了 A 再准备加 B,另一个进程有可能先加了 B 再准备加 A),在这里,我们决定在遍历时,无论自己的 cpu id 是多少,都从 0 号 kmem 开始遍历,而不是从自己的 cpu 号开始进行环形遍历。

有了这个函数,kalloc() 就好实现了:

kalloc

Task 2:Buffer cache (hard)

这个题目依然也是需要将一把大锁拆分成小锁从而提高并发度的任务。

bcache 用来缓存文件系统的 block,一个 bcache 由多个 buf 组成,每个 buf 可以存放一个 block,原有实现将这些 buf 串联成一个 linked list,并利用这个 linked list 来使用 LRU 算法淘汰出无用的 buf cache。

task 的内容是,原有的 bcache 在分配 block cache 时会使用一把大锁 bcache.lock,无论是寻找 cache、分配新 cache 等都是加这一把锁,从而导致 bcache 部分具有严重的锁竞争。在之前的实现中,所有的 block buf 被串联成 linked list,现在我们需要将其改成 hash table 的形式,这个 hash table 由多个 buckets 组成,每个 bucket 有一个 buf 指针的 linked list 并对应一个 lock,bucket 记录了 block 号哈希值正好映射到这个 bucket 号的所有 buf 的指针,图解如下:

buf 数组图解
buf 数组声明在 bcache 中,同时有一个 hash table 使用拉链法来记录了 bucketno -> bufs 的映射。所以,对于一个 block,对其 block 号进行取模得到 bucket 号,再从 hash table 的这个 bucket 中存储的 linked list 寻找出一个 buf 来做 block 的缓存。

代码实现如下:

首先将 bcache 结构体中的 head 删掉,只保留 lock 和 buf 数组:

bcache

之后初始化一个 hash table,也就是 buckets 数组,bucket 数量选择质数 13:

hash table

在初始化函数 binit() 中,实现对 bcache 和 hash table 中的锁和相关指针的初始化:

binit

然后我们实现一个辅助函数 replace_buffer() 用来在 buffer 中填充我们新的 block 缓存的相关信息:

replace_buffer

在实现一个辅助函数 bucket_add() 用来向一个 bucket 加入一个 buffer,这里的实现是将其加到链表的第一个元素上(也就是 head 的 next):

bucket_add

然后就是实现 bget() 函数,也就是传入一个 block 信息,让我们找到一个存放这个 block 的 buf cache,这里分两种情况:

  • case 1:已经在 cache 中,所以我们需要找到 block 所在的 bucket,并从中找到存放这个 block 缓存的 buf
  • case 2:cache 中没有这个 block 的缓存,需要我们从现有的 cache 中根据 LRU 策略淘汰出不用的 buf 作为新 block 的缓存

这个关键函数的代码实现如下,已经做了详细的注释:

// 在一个 buffer 中填入缓存块的信息
void
replace_buffer(struct buf* buffer, uint dev, uint blockno, uint tick) {
  buffer->dev = dev;
  buffer->blockno = blockno;
  buffer->tick = tick;
  buffer->valid = 0;  // 表示数据还未写入 buffer 的 data 字段中
  buffer->refcnt = 1;
}

void
bucket_add(struct BcacheBucket *bucket, struct buf *buffer) {
  buffer->next = bucket->head.next;
  bucket->head.next->prev = buffer;
  bucket->head.next = buffer;
  buffer->prev = &bucket->head;
}

// 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 bucketNo = blockno % N_BUCKETS;  // 根据 blockno 计算 hash table 的 bucket 序号
  struct BcacheBucket *bucket = hash_table + bucketNo;
  acquire(&bucket->lock);
  
  // 检查这个 block 是否存在于 cache 中
  for (b = bucket->head.next; b != &bucket->head; b = b->next) {  // 遍历这个 bucket 的链表
    // 如果没找到:
    if (b->dev != dev || b->blockno != blockno) {
      continue;
    }
    // 如果找到了:
    b->tick = ticks;
    b->refcnt++;
    release(&bucket->lock);
    acquiresleep(&b->lock);
    return b;
  }

  // 如果 bucket 中没有找到 cache,则需要从 kcache 中找一块未使用的 buffer
  acquire(&bcache.lock);
  struct buf* victim = 0;  // 根据 LRU 策略所决定淘汰的 buffer
  for (b = bcache.buf; b < bcache.buf + NBUF; b++) {  // 遍历所有 buffer,寻找一个未使用的 buffer
    // 如果 buffer 不能使用:
    if (b->refcnt != 0) {
      continue;
    }
    // 如果 buffer 可以使用,则根据时间戳来决定是否将它作为 victim
    else {
      if (victim == 0 || victim->tick > b->tick) {
        victim = b;
      }
    }
  }

  // 是否能够找到 victim?
  if (victim == 0) {
    panic("bget: no buffers");
  }

  // 将 victim 的 buffer 中填入数据,并将其移动到 bucket 中
  if (victim->tick == 0) {  // 如果 victim 还未加入到 hash table 中
    replace_buffer(victim, dev, blockno, ticks);
    bucket_add(bucket, victim);
  } else if ((victim->blockno % N_BUCKETS) != bucketNo) {  // 如果 victim 之前所在的 bucket 与现在需要加入的 bucket 不同的话
    struct BcacheBucket *old_bucket = &hash_table[victim->blockno % N_BUCKETS];
    acquire(&old_bucket->lock);
    replace_buffer(victim, dev, blockno, ticks);
    victim->prev->next = victim->next;
    victim->next->prev = victim->prev;
    release(&old_bucket->lock);
    bucket_add(bucket, victim);
  } else {  // 如果 victim 之前就是在现在需要加入的 bucket 的话
    replace_buffer(victim, dev, blockno, ticks);
  }

  // 释放掉相关的 lock
  release(&bcache.lock);
  release(&bucket->lock);
  acquiresleep(&victim->lock);

  return victim;
}

bget() 实现之后,剩下的几个函数就容易修改了,大致就是将原来对大锁的加解锁改成”寻找 buf 对应的 bucket 的小锁在加解锁“:

在这里插入图片描述

完成上述修改后,即可通过测试:

kcachetest 通过

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

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

相关文章

PY32离线烧录器功能介绍,可批量烧录,支持PY32系列多款单片机

PY32离线烧录器可以对PY系列单片机进行批量烧录&#xff0c;现支持PY32F002A/002B/002/003/030/071/072/040/403/303芯片各封装和XL2409&#xff0c;XL32F001/003等芯片。PY32离线烧录器需要搭配上位机软件才能使用&#xff0c;上位机软件在我们官网&#xff08;www.xinlinggo.…

JVM基础篇

什么是JVM java虚拟机 JVM的功能 1.解释和运行 对字节码文件中的指令&#xff0c;实时的解释成机器码&#xff0c;让计算机执行 2.内存管理 自动为对象、方法等分配内存 自动的垃圾回收机制&#xff0c;回收不再使用的对象&#xff08;c不会自动回收&#xff0c;相当于降…

QT 如何防止 QTextEdit 自动滚动到最下方

在往QTextEdit里面append字符串时&#xff0c;如果超出其高度&#xff0c;默认会自动滚动到QTextEdit最下方。但是有些场景可能想从文本最开始的地方展示&#xff0c;那么就需要禁止自动滚动。 我们可以在append之后&#xff0c;添加如下代码&#xff1a; //设置编辑框的光标位…

指针的函数传参的详细讲解(超详细)

如果对指针基础知识已经有可以直接跳到 函数的指针传参与解引用&#xff0c;哪里不明白可以评论&#xff0c;随时解答。 目录 所以就有了一句话&#xff1a;指针就是地址&#xff0c;地址就是指针 对于指针在C语言中&#xff0c;指针类型就是数据类型&#xff0c;是给编译器…

PHP极简网盘系统源码 轻量级文件管理与共享系统网站源码

PHP极简网盘系统源码 轻量级文件管理与共享系统网站源码 极简网盘是一个轻量级文件管理与共享系统&#xff0c;支持多用户&#xff0c;可充当网盘程序&#xff0c;程序无需数据库 安装步骤&#xff1a; 1.建议安装在apache环境下&#xff0c;并确保.htaccess可用 2.解压文件…

论文阅读——RingMo

RingMo: A Remote Sensing Foundation Model With Masked Image Modeling 与自然场景相比&#xff0c;RS图像存在以下困难。 1&#xff09;分辨率和方位范围大&#xff1a;受遥感传感器的影响&#xff0c;图像具有多种空间分辨率。此外&#xff0c;与自然图像的实例通常由于重…

【Python】Leetcode 240. 搜索二维矩阵 II - 削减矩阵+递归,击败88%

描述 搜索二维矩阵 II 编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。 该矩阵具有以下特性&#xff1a; 每行的元素从左到右升序排列。 每列的元素从上到下升序排列。 思路 确定左右及上下限&#xff0c;削减矩阵&#xff0c;递归。 注意判断四个端…

15届蓝桥杯第一期模拟赛所有题目解析

文章目录 &#x1f9e1;&#x1f9e1;t1_字母数&#x1f9e1;&#x1f9e1;问题描述思路代码 &#x1f9e1;&#x1f9e1;t2_大乘积&#x1f9e1;&#x1f9e1;问题描述思路代码 &#x1f9e1;&#x1f9e1;t3_星期几&#x1f9e1;&#x1f9e1;问题描述思路代码 &#x1f9e1;…

Spring基础——使用注解开发SpringMVC

目录 配置SpringMVC的初始化信息配置ServletWebApplicationContext配置RootWebApplicationContext配置ServletContext 创建Controller控制器配置Controller响应路径接收用户传递参数接收JSON数据接收简单类型对象封装参数 接收数组类型 Restful 文章源码仓库&#xff1a;Spring…

大模型笔记:吴恩达 ChatGPT Prompt Engineering for Developers(1) prompt的基本原则和策略

1 intro 基础大模型 VS 用指令tune 过的大模型 基础大模型 只会对prompt的文本进行续写 所以当你向模型发问的时候&#xff0c;它往往会像复读机一样续写几个问题这是因为在它见过的语料库文本&#xff08;通常大多来自互联网&#xff09;中&#xff0c;通常会连续列举出N个问…

Flask vs. Django:选择适合你的Web开发框架【第134篇—Flask vs. Django】

Flask vs. Django&#xff1a;选择适合你的Web开发框架 在选择一个适合你项目的Web开发框架时&#xff0c;常常会遇到 Flask 和 Django 这两个流行的选择。两者都有其优势和适用场景&#xff0c;本文将探讨它们的特点&#xff0c;并通过代码实例和解析来帮助你更好地做出选择。…

环形链表的起点——细节讲解

对于一个环形链表&#xff0c;我们要找到他的起点。可以通过如下推导。 我们设置两个快慢指针&#xff0c;相遇的点为X. 到起点的距离是T&#xff0c;圈长是C&#xff0c;第一次相交的点是X &#xff08;TX&#xff09;2TNCX 化出来TN*C-X 也就是说我们把一个节点放头部重新遍…

SQLiteC/C++接口详细介绍之sqlite3类(六)

快速前往文章列表&#xff1a;SQLite—系列文章目录 上一篇&#xff1a;SQLiteC/C接口详细介绍之sqlite3类&#xff08;五&#xff09; 下一篇&#xff1a;SQLiteC/C接口详细介绍之sqlite3类&#xff08;七&#xff09; 19. sqlite3_changes与sqlite3_changes64 是SQLite中用…

手机安装Kali Linux

在数字化时代&#xff0c;信息安全和隐私保护显得尤为重要。Kali Linux&#xff0c;作为一款专业的渗透测试和安全审计工具&#xff0c;因其强大的功能和丰富的资源库而受到广大安全研究者和爱好者的青睐。然而&#xff0c;我们通常只能在传统的电脑设备上安装和使用Kali Linux…

外卖小程序-购物车模块表结构设计和后端代码

表结构设计 添加购物车代码 Service public class ShoppingCartServiceImpl implements ShoppingCartService {Autowiredprivate ShoppingCartMapper shoppingCartMapper;Autowiredprivate DishMapper dishMapper;Autowiredprivate SetmealMapper setmealMapper;/*** 添加购物…

在Linux/Ubuntu/Debian中使用7z压缩和解压文件

要在 Ubuntu 上使用 7-Zip 创建 7z 存档文件&#xff0c;你可以使用“7z”命令行工具。 操作方法如下&#xff1a; 安装 p7zip&#xff1a; 如果你尚未在 Ubuntu 系统上安装 p7zip&#xff08;7-Zip 的命令行版本&#xff09;&#xff0c;你可以使用以下命令安装它&#xff1a;…

某夕夕商品数据抓取逆向之webpack扣取

逆向网址 aHR0cHM6Ly93d3cucGluZHVvZHVvLmNvbQ 逆向链接 aHR0cHM6Ly93d3cucGluZHVvZHVvLmNvbS9ob21lL2JveXNoaXJ0 逆向接口 aHR0cHM6Ly9hcGl2Mi5waW5kdW9kdW8uY29tL2FwaS9naW5kZXgvdGYvcXVlcnlfdGZfZ29vZHNfaW5mbw 逆向过程 请求方式&#xff1a;GET 参数构成 【anti_content】…

PHP中的反序列化漏洞

PHP中的反序列化漏洞 目录 PHP 中的序列化与反序列化 概述 序列化 基本类型的序列化 对象的序列化 反序列化 示例序列化与反序列化 反序列化漏洞 - PHP 中的魔术方法 - Typecho_v1.0 中的反序列化漏洞 POP链的构造思路 pop链案例 反序列化逃逸 字符串逃逸&#xff…

GoLang:云原生时代致力于构建高性能服务器的后端语言

Go语言的介绍 概念 Golang&#xff08;也被称为Go&#xff09;是一种编程语言&#xff0c;由Google于2007年开始设计和开发&#xff0c;并于2009年首次公开发布。Golang是一种静态类型、编译型的语言&#xff0c;旨在提供高效和可靠的软件开发体验。它具有简洁的语法、高效的编…

C# wpf 使用GDI实现截屏

wpf截屏系列 第一章 使用GDI实现截屏&#xff08;本章&#xff09; 第二章 使用GDI实现截屏 第三章 使用DockPanel制作截屏框 第四章 实现截屏框热键截屏 第五章 实现截屏框实时截屏 第六章 使用ffmpeg命令行实现录屏 文章目录 wpf截屏系列前言一、导入gdi32方法一、NuGet获取…