内核中提供了多种方法来防止竞争条件,理解了这些方法的使用场景有助于我们在编写内核代码时选用合适的同步方法,
从而即可保证代码中临界区的安全,同时也让性能的损失降到最低。
主要内容:
1. 原子操作
原子操作是由编译器来保证的,保证一个线程对数据的操作不会被其他线程打断。
原子操作有2类:
原子整数操作,有32位和64位。头文件分别为<asm/atomic.h>和<asm/atomic64.h>原子位操作。头文件 <asm/bitops.h>
原子操作的api很简单,参见相应的头文件即可。
原子操作头文件与具体的体系结构有关,比如x86架构的相关头文件在 arch/x86/include/asm/*.h
2. 自旋锁
原子操作只能用于临界区只有一个变量的情况,实际应用中,临界区的情况要复杂的多。
对于复杂的临界区,linux内核中也提供了多种同步方法,自旋锁就是其中一种。
自旋锁的特点就是当一个线程获取了锁之后,其他试图获取这个锁的线程一直在循环等待获取这个锁,直至锁重新可用。
由于线程实在一直循环的获取这个锁,所以会造成CPU处理时间的浪费,因此最好将自旋锁用于能很快处理完的临界区。
自旋锁的实现与体系结构有关,所以相应的头文件 <asm/spinlock.h> 位于相关体系结构的代码中。
自旋锁使用时有2点需要注意:
自旋锁是不可递归的,递归的请求同一个自旋锁会自己锁死自己。线程获取自旋锁之前,要禁止当前处理器上的中断。(防止获取锁的线程和中断形成竞争条件) 比如:当前线程获取自旋锁后,在临界区中被中断处理程序打断,中断处理程序正好也要获取这个锁, 于是中断处理程序会等待当前线程释放锁,而当前线程也在等待中断执行完后再执行临界区和释放锁的代码。
中断处理下半部的操作中使用自旋锁尤其需要小心:
自旋锁方法列表如下:
方法
描述
spin_lock()获取指定的自旋锁
spin_lock_irq()禁止本地中断并获取指定的锁
spin_lock_irqsave()保存本地中断的当前状态,禁止本地中断,并获取指定的锁
spin_unlock()释放指定的锁
spin_unlock_irq()释放指定的锁,并激活本地中断
spin_unlock_irqstore()释放指定的锁,并让本地中断恢复到以前状态
spin_lock_init()动态初始化指定的spinlock_t
spin_trylock()试图获取指定的锁,如果未获取,则返回0
spin_is_locked()如果指定的锁当前正在被获取,则返回非0,否则返回0
3. 读写自旋锁
注:读写锁要分别使用,不能混合使用,否则会造成死锁。
正常的使用方法:
DEFINE_RWLOCK(mr_rwlock);read_lock(&mr_rwlock);read_unlock(&mr_rwlock);write_lock(&mr_lock);write_unlock(&mr_lock);
混合使用时:
read_lock(&mr_lock);/* 在获取写锁的时候,由于读写锁之间是互斥的, * 所以写锁会一直自旋等待读锁的释放, * 而此时读锁也在等待写锁获取完成后继续下面的代码。 * 因此造成了读写锁的互相等待,形成了死锁。 */write_lock(&mr_lock);
读写锁相关文件参照 各个体系结构中的 <asm/rwlock.h>
读写锁的相关函数如下:
方法
描述
read_lock()获取指定的读锁
read_lock_irq()禁止本地中断并获得指定读锁
read_lock_irqsave()存储本地中断的当前状态,禁止本地中断并获得指定读锁
read_unlock()释放指定的读锁
read_unlock_irq()释放指定的读锁并激活本地中断
read_unlock_irqrestore()释放指定的读锁并将本地中断恢复到指定前的状态
write_lock()获得指定的写锁
write_lock_irq()禁止本地中断并获得指定写锁
write_lock_irqsave()存储本地中断的当前状态,禁止本地中断并获得指定写锁
write_unlock()释放指定的写锁
write_unlock_irq()释放指定的写锁并激活本地中断
write_unlock_irqrestore()释放指定的写锁并将本地中断恢复到指定前的状态
write_trylock()试图获得指定的写锁;如果写锁不可用,返回非0值
rwlock_init()初始化指定的rwlock_t
4. 信号量
信号量也是一种锁,和自旋锁不同的是,线程获取不到信号量的时候,不会像自旋锁一样循环的去试图获取锁,
而是进入睡眠,直至有信号量释放出来时,才会唤醒睡眠的线程,进入临界区执行。
由于使用信号量时,线程会睡眠,所以等待的过程不会占用CPU时间。所以信号量适用于等待时间较长的临界区。
信号量消耗的CPU时间的地方在于使线程睡眠和唤醒线程,
如果 (使线程睡眠 + 唤醒线程)的CPU时间 > 线程自旋等待的CPU时间,那么可以考虑使用自旋锁。
信号量有二值信号量和计数信号量2种,其中二值信号量比较常用。
二值信号量表示信号量只有2个值,即0和1。信号量为1时,表示临界区可用,信号量为0时,表示临界区不可访问。
二值信号量表面看和自旋锁很相似,区别在于争用自旋锁的线程会一直循环尝试获取自旋锁,
而争用信号量的线程在信号量为0时,会进入睡眠,信号量可用时再被唤醒。
计数信号量有个计数值,比如计数值为5,表示同时可以有5个线程访问临界区。
信号量相关函数参照: <linux/semaphore.h> 实现方法参照:kernel/semaphore.c
使用信号量的方法如下:
DECLARE_MUTEX(mr_sem);/* 试图获取信号量…., 信号未获取成功时,进入睡眠 * 此时,线程状态为 TASK_INTERRUPTIBLE */down_interruptible(&mr_sem);/* 这里也可以用: * down(&mr_sem); * 这个方法把线程状态置为 TASK_UNINTERRUPTIBLE 后睡眠 up(&mr_sem);
一般用的比较多的是down_interruptible()方法,因为以 TASK_UNINTERRUPTIBLE 方式睡眠无法被信号唤醒。
经验是由痛苦中粹取出来的