并发共享数据是造成不稳定的一类隐患,而且这种错误一般难以跟踪和调度,因此必须给内核同步以高度重视.
8.1 临界区和竞争条件
临界区:访问和操作共享数据的代码段.
同步:避免并发和防止竞争条件的策略.
原子操作:确保每个事务的完整操作,是同步一种笼统策略说法.
8.2 加锁
锁提供的就是这样一种机制:它如同一把门锁,门后的房间可想象成一个临界区.在一个指定的时间内,房间里只有一个执行线程存在,当一个线程进入房间后,它会锁住身后的门;当它结束对共享数据的操作后,就会走出房间,打开门锁.如果另一个线程在房门上锁时来,那么它必须等待房间内的线程出来并打开锁后,才可以进入房间.
锁的实现是采用原子操作实现的,而原子操作不存在竞争.其实现与具体的体系结构密切相关,几乎所有的处理器都实现了测试和设置指令,这一指令测试整数的值,如果其值为0,就设置一新值.0意味着开锁.而LINUX根据锁被争用时的行为表现,实现了不同的锁,这些锁主要表现是:被争用时会简单地执行忙等待或者会使当前任务睡眠直到锁可用为止.
8.2.1 并发执行
LINUX内核中并发执行的原因如下:
.中断--中断几乎可以在任务时刻异步发生,随时打断当前正在执行的代码;
.软中断和tasklet--内核能在任何时刻唤醒或调度软中断和tasklet,打断当前正在执行的代码;
.内核抢占--内核中的任务可能会被另一任务抢占;
.睡眠及与用户空间同步--内核执行的进程可能会睡眠,这就会唤醒调度程序,从而导致调度一个新的用户进程执行;
.对称处理器--两个或多个处理器可以同时执行代码.
需要保护哪些数据:
如果有其他执行线程可以访问这些数据,那么就给这些数据加上某种形式的锁;如果任何其他什么东西能看到它,那么就要锁住它.记住:要给数据而不是代码加锁.总而言之,几乎访问所有内核全局变量和共享数据都需要某种形式的同步方法.
8.3 死锁
死锁一个形象的例子就是:进程1和进程2都要获取锁A和锁B.如果进程1先获取了锁A,进程2获取了锁B,进程1试图去获取锁B而进程2试图去获取锁A.那么这两个进程便会发生死锁!
防止死锁的规则如下:
.加锁的顺序是关键;
.防止发生饥饿,比如说,如果"张"不发生,"王"有必要一定等下去吗?
.不要重复请求同一个锁;
.越复杂的加锁方案越容易造成死锁.