Lecture 10

有一段时间没看了 忘了好多

why need it

多个CPU核的并行计算可以带来倍数级的提升 但是假如一个程序运行在多个核上 会同时带来一些竞态的问题 并且需要内核能够正确的处理它们

有很多老生常谈的环境 例如说什么生产者消费者模式 共享内存的情景下 我们都需要使用锁来保证多个核之间 数据的一致性

简而言之 多核并行计算带来了效率提升 也带来了多核时的并发控制成本 和编码难度

实际的情况有些令人失望,因为我们想要通过并行来获得高性能,我们想要并行的在不同的CPU核上执行系统调用,但是如果这些系统调用使用了共享的数据,我们又需要使用锁,而锁又会使得这些系统调用串行执行,所以最后锁反过来又限制了性能。

那么除了使用锁还有其他的方法可以提高性能吗 肯定是有的 但是之前的技术发展路线已经解释了 为什么锁的使用频率会越来越高

text

从这图可以看到 从2000年开始:

  • CPU时钟频率就没有再增加过了(绿)

  • 意味着 CPU单线程下的性能达到了一个极限(蓝)

于是就往增加并行计算能力这个方向思考了

  • CPU的晶体管数量增加(深红)

  • CPU数量增加(黑)

粗略的理解就是 同一份工作 单个人的工作效率已经到瓶颈了 无法再提升了 所以此时唯一提高效率的方法就是找多一个人一起工作(并行)

同理,某个应用程序如果想要提升性能 必须要依赖于多核 即其所依赖的操作系统也需要高效的在多个CPU核上运行

而锁 就是保证在这多个CPU核上运行时的正确性

如果在kfree()的时候 取消掉上锁 下锁的语句 此时会发生什么?

1
2
3
4
5
6
7
8
9
void kfree(void *pa){
struct run *r;
...
acquire(&kmem.lock);
r->next = kmem.freelist;
kmem.freelist = r;
release(&kmem.lock);
}

当并发发生的时候 多个程序同时执行 可能会导致他们的freelist都仅加上了自己的空余空间 即race condition 竞态问题

而通过锁 就可以保证该中间语句执行的原子性 他们要么就一起执行 要么就一条都不会执行 被称为 critical section

他所起到的作用就是序列化代码的执行 让本该并行执行的代码串行

当多个CPU都遇到锁的时候 此时的的并行度不再取决于CPU核的数量 而是取决于此时锁的数量

这个问题其实很简单 如何理解?假如内核中只有一把big kernel lock。某个地方取了锁 此时其他的地方无论执行到什么地步 无论是否会导致竞态问题的产生 都只会阻塞在这

换句话来说 也就是通过多把锁 可以换取一定程度上的并行度 但其本身就需要耗费一定的资源罢了

经典两害取其轻~

when use it

标准回答:当涉及到临界资源的时候要加锁

但是有些情况下 有可能即使多个进程同时操作同一个数据结构 在某些场合不加锁也可以正常工作 称为lock-free program

特性

作用:

  • 避免丢失更新 (race condition导致的丢失更新)

  • 打包多个操作 使其具有原子性

  • 维护共享数据结构的不变性 (在修改操作进行的时候 不变性暂时被破坏 修改前和修改后都存有)

死锁

也是老生常谈的一个问题 资源争用的时候 如果锁住的对方所需要的临界资源 此时都需要阻塞直至对方放弃 此时产生死锁

也有一个最简单的触发例子 一个地方acquire()一把锁之后 再次acquire()同一把锁 就会导致死锁(被自己死锁了)

如何解决死锁?

这里没有什么方法论 只是从设计思路上给了一些建议

确定锁对象的顺序

在获取锁的时候 我门根据这个顺序总是先获取考前的目录的锁 本质意思就是将程序中所有涉及到并行计算的地方(所有用得到锁的地方) 都拓扑出来 根据这个拓扑的关系图对锁的使用顺序进行排序

但是这个就违背了代码抽象的原则 因为正常的要求下 代码抽象是要求A模块完全不知道相邻模块 B模块的实现

但是此处为了实现锁排序 就需要暴露这些信息

同时 这里所指的排序 指的是有几率会产生死锁为单位的锁集合 并不是指所有锁

一辈子的不会干扰到的锁们无需排序 可正常的并行运行

自旋锁内部实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 获取当前锁
// 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");

// 开始自旋 类似atom swap 直至成功获取到锁
while (__sync_lock_test_and_set(&lk->locked, 1) != 0)
;

// 给cpu发送指令 禁止此时发生的store等指令
__sync_synchronize();

// Record info about lock acquisition for holding() and debugging.
// 记录当前获得锁的是哪个核
lk->cpu = mycpu();
}

// Release the lock.
void release(struct spinlock* lk) {
if (!holding(lk))
panic("release");

lk->cpu = 0;
// 给cpu发送指令 禁止此时发生的store等指令
__sync_synchronize();
// 再次进行一个atom release本质上也是atom swap
__sync_lock_release(&lk->locked);

pop_off();
}

看着代结构是很简单的 自旋的锁的美学

一致轮询直至得到锁为止

这个获取效率是很高的 但是经典两面性

它本身空转会消耗CPU资源 为此也有很多的措施优化

例如说锁本身的细粒度 空转间的sleep 获取失败之后的租约机制 etc…