内核同步方法
1.原子操作
原子操作可以保证指令以原子的方式执行——执行过程不被打断。内核提供了两组原子操作接口,一组针对整数进行操作,另一组针对单独的位进行操作。
针对整数的原子操作只能对 atomic_t 类型的数据进行处理。
除了原子整数操作外,内核还提供了一组针对位这一级数据进行操作的函数。位操作函数是对普通的内存地址进行操作的,它的参数是一个指针和一个位号,第0位是给定地址的最低有效位。
内核还提供了一组与上述操作对应的非原子位函数。非原子位函数与原子位函数的操作完全相同,但是前者不保证原子性,且其名字前缀多两个下划线。例如,与 set_bit 对应的非原子形式是 __set_bit。如果你不需要原子性操作(比如说,如果你已经用锁保护了自己的数据),那么这些非原子的位函数相比原子的位函数可能会执行得更快些。
2.自旋锁
Linux内核中最常见的锁是自旋锁(spin lock)。自旋锁最多只能被一个可执行线程持有。如果一个执行线程试图获得一个被争用(已经被持有)的自旋锁,那么该线程就会一直进行忙循环——旋转——等待锁重新可用。
spinlock_t mr_lock = SPIN_LOCK_UNLOCKED; spin_lock( &mr_lock ); /* 临界区... */ spin_unlock( &mr_lock );
Linux内核实现的自旋锁是不可递归的。
自旋锁可以使用在中断处理程序中(此处不能使用信号量,因为它们会导致睡眠)。在中断处理程序中使用自旋锁时,一定要在获取锁之前,首先禁止本地中断(在当前处理器上的中断请求),否则,中断处理程序就会打断正持有锁的内核代码,有可能会试图去争用这个已经被持有的自旋锁,造成死锁。注意,需要关闭的只是当前处理器上的中断。如果中断发生在不同的处理器上,即使中断处理程序在同一锁上自旋,也不会妨碍锁的持有者(在不同处理器上)最终释放锁。
内核提供了禁止中断同时请求锁的接口:
spinlock_t mr_lock = SPIN_LOCK_UNLOCKED; unsigned long flags; spin_lock_irqsave( &mr_lock, flags ); //保存中断当前状态,并禁止中断,获取锁。 /* 临界区... */ spin_unlock_irqrestore( &mr_lock, flags ); //解锁,并让中断恢复到加锁前的状态。
在与下半部配合使用时,必须小心地使用锁机制。函数 spin_lock_bh 用于获取指定锁,同时它会禁止所有下半部的执行。相应的 spin_unlock_bh 函数执行相反的操作。
由于下半部可以抢占进程上下文的代码,所以当下半部和进程上下文共享数据时,必须对进程上下文中的共享数据进行保护,需要加锁的同时还要禁止下半部执行。同样,由于中断处理程序可以抢占下半部,所以如果中断处理程序和下半部共享数据,那么就必须在获取恰当的锁的同时还要禁止中断。
同类的 tasklet 不可能同时运行,所以对于同类 tasklet 中的共享数据不需要保护。但是当数据被两个不同种类的 tasklet 共享时,就需要在访问下半部中的数据前先获得一个普通的自旋锁,这里不需要禁止下半部,因为在同一个处理器上决不会有 tasklet 相互强占的情况。
对于软中断,无论是否同种类型,如果数据被软中断共享,那么它必须得到锁的保护,这是因为即使是同种类型的两个软中断也可以同时运行在一个系统的多个处理器上。但是,同一处理器上的一个软中断绝不会抢占另一个软中断。因此,根本没必须禁止下半部。
3.读—写自旋锁
这种自旋锁为读和写分别提供了不同的锁。一个或多个读任务可以并发的持有读者锁;相反,用于写的锁最多只能被一个写任务持有,而且此时不能有并发的读操作。
rwlock_t mr_rwlock = RW_LOCK_UNLOCKED; read_lock( &mr_rwlock ); /* 临界区(只读) */ read_unlock( &mr_rwlock ); write_lock( &mr_rwlock ); /* 临界区(读写) */ write_unlock( &mr_rwlock );
注意:不能把一个读锁“升级”为写锁
read_lock( &mr_rwlock ); write_lock( &mr_rwlock );
将会带来死锁,因为写锁会不断自旋,等待所有的读者释放锁,其中也包括它自己。
这种锁机制照顾读比照顾写要多一点。当读锁被持有时,写操作为了互斥访问只能等待,但是,读者却可以继续成功地占用锁,而自旋等待的写者在所有读者释放锁之前是无法获得锁的。所以,大量读者必定会使挂起的写者处于饥饿状态。