xv6 lec10 Multiprocessors and locking


https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/lec10-multiprocessors-and-locking

10.1 为什么要使用锁?

  • 一个进程可以运行在多个CPU核上(虽然有CPU的亲和性),所以XV6存在很多共享的数据结构
  • 一个矛盾点在于:并行的目的是为了获取高性能,而在不同的CPU核上执行系统调用,访问共享数据,又需要是使用??,这又限制了性能
  • race condition导致程序出错,需要加锁避免

10.2 锁如何避免race condition?

  • ??是一个对象,在内核中有一个结构体叫做lock,其中的字段维护了??的状态,有两个api,lock和release
  • acquire与release之间的区域被称为critical section,
  • 内核中存在一个big kernel lock,基本上所有的系统调用都被被这把大锁保护,以此达到一个应用程序串行执行多个系统调用。
  • XV6具有多把??,以此达到不同的并发程度

10.3 什么时候使用锁?

  • 不能够自动加锁,

10.4 锁的特性和死锁

  • ??有三种作用:
    • ??可以避免丢失更新。
    • ??可以使多个操作具有原子性,打包多个操作
    • ??可以维护共享数据结构的不变性。共享数据结构如果不被任何进程修改的话是会保持不变的。如果某个进程acquire了锁并且做了一些更新操作,共享数据的不变性暂时会被破坏,但是在release锁之后,数据的不变性又恢复了。
  • ??的不正确使用会带来deadlock,aa型可以通过触发panic处理,ab型通过??排序解决

10.5 锁与性能

  • 对于??与性能的取舍在于:先对模块加上coarse-grained lock,之后在测试,是否需要重构程序减少??的粒度

10.6 XV6中UART模块对于锁的使用

  • UART模块是一个coarse-grained lock的设计,
  • uartputc函数中??的使用
  • 在UART驱动的bottom部分调用的uartintr函数会acquire??,同时在UART驱动的top的部分譬如uartputc函数也会acquire??,所以top与bottom可以并行无错的进行。

10.7 自旋锁(Spin lock)的实现(一)

  • 对于自旋锁的acquire,其中有一个死循环,循环判断locked是否为0,如果为0,那么表明当前??没有持有者,那么当前调用acquire的进程可以通过将locked字段置1来获取??,这里对于locked字段的获取存在race,这里通过硬件原子指令amoswap(atomic memory swap),这个指令接收3个参数,分别是address,寄存器r1,寄存器r2。这条指令会先锁定住address,将address中的数据保存在一个临时变量中(tmp),之后将r1中的数据写入到地址中,之后再将保存在临时变量中的数据写入到r2中,最后再对于地址解锁。基本思想是对于地址加锁,读出数据,写入新数据,然后返回旧数据
  • spinlock中存的是一个locked“布尔变量”,其他两个是调试信息
  • acquire函数,这个while循环里就是atomic swap操作atomic swap操作就是
  • release函数就是将0写回s1,注意这里的写回也是原子操作,而不是单纯的store

10.8 自旋锁(Spin lock)的实现(二)

  • sotre指令不是原子指令
  • acquire一开始会关中断,如果不关中断,如果当一个CPU执行UART驱动的top部分时,也就是uartputc,获取了??,这个时候来了一个中断,如果没有关中断,这个时候CPU去处理中断,而中断处理程序,也就是驱动中断的bottom部分uartintr函数,这个函数里会去获取同一把??,这时发生了aa型deadlock,这个时候os会触发panic。针对这种情况,需要在acquire中关闭中断,在release的结束位置再次打开,
  • memeory ordering,编译器会优化,改变指令执行顺序,使用memory fence或者叫做synchronize指令解决这个问题
  • 即使是一个CPU,也要处理原子操作,也就是开关中断