Linux内核中最常见的锁是自旋锁。一个自旋锁就是一个互斥设备,它只能有两个值:"锁定"和"解锁"。如果锁可用,则"锁定"位被设置,而代码继续进入临界区;相反,如果锁被其他进程争用,则代码进入忙循环并重复检查这个锁,直到锁可用为止。这个循环就是自旋锁的"自旋"。自旋锁最多只能被一个可执行的线程持有。如果一个执行线程试图获得一个被争用的自旋锁,那么该线程就会一直进行忙循环-旋转-等待锁重新可用。注意,同一个锁可以用在多个位置。缺点:一个被争用的自旋锁使得请求它的线程在等待锁重新可用时自旋(特别浪费处理器时间)。所以,自旋锁不应该被长时间持有。当然,可以采用另外的方式处理对锁的争用:让请求线程睡眠,直到锁重新可用时在唤醒它。但是,这里有两次明显的上下文切换,被阻塞的线程要换入或换出。因此,持有自旋锁的时间最好小于完成两次上下文切换的耗时。

注意:

1) 如果禁止内核被抢占,那么在编译时自旋锁会被完成剔除出内核。

2) Linux内核实现的自旋锁是不可递归的。小心自加锁。

3) 调试自旋锁, 加上配置选项CONFIG_DEBUG_SPINLOCK。

4)自旋锁可以使用在中断处理程序中(此处不能使用信号量,因为它们会导致睡眠),在中断处理程序中使用自旋锁时,一定要在获取锁之前,首先禁止本地中断(在当前处理器上的中断请求),否则中断处理程序就会打断正持有锁的内核代码,有可能试图去争用这个已经被持有的自旋锁。

5) 加锁是对数据不是对代码。

6) 所有自旋锁的等待在本质上都是不可中断的。

3.1. 自旋锁API介绍

自旋锁实现与体系结构密切相关,定义在<linux/spinlock.h>中。在编译时对自旋锁的初始化:

spinlock_t my_lock=SPIN_LOCK_UNLOCKED;

或者在运行时:

void spin_lock_init(spinlock_t *lock);

进入临界区:

spin_lock(&my_lock);

/*访问数据*/

spin_unlock(&my_lock);

内核提供禁止中断同时请求锁的接口:

spinlock_t my_lock=SPIN_LOCK_UNLOCKED;

unsigned long flags;

spin_lock_irqsave(&my_lock, flags);

/*访问数据*/

spin_unlock_irqrestore(&my_lock, flags);

函数spin_lock_irqsave保存了中断的当前状态,并禁止了本地中断,然后在获取指定的锁;函数spin_unlock_irqrestore对指定的锁解锁,然后让中断恢复到加锁前的状态。

内核提供的自旋锁的接口:

void spin_lock_irq (spinlock_t *lock);

void spin_unlock_irq (spinlock_t *lock);

void spin_lock_bh (spinlock_t *lock);

void spin_unlock_bh (spinlock_t *lock);

如果能够确保没有任何其他代码禁止本地处理器的中断,也就是说,能够确保在释放自旋锁时应该启用中断,这可以使用spin_lock_irq函数,而无需跟踪标志。函数spin_lock_bh在获得锁之前禁止软件中断,但是会让硬件中断保持打开。

int spin_trylock (spinlock_t *lock);

int spin_trylock_bh (spinlock_t *lock);

这两个函数是非阻塞性的,成功获取自旋锁返回非零值,否则返回零。

3.2. 自旋锁和下半部

适用于自旋锁的核心规则是:任何拥有自旋锁的代码都必须是原子的。它不能被休眠,事实上,它不能因为任何原因放弃处理器,除了服务中断以外(这个中断服务没有访问已经获取自旋锁的数据)。内核抢占的情况由自旋锁代码本身处理,任何时候,只要内核代码拥有自旋锁,在相关处理器上的抢占就会被禁止。

函数spin_lock_bh用于获取指定锁,同时它会禁止所有下半部的执行。由于下半部会抢占进程上下文中的代码,所以当下半部与进程上下文共享数据时,必须对进程上下文中的共享数据进行保护,所以需要加锁的同时还有禁止下半部的执行。

同类的tasklet不可能同时运行,所以对于同类的tasklet的共享数据不需要保护。但是当数据被两个不同种类的tasklet共享时,就需要在访问下半部中的数据前先获得一个普通的自旋锁。这里不需要禁止下半部,因为在同一个处理器上决不会有tasklet相互抢占的情况。

对于软中断,无论是否同种类型,如果数据被软中断共享,那么它必须得到锁的保护。因为即使是同种类型的两个软中断也可以同时运行在一个系统的多个处理器上。但是,同一个处理器上的一个软中断决不会抢占另一个软中断,因此,不需要禁止下半部。

3.3. 读-写自旋锁

当对某个数据结构的操作可以清晰化为读/写两种类型时,读/写自旋锁就派上了用场。一个或多个读任务可以并发的持有读锁;相反,写锁只能被一个写任务所持有,而且此时不能有并发的读操作。多个读任务可以安全地获得同一个读锁,事实上,即使一个线程递归地获取同一读锁也是安全的。

初始化读/写锁:

rwlock_t my_rwlock=RW_LOCK_UNLOCKED;

在读任务的代码分支上:

read_lock(&my_rwlock);

/*读数据*/

read_unlock(&my_rwlock);

在写任务的代码分支上:

write_lock(&my_rwlock);

/*读数据*/

write_unlock(&my_rwlock);

注意:不能把一个读锁升级为一个写锁:

read_lock(&my_rwlock);

write_lock(&my_rwlock);

这将会带来死锁,因为写锁会不断自旋,等待所有的读锁被释放,其中包括自己。

内核实现的读锁和写锁接口在<linux/spinlock.h>中有定义。

在使用Linux读-写自旋锁时,需要考虑一点事这种机制照顾读比照顾写要多一点。当读锁被持有时,写操作为了互斥访问只能等待,但是读任务却可以继续成功的占用读锁,而且自旋等待的写任务在所有读任务释放锁之前是无法获得写锁的。

05-11 13:57