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

目录

    • 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 中找一块未使用的 bufferacquire(&bcache.lock);struct buf* victim = 0;  // 根据 LRU 策略所决定淘汰的 bufferfor (b = bcache.buf; b < bcache.buf + NBUF; b++) {  // 遍历所有 buffer,寻找一个未使用的 buffer// 如果 buffer 不能使用:if (b->refcnt != 0) {continue;}// 如果 buffer 可以使用,则根据时间戳来决定是否将它作为 victimelse {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);}// 释放掉相关的 lockrelease(&bcache.lock);release(&bucket->lock);acquiresleep(&victim->lock);return victim;
}

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

在这里插入图片描述

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

kcachetest 通过

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

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

相关文章

【项目研究】MeetingServer项目小研究

目录 项目研究 【vscode】 unable to connect to github.com-CSDN博客 解决跨域问题 项目截图 websocket-webrtc-chat 项目研究 完整参考 https://github.com/nnn149/MeetingServer C:\Users\duanxiongwen\Downloads\MeetingServer-master\MeetingServer-master meeting.…

边缘计算与物联网的核心 —— 低功耗芯片

一、低功耗芯片 在边缘计算与物联网&#xff08;IoT&#xff09;中&#xff0c;低功耗芯片扮演了至关重要的角色&#xff0c;主要体现在以下几个方面&#xff1a; 延长设备寿命&#xff1a;物联网设备通常需要部署在难以更换电池或不方便进行频繁维护的环境中&#xff0c;比如…

固态硬盘有缓存和没缓存有什么区别

固态硬盘&#xff08;SSD&#xff09;已经成为现代计算机的重要组成部分&#xff0c;它们提供了比传统机械硬盘更快的读写速度&#xff0c;从而显著提升了操作系统的运行速度和应用程序的加载效率。 其中&#xff0c;缓存&#xff08;Cache&#xff09;是固态硬盘中一个重要的…

基于word2vec 和 fast-pytorch-kmeans 的文本聚类实现,利用GPU加速提高聚类速度

文章目录 简介GPU加速 代码实现kmeans聚类结果kmeans 绘图函数相关资料参考 简介 本文使用text2vec模型&#xff0c;把文本转成向量。使用text2vec提供的训练好的模型权重进行文本编码&#xff0c;不重新训练word2vec模型。 直接用训练好的模型权重&#xff0c;方便又快捷 完整…

软考高级:遗留系统演化策略(集成、淘汰、改造、继承)概念和例题

作者&#xff1a;明明如月学长&#xff0c; CSDN 博客专家&#xff0c;大厂高级 Java 工程师&#xff0c;《性能优化方法论》作者、《解锁大厂思维&#xff1a;剖析《阿里巴巴Java开发手册》》、《再学经典&#xff1a;《Effective Java》独家解析》专栏作者。 热门文章推荐&am…

【刷题训练】Leetcode415.字符串相加

字符串相加 题目要求 示例 1&#xff1a; 输入&#xff1a;num1 “11”, num2 “123” 输出&#xff1a;“134” 示例 2&#xff1a; 输入&#xff1a;num1 “456”, num2 “77” 输出&#xff1a;“533” 示例 3&#xff1a; 输入&#xff1a;num1 “0”, num2 “0”…

【计算机视觉】一、计算机视觉概述

文章目录 一、计算机视觉二、计算机视觉与其它学科领域的关系1、图像处理2、计算机图形学3、模式识别4、人工智能&#xff08;AI&#xff09;5、神经生理学与认知科学 三、计算机视觉的应用1. 人脸识别2. 目标检测3. 图像生成4. 城市建模5. 电影特效6. 体感游戏动作捕捉7. 虚拟…

java数据结构与算法刷题-----LeetCode47. 全排列 II

java数据结构与算法刷题目录&#xff08;剑指Offer、LeetCode、ACM&#xff09;-----主目录-----持续更新(进不去说明我没写完)&#xff1a;https://blog.csdn.net/grd_java/article/details/123063846 文章目录 1. 暴力回溯2. 分区法回溯 此题为46题的衍生题&#xff0c;在46题…

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

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

PHP序列化基础知识储备

一、序列化与反序列化 1、概念 PHP中的序列化是指将复杂的数据类型转换为可存储或可传输的字符串&#xff0c;而反序列化则是将这些字符串重新转换回原来的数据类型。 序列化通常使用 serialize() 函数完成&#xff0c;它可以将数组、对象、字符串等复杂数据类型压缩到一个字…

Infineon_TC264智能车代码初探及C语言深度学习(二)

本篇文章记录我在智能车竞赛中&#xff0c;对 Infineon_TC264 这款芯片的底层库函数的学习分析。通过深入地对其库函数进行分析&#xff0c;C语言深入的知识得以再次在编程中呈现和运用。故觉得很有必要在此进行记录分享一下。 目录 ​编辑 一、代码段分析 NO.1 指向结构体…

CSDN 编辑器设置图片缩放和居中

CSDN 编辑器设置图片缩放和居中 文章目录 CSDN 编辑器设置图片缩放和居中对齐方式比例缩放 对齐方式 Markdown 编辑器插入图片的代码格式为 ![图片描述](图片路径)CSDN 的 Markdown 编辑器中插入图片&#xff0c;默认都是左对齐&#xff0c;需要设置居中对齐的话&#xff0c;…

QTextToSpeech的使用——Qt

前言 之前随便看了几眼QTextToSpeech的帮助就封装使用了&#xff0c;达到了效果就没再管了&#xff0c;最近需要在上面加功能&#xff08;变换语速&#xff09;&#xff0c;就写了个小Demo后&#xff0c;发现不对劲了。 出现的问题 场景 写了个队列添加到语音播放子线程中&a…

HTTPS基础

目录 HTTPS简介 HTTP与HTTPS的区别 CA证书 案例 服务器生成私钥与证书 查看证书和私钥存放路径 Cockpit(图像化服务管理工具) HTTPS简介 超文本传输协议HTTP协议被用于在Web浏览器和网站服务器之间传递信息。HTTP协议以明文方式发送内容&#xff0c;不提供任何方式的数据加密&…

C++——类和对象(1)

1. 面向对象和面向过程对比 当涉及到编程范式时&#xff0c;两个主要的方法是面向对象编程&#xff08;Object-Oriented Programming&#xff0c;OOP&#xff09;和面向过程编程&#xff08;Procedural Programming&#xff09;。这两种编程范式在解决问题和组织代码时有着不同…

COX回归影响因素分析的基本过程与方法

在科学研究中&#xff0c;经常遇到分类的结局&#xff0c;主要是二分类结局&#xff08;阴性/阳性&#xff1b;生存/死亡&#xff09;&#xff0c;研究者可以通过logistic回归来探讨影响结局的因素&#xff0c;但很多时候logistic回归方法无法使用。如比较两种手段治疗新冠肺炎…

Annaconda环境下ChromeDriver配置及爬虫编写

Anaconda环境的chromedriver安装配置_anaconda 配置chromedriver-CSDN博客 Chromedriver驱动( 121.0.6167.85 ) - 知乎 下载好的驱动文件解压&#xff0c;将exe程序复制到Annaconda/Scripts目录以及Chrome/Application目录下 注意要提前pip install selenium包才能运行成功&a…

BEV系列一:BEV介绍和常用BEV算法简介

BEV系列一&#xff1a;BEV介绍和常用BEV算法简介 自动驾驶最全学习资料获取&#xff1a;链接

Linux操作系统——线程概念

1.什么是线程&#xff1f; 在一个程序里的一个执行路线就叫做线程&#xff08;thread&#xff09;。更准确的定义是&#xff1a;线程是“一个进程内部的控制序列”一切进程至少都有一个执行线程线程在进程内部运行&#xff0c;本质是在进程地址空间内运行在Linux系统中&#x…

openGauss学习笔记-242 openGauss性能调优-SQL调优-典型SQL调优点-SQL自诊断

文章目录 openGauss学习笔记-242 openGauss性能调优-SQL调优-典型SQL调优点-SQL自诊断242.1 SQL自诊断242.1.1 告警场景242.1.2 规格约束 openGauss学习笔记-242 openGauss性能调优-SQL调优-典型SQL调优点-SQL自诊断 SQL调优是一个不断分析与尝试的过程&#xff1a;试跑Query&…