惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
人人都是产品经理
人人都是产品经理
Cisco Talos Blog
Cisco Talos Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
V
V2EX
博客园 - 三生石上(FineUI控件)
Martin Fowler
Martin Fowler
WordPress大学
WordPress大学
D
Docker
S
SegmentFault 最新的问题
博客园 - 聂微东
美团技术团队
Apple Machine Learning Research
Apple Machine Learning Research
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Last Week in AI
Last Week in AI
M
MIT News - Artificial intelligence
F
Fortinet All Blogs
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The GitHub Blog
The GitHub Blog
GbyAI
GbyAI
L
LangChain Blog
Vercel News
Vercel News
博客园 - 叶小钗
MongoDB | Blog
MongoDB | Blog
Stack Overflow Blog
Stack Overflow Blog
H
Help Net Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Cloudflare Blog
Engineering at Meta
Engineering at Meta
T
Threat Research - Cisco Blogs
T
Threatpost
Scott Helme
Scott Helme
T
Tailwind CSS Blog
Latest news
Latest news
Stack Overflow Blog
Stack Overflow Blog
Blog — PlanetScale
Blog — PlanetScale
The Register - Security
The Register - Security
罗磊的独立博客
P
Proofpoint News Feed
腾讯CDC
S
Schneier on Security
雷峰网
雷峰网
A
About on SuperTechFans
T
Tenable Blog
F
Full Disclosure
Cyberwarzone
Cyberwarzone
博客园_首页
有赞技术团队
有赞技术团队
K
Kaspersky official blog

文章列表

游戏玩后感:ReLief:献给亲爱的你 我的周边(谷子)分享 游戏玩后感:Kanon 简谱:致真实的你 《Rust中常见的有关生命周期的误解》学习笔记 简谱:StarMap 简谱:かく咲きたらばいと恋ひめやも 简谱:东风 简谱:无法诉说的思念 简谱:Girlish 游戏玩后感:时钟机关的Layline 简谱:风之琶音 简谱:星空的记忆 简谱:因为遇见了你 简谱:月童 番茄简谱脚本转调器 游戏玩后感:青空下的约定:Refine 游戏玩后感:在这苍穹展翅 书籍读后感:控制论与科学方法论 游戏玩后感:恋爱表达式 游戏玩后感:樱之诗 MLIR-tutorial学习笔记 游戏玩后感:潜伏之赤途 游戏玩后感:纯爱咖啡厅:帕露菲重制版 游戏玩后感:智以泪聚 游戏玩后感:初雪樱 游戏玩后感:告别回忆:从今以后 游戏玩后感:梦灯花 游戏玩后感:金辉恋曲四重奏 游戏玩后感:五彩斑斓的世界 昇腾310P使用记录 游戏玩后感:AIR 游戏玩后感:弹丸论破 游戏玩后感:流景之海的艾佩莉亚 Xilinx_HLS上板过程记录 游戏玩后感:告别回忆2 游戏玩后感:恋爱绮谭 Faiss和Rapidsai_Raft使用记录 游戏玩后感:近月少女的礼仪 游戏玩后感:樱色之云,绯色之恋 游戏玩后感:幸运草的约定 游戏玩后感:星之梦、候鸟和丸子与银河龙 游戏玩后感:白色相簿2 Windows上使用VTune分析PyTorchExtension调用的Cpp程序 SpinalHDL上板过程记录 游戏玩后感:仰望夜空的星辰 最简单的算卦方法之一:梅花易数法 游戏玩后感:苍之彼方的四重奏 krkr引擎解包工具介绍 自定义CUDA实现PyTorch算子的四种简单方法 游戏玩后感:星空的记忆 游戏玩后感:9nine 游戏玩后感:AtriMyDearMoments 游戏玩后感:极限脱出 游戏玩后感:魔女的夜宴 SSH实现多跳代理 动漫观后感:向山进发 flv重封装H264、AAC流 动漫观后感:夏日重现 CSP模板 游戏玩后感:海沙风云 动漫观后感:灵能百分百 游戏玩后感:交响乐之雨 游戏玩后感:爱上火车LastRun 游戏玩后感:LittleBustersEX 游戏玩后感:SummerPockets 游戏玩后感:逆转裁判 Ultra96V2开发板简单使用 SpinalWorkshop实验笔记(三) SpinalWorkshop实验笔记(二) SpinalWorkshop实验笔记(一) PYNQ开发板上使用USB声卡+OSS兼容层播放音频 TestOS移植K210开发板 rCore-Tutorial-Book-v3学习笔记(七) 动漫观后感:凉宫春日的忧郁 rCore-Tutorial-Book-v3学习笔记(♭七) rCore-Tutorial-Book-v3学习笔记(六) rCore-Tutorial-Book-v3学习笔记(五) rCore-Tutorial-Book-v3学习笔记(四) rCore-Tutorial-Book-v3学习笔记(三) rCore-Tutorial-Book-v3学习笔记(二) rCore-Tutorial-Book-v3学习笔记(一) 游戏玩后感:RewritePlus MIT-6.S081-2020实验(xv6-riscv64)十一:net MIT-6.S081-2020实验(xv6-riscv64)十:mmap MIT-6.S081-2020实验(xv6-riscv64)九:fs MIT-6.S081-2020实验(xv6-riscv64)七:thread MIT-6.S081-2020实验(xv6-riscv64)六:cow MIT-6.S081-2020实验(xv6-riscv64)五:lazy MIT-6.S081-2020实验(xv6-riscv64)四:traps MIT-6.S081-2020实验(xv6-riscv64)三:pgtbl MIT-6.S081-2020实验(xv6-riscv64)二:syscall 动漫观后感:吹响吧上低音号 MIT-6.S081-2020实验(xv6-riscv64)一:util 快速生成网络mp4视频缩略图技术 用plantuml画图示例 QQ缩略图和大图不同实现 Python制作字符图片 动漫观后感:命运石之门 Unity3D+Post_Processing_Stack_V2自定义后处理效果研究
MIT-6.S081-2020实验(xv6-riscv64)八:lock
VnYzm · 2021-01-08 · via

实验文档

概述

这次实验主要涉及锁在内核的应用,没有用到什么特别的理论知识,但是编程的时候陷阱重重,要么资源竞争,要么死锁,和实验三差不多,非常考验耐心和细心。

内容

Memory allocator

这个任务要求给物理内存分配程序重新设计锁,使得等待锁时的阻塞尽量少。可以按CPU的数量将空闲内存分组,分配内存的时候优先从当前所用CPU所管理的空闲内存中分配,如果没有则从其他CPU的空闲内存中获取,这样就可以把原来的锁拆开,每个CPU各自处理自己的空闲内存时只要锁上自己的锁就行了:

void
kinit()
{
  for (int i = 0; i < NCPU; i++) initlock(&kmem[i].lock, "kmem");
  freerange(end, (void*)PHYSTOP);
}
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;

  int cpu_id; push_off(); cpu_id = cpuid(); pop_off();
  acquire(&kmem[cpu_id].lock);
  r->next = kmem[cpu_id].freelist;
  kmem[cpu_id].freelist = r;
  release(&kmem[cpu_id].lock);
}

由于kinit这个函数不是并行的,所以一开始会将所有空闲内存都交给一个CPU管理。

void *
kalloc(void)
{
  struct run *r;

  int cpu_id; push_off(); cpu_id = cpuid(); pop_off();
  acquire(&kmem[cpu_id].lock);
  r = kmem[cpu_id].freelist;
  if(r) {
      kmem[cpu_id].freelist = r->next;
      release(&kmem[cpu_id].lock);
  } else {
      release(&kmem[cpu_id].lock);
      for (int i = 0; i < NCPU; i++) if (i != cpu_id) {
          acquire(&kmem[i].lock);
          r = kmem[i].freelist;
          if (r) {
              kmem[i].freelist = r->next;
              release(&kmem[i].lock);
              break;
          } else release(&kmem[i].lock);
      }
  }
  ......

Buffer cache

这个任务要求给硬盘缓存分配程序重新设计锁,使得等待锁时的阻塞尽量少。但是,因为硬盘缓存包含遍历查找操作,即查找当前硬盘块是否已被缓存,显然这时就不能把缓存也按CPU进行分配,加上这个任务的操作也比较复杂,因此比上个任务多了很多问题。

实验文档给出的分配方式是对硬盘块号进行取模哈希,数据结构如下:

struct {
  struct spinlock lock;
  struct spinlock block[BNUM];
  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[BNUM];
} bcache;

这里保留了原来的锁是因为在不要求修改的bpin、bunpin函数中使用了,但要小心的是,既然用到了新定义的锁,那么在整个实验中所有相关的代码都必须用新定义的锁,不能用原来的锁。

void
binit(void)                                                                     {
  struct buf *b;

  initlock(&bcache.lock, "bcache");

  // Create linked list of buffers
  for (int i = 0; i < BNUM; i++) {
      initlock(bcache.block + i, "bcache");
      bcache.head[i].prev = &bcache.head[i];
      bcache.head[i].next = &bcache.head[i];
  }
  for(b = bcache.buf; b < bcache.buf+NBUF; b++){
    b->next = bcache.head[0].next;
    b->prev = &bcache.head[0];
    initsleeplock(&b->lock, "buffer");
    bcache.head[0].next->prev = b;
    bcache.head[0].next = b;
  }
}

因为一开始所有的缓存对应硬盘块号都是0,所以把它们都放到0号桶里。

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

  releasesleep(&b->lock);

  int entry = b->blockno % BNUM;
  acquire(bcache.block + entry);
  b->refcnt--;
  if (b->refcnt == 0) b->ticks = ticks;
  release(bcache.block + entry);
}

释放缓存时只要获取块对应的桶对应的锁即可,由于原来的代码在释放缓存时会将缓存插在head的下一个节点,按照原来程序的思路head的下一个节点是目前最近使用的缓存,所以把查找最久未使用缓存的方式改成了查找最前时间戳后,这里也应该更新时间戳。

最麻烦的是bget,这里拆成两部分,第一部分是待查找磁盘块已被缓存的情况:

  int entry = blockno % BNUM;
  acquire(bcache.block + entry);

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

也是只要获取待查找块对应的桶对应的锁即可,但要注意一个问题,这个锁必须一直持有到整个函数运行结束,不能在中间释放了再重新获取,也就是说:

获得锁
查找缓存但没找到
释放锁
可以并行的其他操作
获得锁
更新一个可用的缓存
释放锁

不等价于

获得锁
查找缓存但没找到
可以并行的其他操作
更新一个可用的缓存
释放锁

而且前者是错的,后者是错的。理由是如果中间释放了锁,当前进程可能会让出控制权执行别的进程,那么就会出现一个问题,比如A进程查找1号缓存,查不到,释放了锁,程序转到B进程,它也查找1号缓存,查不到,释放了锁而刚好B立刻又获得了锁,继续往下更新了一个可用的缓存,释放锁,这是A获得锁,继续往下更新了一个可用的缓存,释放锁。现在缓存中就有两个编号完全相同且引用数都为1的缓存了,这个是不允许的行为,可能会出现各种问题,而且这些问题的发生全靠运气,有时不出错,有时这个panic,有时那个panic,非常难调。

第二部分是待查找磁盘块未被缓存的情况:

  for (int i = (entry + 1) % BNUM; i != entry; i = (i + 1) % BNUM) {
      uint minticks = 0x3fffffff; struct buf *minbuf = 0;
      acquire(bcache.block + i);
      for(b = bcache.head[i].prev; b != &bcache.head[i]; b = b->prev)
          if (b->refcnt == 0 && b->ticks < minticks) {
              minticks = b->ticks; minbuf = b;
          }
      if (minbuf != 0) {
          minbuf->dev = dev;
          minbuf->blockno = blockno;
          minbuf->valid = 0;
          minbuf->refcnt = 1;
          minbuf->next->prev = minbuf->prev;
          minbuf->prev->next = minbuf->next;
          minbuf->next = bcache.head[entry].next;
          minbuf->prev = &bcache.head[entry];
          bcache.head[entry].next->prev = minbuf;
          bcache.head[entry].next = minbuf;
          release(bcache.block + i);
          release(bcache.block + entry);
          acquiresleep(&minbuf->lock);
          return minbuf;
      }
       release(bcache.block + i);
  }
  panic("bget: no buffers");
}

一开始我寻找可更新缓存的办法是直接遍历整个数组,为了防止竞争,需要在遍历前把所有的桶锁起来,然而这样会发生死锁,即假设处理0号桶的进程运行到这里,把0号桶锁了,准备获取1号桶的锁,与此同时处理1号桶的进程运行到这里,把1号桶锁了,准备获取0号桶的锁,这样就死锁了。仔细分析原因,发现只要锁桶的顺序是乱序的,都可能发生死锁,这里的解决方法是使用“资源有序分配法”,就如上面的循环,从当前桶的下一个桶往上遍历到当前桶的前一个桶(循环遍历),保证了顺序,就不会死锁了。另外一个需要小心的是链表的操作,双向链表确实很容易写错,需要谨慎。


总结一下,这个实验和上一个实验相比,感觉更考验并行思维,主要体现在锁的应用,上个实验的后两个任务和这个实验比真的是小巫见大巫了,可能设计实验的老师主要还是想让学生熟悉一下pthread才弄那两个任务,毕竟pthread太常用了。目前觉得系统编程最难的就是四个问题:缺页错误(这里指的是编程逻辑的错误导致的错误)、内存泄漏、资源竞争、死锁,都是极为难以检查难以调试的。