XV6中的锁:MIT6.s081/6.828 lectrue10:Locking 以及 Lab8 locks Part1 心得
这节课程的内容是锁(本节只讨论最基础的锁)。其实锁本身就是一个很简单的概念,这里的简单包括 3 点:
- 概念简单,和实际生活中的锁可以类比,不像学习虚拟内存时,现实世界中几乎没有可以类比的对象,所以即使这节课偏向于理论介绍,也一点不会感觉晦涩。
 - 使用简单,几乎所有的锁都实现了非常简单的api,acquire 就是获取锁,release 就是释放锁,作为用户,用起来锁来非常简单(甚至比你现实中拿钥匙开一把锁还要简单)
 - 实现简单,本节展示了如何实现一个最基础的 spin lock,实现起来甚至一句话就可以概括:将软件锁转换为硬件锁,利用CPU的低层指令atomic swap 实现锁。
 
至于CPU底层如何保证 atomic,这个话题已经超出了本节的讨论范围,甚至我的看法更加激进:如果不是CPU的设计者,压根没必要了解这一点,因为即使详尽如 CPU 的 data sheet,也不会和用户说明 atomic swap 是如何实现的,不过想要了解的童鞋看这里:atomic的底层实现
这里顺便说一下,Lab8本来是后面的lab,但是和这一节相关度较高,所以拿到这里讲解,Lab8有两个part,其中part1要求重新设计内存分配器,而part涉及到文件系统,所以讲到文件系统时再来讲解。
锁
为什么要用锁
首先考虑一个问题,单线程的性能是由什么决定:是CPU的时钟频率,频率越快,执行一条指令所需的时间越短,从下图可以看出,大概从2000年开始:
- CPU的时钟频率就没有再增加过了(绿线)。
 - 这样的结果是,CPU的单线程性能达到了一个极限并且也没有再增加过(蓝线)。
 - 但是另一方面,CPU中的晶体管数量在持续的增加 (深红色线)。
 - 所以现在不能通过使用单核来让代码运行的更快,要想运行的更快,唯一的选择就是使用多个CPU核。所以从2000年开始,处理器上核的数量开始在增加(黑线)。
 

但是多核带来的问题就是会有多个进程访问共享的数据结构,所以需要锁,锁可以保证共享数据的正确性
锁是怎么生效的
锁就是一个对象,有一个结构体叫做 lock,它包含了一些字段,这些字段中维护了锁的状态,最典型也是最基本的一个锁应该有以下三个字段:是否加锁?锁的名字?哪个cpu核持有锁?
// Mutual exclusion lock.
struct spinlock {
  uint locked;       // Is the lock held?
  // For debugging:
  char *name;        // Name of lock.
  struct cpu *cpu;   // The cpu holding the lock.
};
锁应该有非常直观的API:
- acquire,接收指向lock的指针作为参数,表示要获取这把锁。acquire确保了在任何时间,只会有一个进程能够成功的获取锁。
 - release,也接收指向lock的指针作为参数,表示要释放这把锁。在同一时间尝试获取锁的其他进程需要等待,直到持有锁的进程对锁调用release。
 
锁的acquire和release之间的代码,通常被称为critical section,称为临界区,而锁就是保护这部分代码的原子性的
这里的关键问题是锁到底是怎么做到原子性的?即锁为什么不能被两个进程同时 acquire?要解答这个问题。就需要深入源码,看一看 acquire是如何实现的
// Acquire the lock.
// Loops (spins) until the lock is acquired.
void
acquire(struct spinlock *lk)
{
  push_off(); // disable interrupts to avoid deadlock.
  if(holding(lk))
    panic("acquire");
  // On RISC-V, sync_lock_test_and_set turns into an atomic swap:
  //   a5 = 1
  //   s1 = &lk->locked
  //   amoswap.w.aq a5, a5, (s1)
  while(__sync_lock_test_and_set(&lk->locked, 1) != 0)
    ;
  // Tell the C compiler and the processor to not move loads or stores
  // past this point, to ensure that the critical section's memory
  // references happen strictly after the lock is acquired.
  // On RISC-V, this emits a fence instruction.
  __sync_synchronize();
  // Record info about lock acquisition for holding() and debugging.
  lk->cpu = mycpu();
}
这里的关键是:while(__sync_lock_test_and_set(&lk->locked, 1) != 0),注释中已经给出了提示,__sync_lock_test_and_set 是一个C 标准库中的函数,用来实现 atomic swap,或者说用来实现原子性的 test and set。
这里是需要重点解释的地方:
首先解释什么是 test and set,就和他的名字一样,test locked 是否为1,是 1 则说明该锁已经被获取,是 0 则说明该锁没有被获取,就可以获取到锁,所谓获取锁就是 set locked 的值为 1,这样其他进程就 test 到 locked 值为1,从而无法获取锁。
然后解释 atomic swap 是什么,这是一种底层硬件指令,几乎所有真实的CPU都会支持这个指令,可以在硬件层面保证“原子交换”。在RISC-V中,这个特殊的指令叫 amoswap(atomic memory swap),这个指令接收3个参数,分别是address,寄存器r1,寄存器r2。这条指令可以会先锁定住address,将address中的数据保存在一个临时变量中(tmp),之后将r1中的数据写入到address中,之后再将tmp变量中的数据写入到r2中,最后再对于地址解锁。
相当于原子性地实现了 address->r2, r1->addresstest and set 和 atomic swap 什么关系?答案就是 atomic swap 就可以实现 test and set :
看下面的 test and set 函数,做的工作就是 *ptr->old, new->*ptr,然后返回old,这个模式是不是和上面的 address->r2, r1->address 一模一样?test 就是将locked的值取出赋值到old并返回,set就是将new值set到locked中
//test-and-set的C代码模拟
int TestAndSet(int *ptr, int new) {
int old = *ptr; //抓取旧值
*ptr = new; //设置新值
return old; //返回旧值
} typedef struct __lock_t {
int locked;
} lock_t; void init (lock_t *lock) {
lock->flag = 0;
} void lock(lock_t *lock) {
// 如果为 1,说明锁被其他进程获取
// 如果为 0,说明该进程可以获取锁
while (TestAndSet(&lock->flag, 1) == 1)
; //spin-wait (do noting)
} void unlock (lock_t *lock) {
lock->flag = 0;
}
什么时候用锁
什么时候才必须要加锁呢?课程给出了一个非常保守同时也是非常简单的规则:如果两个进程访问了一个共享的数据结构,并且其中一个进程会更新共享的数据结构,那么就需要对于这个共享的数据结构加锁。
死锁的场景
死锁的两个常见情形:
- 重入导致死锁:即多次 acquire 同一个锁,一个死锁的最简单的场景就是:首先acquire一个锁,然后进入到critical section;在critical section中,再acquire同一个锁;第二个acquire必须要等到第一个acquire状态被release了才能继续执行,但是不继续执行的话又走不到第一个release,所以程序就一直卡在这了。这就是一个死锁。(但如果是可重入锁的话,这种情况就不会死锁)
 - 相互等待导致死锁:两个进程,两个锁,A进程需要获取锁1和2,B进程需要获取锁2和1,A获取了1,B获取了2;A要获取2,B要获取1,相互等待导致死锁
 
避免死锁的方法:使用锁定策略(locking strategy),对锁进行排序,所有的操作都必须以相同的顺序获取锁。
Lab8 Part1 Memory allocator
这个lab要求重新设计内存分配器,原来的内存分配器实现如下,可以看到使用了一个链表 freelist 来保存所有的空闲物理 page
// kernel/kalloc.c
void *
kalloc(void)
{
  struct run *r;
  acquire(&kmem.lock);
  r = kmem.freelist;
  if(r)
    kmem.freelist = r->next;
  release(&kmem.lock);
  if(r)
    memset((char*)r, 5, PGSIZE); // fill with junk
  return (void*)r;
}
下图解释了 kalloc 的运行过程,如果有进程需要内存空间,就调用kalloc函数,kalloc就会返回一个page的地址

可以看到 kalloc 加了一把大锁(即在kalloc首尾acquire和release锁),来保护所有进程共享的数据结构 freelist,但是这样做会有效率问题,所以lab要求在保证正确性的前提下优化这把锁的性能。优化方式也给出来了,只需要为每个cpu core维护一个freelist就好了,然后每个freelist都有自己的锁,之所以还要设计锁,是因为在一个 CPU core 的 freelist 中空闲页不足的情况下,仍需要从其他 CPU 的 freelist 中“偷”内存页,所以一个 CPU core 的 freelist 还可能在“偷”内存页的时候被其他 CPU core 访问,故仍然需要使用单独的锁来保护每个 CPU core 的 freelist。
至于怎么偷?雨露均沾地偷?还是全部从一个 other cpu core 中偷?lab就没有要求了,自己设计即可。
这里的一个关键、也是guide中没有提到的就是:要清楚首次初始化时所有的page都被分配给了一个 core,所以其他 core 首次调用 kalloc 时一定会执行 steal 动作,这里给出修改后的kalloc代码:
void *
kalloc(void)
{
  struct run *r;
  push_off();
  int cpu_id = cpuid();
  acquire(&kmem[cpu_id].lock);
  //steal pages form other cpu's freelist
  if(!kmem[cpu_id].freelist) {
    int steal_page = 32;
    for(int i = 0; i < NCPU; i++) {
      if(i == cpu_id) continue;
      acquire(&kmem[i].lock);
      struct run *rr = kmem[i].freelist;
      while(rr && steal_page) {//该cpu的freelist有page,且steal_page不为0
        kmem[i].freelist = rr->next;
        rr->next = kmem[cpu_id].freelist;
        kmem[cpu_id].freelist = rr;
        rr = kmem[i].freelist;
        steal_page--;
      }
      release(&kmem[i].lock);
      if(steal_page == 0) break;
    }
  }
  r = kmem[cpu_id].freelist;
  if(r)
    kmem[cpu_id].freelist = r->next;
  release(&kmem[cpu_id].lock);
  if(r)
    memset((char*)r, 5, PGSIZE); // fill with junk
  pop_off();
  return (void*)r;
}
获得更好的阅读体验,这里是我的博客,欢迎访问:byFMH - 博客园
所有代码见:我的GitHub实现(记得切换到相应分支)
XV6中的锁:MIT6.s081/6.828 lectrue10:Locking 以及 Lab8 locks Part1 心得的更多相关文章
- MIT6.S081/6.828 实验1:Lab Unix Utilities
		
Mit6.828/6.S081 fall 2019的Lab1是Unix utilities,主要内容为利用xv6的系统调用实现sleep.pingpong.primes.find和xargs等工具.本 ...
 - 【MIT6.S081/6.828】手把手教你搭建开发环境
		
目录 1. 简介 2. 安装ubuntu20.04 3. 更换源 3.1 更换/etc/apt/sources.list文件里的源 3.2 备份源列表 3.3 打开sources.list文件修改 3 ...
 - MIT 6.S081 聊聊xv6中的文件系统(上)
		
前言 Lab一做一晚上,blog一写能写两天,比做Lab的时间还长( 这篇博文是半夜才写完的,本来打算写完后立刻发出来,但由于今天发现白天发博点击量会高点,就睡了一觉后才发(几十的点击量也是点击量啊T ...
 - Hibernate中的锁机制
		
锁机制:是数据库为了保证数据的一致性<一个事务的各种操作不相互影响>而使各种共享资源在被并发访问访问变得有序所设计的一种规则,用来保证在当前用户进行操作数据的时候其他的用户不能对同一数据进 ...
 - 谈谈iOS中的锁
		
1 前言 近日工作不是太忙,刚好有时间了解一些其他东西,本来打算今天上午去体检,但是看看天气还是明天再去吧,也有很大一个原因:就是周六没有预约上!闲话少说,这里简单对锁来个简单介绍分享. 2 目录 第 ...
 - 【转】T-SQL查询进阶—理解SQL Server中的锁
		
简介 在SQL Server中,每一个查询都会找到最短路径实现自己的目标.如果数据库只接受一个连接一次只执行一个查询.那么查询当然是要多快好省的完成工作.但对于大多数数据库来说是需要同时处理多个查 ...
 - SQL server 2005中的锁(1)
		
在之前的一片随笔中,简单的说了一下SQL Server中的隔离级别.而SQL Server的隔离级别是通过锁的机制来实现的.现在深入一下,谈谈SQL Server中的锁. 开始之前,先要定义一下前提: ...
 - Atitit.软件与编程语言中的锁机制原理attilax总结
		
Atitit.软件与编程语言中的锁机制原理attilax总结 1. 用途 (Db,业务数据加锁,并发操作加锁.1 2. 锁得类型 排它锁 "互斥锁 共享锁 乐观锁与悲观锁1 2.1. 自旋锁 ...
 - SQL Server中的锁的简单学习
		
简介 在SQL Server中,每一个查询都会找到最短路径实现自己的目标.如果数据库只接受一个连接一次只执行一个查询.那么查询当然是要多快好省的完成工作.但对于大多数数据库来说是需要同时处理多个查询的 ...
 - MySQL数据库InnoDB存储引擎中的锁机制
		
MySQL数据库InnoDB存储引擎中的锁机制 http://www.uml.org.cn/sjjm/201205302.asp 00 – 基本概念 当并发事务同时访问一个资源的时候,有可能 ...
 
随机推荐
- [SWPUCTF 2021 新生赛]PseudoProtocols
			
[SWPUCTF 2021 新生赛]PseudoProtocols 一.题目 二.WP 1.打开题目,发现提示我们是否能找到hint.php,并且发现URL有参数wllm.所以我们尝试利用PHP伪协议 ...
 - 《Linux的文件目录类指令 20条》
			
文件目录类的指令 1.pwd指令 查看当前目录 2.ls 指令 查看当前目录所有内容信息 ls -a 显示当前目录所有的文件和目录,包括隐藏的 ls -l 以列表的方式显示信息 ls -al或la ...
 - vue+iview 动态调整Table的列顺序
			
需求:因table列太多,且每个部门关注的信息不一样,拖来拖去不方便观看,客户想让Table列可以拖动,且可以保存顺序. 但是搞动态拖动太难了,我不会,于是改为操作columns数据 思路: < ...
 - Golang 协程/线程/进程 区别以及 GMP 详解
			
Golang 协程/线程/进程 区别详解 转载请注明来源:https://janrs.com/mffp 概念 进程 每个进程都有自己的独立内存空间,拥有自己独立的地址空间.独立的堆和栈,既不共享堆,亦 ...
 - 曲线艺术编程 coding curves 第五章 谐波图形(谐振图形) HARMONOGRAPHS
			
原作:Keith Peters https://www.bit-101.com/blog/2022/11/coding-curves/ 译者:池中物王二狗(sheldon) blog: http:// ...
 - 统信UOS系统开发笔记(三):从Qt源码编译安装之编译安装Qt5.12.8
			
前言 上一篇,是使用Qt提供的安装包安装的,有些场景需要使用到自己编译的Qt,所以本篇如何在统信UOS系统上编译Qt5.12.8源码. 统信UOS系统版本 系统版本: Qt源码下载 ...
 - malloc/free 与 new/delete
			
malloc/free与new/delete表达式的区别?相同点: 都是用来申请堆空间不同点: 1. malloc/free是库函数; new/delete是表达式 2. malloc开空间时,并不会 ...
 - FreeFileSync结合任务计划实现T级数据的全量备份和每日十几G数据的增量自动备份
			
1. 背景 公司现有nas存储中有共计1.8T左右的文件数据(一般是pdf.excel.图片.压缩文件等等格式),因为nas无法做备份:担心后面nas出现故障造成数据丢失,现急需一个解决方案实现如下目 ...
 - 为什么从 MVC 到 DDD,架构的本质是什么?
			
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 本文来自于小傅哥新编写的<Java简明教程>系列内容,本教程意在于通过简单.明了. ...
 - 为控制器生成OpenAPI注释
			
非常喜欢. NET 的 /// 注释,写代码的时候就顺道完成写文档的过程,简直不要太爽了. ASP. NET CORE 也是一样的,通过 Swagger 工具,可以自动生成 API 的接口文档(Ope ...