临界区和竞争条件
访问和操作共享数据的代码段称为临界区。如果两个执行线程在同一个临界区中同时执行,称为竞争条件。避免并发和防止竞态出现的机制被称为同步。
内核中造成并发的原因包括:
中断 —— 中断几乎可以在任何时刻异步发生,也就可能随时打断当前正在执行的代码。(中断服务程序访问被打断进程正在访问的资源)
软中断和tasklet —— 内核能在任何时刻唤醒或调度软中断和tasklet,打断当前正在执行的代码。
内核抢占 —— 因为内核具有抢占性,所以内核中的任务可能会被另一任务抢占。
睡眠以及用户空间的同步 —— 在内核执行的进程可能会睡眠,这就会唤醒调度程序,从而导致调度一个新的用户进程执行。
对称对处理器 —— 两个或多个处理器可以同时执行代码。
在编写代码的开始阶段就要设计恰当的锁,要给数据而不是代码加锁。
中断安全代码(interrupt-safe) —— 中断处理程序中能避免并发访问的代码。
SMP安全代码(SMP-safe) —— 对称多处理的机器中能避免并发访问的代码。
抢占安全代码(preempt-safe) —— 在内核抢占时能避免并发访问的安全代码。(SMP安全也意味着抢占安全)
内核同步方法
自旋锁(spin lock)
自旋锁最多只能被一个可执行线程持有。如果一个执行线程试图获得一个被争用(已经被持有)的自旋锁,那么该线程就会一直进行忙循环—旋转—等待锁重新可用,要是锁未被争用,请求锁的执行线程便能立刻得到它,继续执行,在任意时间,自旋锁都可以防止多于一个的执行线程同时进入临界区,注意同一个锁可以用在多个位置—例如,对于给定数据的所有访问都可以得到保护和同步。
自旋锁是不可递归的。
自旋锁可以使用在中断处理程序中。在中断处理程序中使用自旋锁时,一定要在获取锁之前,首先禁止本地中断,否则中断处理程序会打断正持有锁的内核代码,有可能会试图去争用这个已经被持有的自旋锁。但锁的持有者在中断处理程序执行完毕前不可能运行,导致双重请求死锁。
读写自旋锁
这种自旋锁为读和写分别提供了不同的锁。一个或多个读任务可以并发地持有读锁;写锁只能被一个写任务持有,而此时不能有并发的读操作。
信号量 (semaphone)
Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个已被持有的信号量时,信号量会将其推入等待队列,然后让其睡眠。这时处理器获得自由去执行 其它代码。当持有信号量的进程将信号量释放后,处于等待队列中的一个任务将被唤醒,并获得这个信号量。
由于信号量的睡眠特性,所以:
由于争用信号量的进程在等待锁变为可用时会进入睡眠状态,所以信号量适用于锁被长时间持有的情况。
相反,如果持有锁的时间较短,使用信号量就不合适了。因为睡眠、维护等待队列以及唤醒进程所花费的时间可能会多于锁被占用的时间。
由于信号量在持有时,执行线程可能会睡眠,所以信号量只能用在进程上下文中。因为中断上下文中是不能CPU进行调度的。
你可以在持有信号量时睡眠,因为其他试图获取该信号量的线程也只是去睡眠而已,并不会造成死锁。(该进程最终会继续执行的)
当你占用信号量时,就不能占有自旋锁,因为自旋锁是不允许进程睡眠的。
信号量根据同一时刻允许持有者的数量分为 互斥信号量和二值信号量
互斥体(mutex)
互斥体行为和使用互斥信号量类似,但接口更简单,实现也更高效。
使用注意事项:
任何时刻中只有一个任务可以持有mutex, 也就是说,mutex的使用计数永远是1
给mutex锁者必须负责给其再解锁——你不能在一个上下文中锁定一个mutex,而在另 一个上下文中给它解锁。这个限制使得mutex不适合内核同用户空间复杂的同步场景。最 常使用的方式是:在同一上下文中上锁和解锁。
递归地上锁和解锁是不允许的。也就是说,你不能递归地持有同一个锁,同样你也不能再去解锁一个已经被解开的mutex
当持有一个mutex时 ,进程不可以退出
mutex不能在中断或者下半部中使用,即使使用mutex_trylock()也不行
mutex只能通过官方API管理:它只能使用上节中描述的方法初始化,不可被拷贝、手动 初始化或者重复初始化
顺序锁(seq)
实现这种锁主要依靠一个序列计数器。当有疑义的数据被写入时,会得到一个锁,并且序列值会增加。在读取序列值之前或之后,序列号都被读取。如果读取的序列号值相同,说明在读操作进行的过程中没有被写操作打断过。此外,如果读取的值是偶数,那么就表明写操作没有发生(要明白因为锁的初值是0,所以写锁会使值成为奇数,释放的时候就变成偶数)。
顺序锁适合以下情况
你的数据存在很多读者。
你的数据写者很少。
虽然写者很少,但是你希望写优先于读,而且不允许读者让写者饥饿。
你的数据很简单,如简单结构,甚至是简单的整型——在某些场合,你是不能使用原子量的。