您的位置:首页 > 运维架构 > Linux

[笔记]《Linux内核设计与实现》第九、十章内核同步

2017-01-12 14:48 260 查看
目前正在通读《Linux内核设计与实现》一书,本文是对第九章<内核同步介绍>、第十章<内核同步方法>的总结。
1.前置概念:

临界区:也叫临界段,就是访问和操作共享数据的代码段。

竞争条件:race conditions,发生了多个执行线程处于同一个临界区中同时执行的情况,我们就称它是竞争条件。

同步:synchronization,避免并发和防止竞争条件,就称为同步。

伪并发:多个任务不真是同时发生的,而是相互交叉进行。

死锁:所有线程都在相互等待,但它们又不会释放已经占有的锁。

自死锁:一个执行线程试图去获得一个自己已经持有的锁,它将不得不等待锁被释放,但是因为在忙等待,所以自己也永远不会有机会释放这个锁。

递归锁:同一个锁可以被一个执行线程多次请求。递归锁可以用来防止自死锁现象。

争用:指锁的争用,lock contention。是指当锁正在被占用时,有其他线程试图获得该锁。

2.并发执行的原因:

1)中断

2)软中断和tasklet

3)内核抢占:内核具有抢占性,内核中的任务可能会被另一任务抢占。

4)睡眠及与用户空间的同步

5)对称多处理:两个或多个处理器可以同时执行代码

3.加锁的难点:

如何辨别出真正需要共享的数据和相应的临界点

4.给谁加锁:

要给数据而不是代码加锁

5.锁的作用:

是使得程序以串行方式对资源进行访问。

6.加锁方案:

加锁粒度的控制,过粗,会导致锁被高度争用,严重降低系统性能;过细,只会增加复杂度加大开销。

原则:设计初期加锁方案应该力求简单,仅当需要时再进一步细化加锁方案,精髓在于力求简单。

7.锁的类型:

各种锁机制之间的区别主要在于:当锁已经被其他线程持有,因而不可用时的行为表现。

锁是用原子操作实现的,而原子操作不存在竞争。

7.1.原子操作

原子操作确保指令执行期间不被打断,要么全部执行完,要么不执行。

真正的原子操作需要的是--所有中间结果都正确无误。

内核提供了两组原子操作接口:原子整数操作和原子位操作。

原子操作通常是内联函数,往往通过内嵌汇编指令来实现。

能用原子操作时,就尽量不要使用复杂的加锁机制。

7.2.自旋锁

spin lock

内核中最普通的锁,最多只能被一个可执行线程持有。争用时忙等待,即一直进行忙循环,特别浪费处理器时间。

其初衷:在短期间内进行轻量级加锁。

在现在的抢占式内核中,锁的持有时间等价于系统的调度等待时间。

自旋锁的特点:

1)在Linux中,自旋锁不可递归;

2)自旋锁可以使用在中断处理程序中。

读写自旋锁:也叫共享/排斥锁,或者并发/排斥锁。多个读操作可以安全地获得同一个读锁,而写操作为了互斥访问只能等待,只有所有的读者都释放了锁,写操作才能获得锁。

7.3.信号量

由Dijkstra在1968年提出,荷兰人。信号量支持两个原子操作P()和V(),这两个名字来自荷兰语Proberen和Vershogen。前者叫做测试操作(字面意思是探查),后者叫做增加操作。

后来的系统把两种操作分别叫做down()和up()。

down()操作通过对信号量计数减1来请求获得一个信号量。

up()操作用来释放信号量。

是一种睡眠锁,当一个从操作试图获得一个不可用的信号量时,信号量会将其推进一个等待队列,然后让其睡眠。这时处理器能重获自由,去执行其他代码。

7.3.1.信号量的特点:

1)适用于锁会被长时间持有的情况;

2)只能在进程上下文中使用,不能在中断上下文中使用;

7.3.2.计数信号量和二值信号量

信号量可以同时允许任意数量的锁的持有者,这个值称为“使用者数量”(usage count)或者简单地叫数量。

二值信号量:也叫互斥信号量,计数为1,在同一时刻仅允许有一个锁持有者。

计数信号量:counting semaphone,count值为大于1的非0值。

读写信号量:是种互斥信号量,只要没有写者,并发持有读锁的读者数不限。相反,在没有读者时,只有唯一的写者可以获得写锁。

7.4.互斥体

mutex,互斥体,一个简单睡眠锁,指的是任何可以睡眠的强制互斥锁。其行为和互斥信号量类似,但是操作接口更简单,实现也更高效,而且使用限制更强。

它不同于信号量,它仅仅是实现了Dijkstra设计初衷中的最基本的行为。

互斥体的特点:

1)任何时刻中只有一个任务可以持有mutex;

2)给mutex上锁者必须负责给其再解锁;最常用的方式是:在同一个上下文中上锁和解锁。

3)当持有一个mutex时,进程不可以退出。

4)mutex不能在中断或者下半部中使用;

5)mutex只能通过官方API管理

mutex和信号量很相似,内核中两者共存会令人混淆。使用规范是:首选mutex,只有碰到特殊场合(一般是很底层代码)才会需要使用信号量。

7.5.完成变量

completion variable,当一个任务完成任务后,就会使用“完成变量”去唤醒在等待的任务。是一种代替信号量的一个简单的解决办法。

7.6.大内核锁(BKL)

是一种全局自旋锁。使用它主要是为了方便实现从Linux最初的SMP过渡到细粒度加锁机制。

BKL的特点:

1)持有BKL的任务仍然可以睡眠,但不是说睡眠是安全的。

2)BKL是一种递归锁;一个进程可以多次请求一个锁,但是必须调用同样次数的解锁操作。在最后一个解锁操作完成后,锁才会被释放。

3)BKL只可以用在进程上下文中。

4)BKL在被持有时同样会禁止内核抢占。

5)新的用户不允许使用BKL。

多数情况下,BKL更像是保护代码而不是保护数据。

7.7.顺序锁

简称seq锁,是在2.6版本内核中才引入的一种新型锁。用于读写共享数据。

实现这种锁主要依靠一个序列计数器。当有疑义的数据被写入时,会得到一个锁,并且序列值会增加。

只要没有其他写者,写锁总是能够被成功获得。读者不会影响写锁,这与读写自旋锁及信号量一样。不同的是不允许读者让写者饥饿,写优先于读。

7.8.禁止抢占

preempt_disable()/preempt_enable()成对调用,禁止/释放内核抢占,支持嵌套调用

7.9.屏障

barriers,用于确保顺序的指令称为屏障。

程序代码的顺序,在处理器执行阶段,会在取指令和分派对时,把表面上看似无关的指令按自认为最好的顺序排列。

大多数情况下,这样的排序是最佳的。尽管有时候程序员知道什么是最好的顺序。

barrier()方法可以防止编译器跨屏障对载入或存储操作进行优化。

内存屏障指令(如rmb()/wmb()/mb())可以完成编译器屏障的功能,但是编译器屏障要比内存屏障轻量得多。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: