进程上下文切换时,不适合用自旋锁,应该用信号量。
自旋锁最多只能被一个可执行线程持有,
如果一个执行线程试图获得一个已经被别的线程所持有的自旋锁,处理器会继续执行这个线程,一直进行忙循环-自旋,等待锁重新可用。
注意:这里是自旋,处理器继续执行该线程,而不是睡眠;信号量就会睡眠。也正因为不会睡眠,所以自旋锁不应该被长时间持有。
自旋锁的初衷就是:在短期内进行轻量级加锁。如果使用另外的锁机制,让请求线程睡眠(如信号量),直到所重新可用时再唤醒它,这样处理器就不必循环等待,可以去执行其他代码。
这样同时会带来一定的开销---这里有2次明显的进程上下文切换,被阻塞线程的换出和切入,与实现自旋锁的少数几行代码相比,上下文切换当然执行时间较长。
因此,持有自旋锁的时间最好小于完成2次上下文切换的耗时。如果时间大于两次上下文切换时,可以使用信号量。
要是锁未被别的线程持有,请求锁的执行线程便能立刻得到这个自旋锁,进而进入临界区继续执行。
任何时间,自旋锁都能防止多于一个线程同时进入临界区。
spinlock_t定义:
typedef struct {
raw_spinlock_t raw_lock;
#if defined(CONFIG_PREEMPT) && defined(CONFIG_SMP)
unsigned int break_lock;
#endif
#ifdef CONFIG_DEBUG_SPINLOCK
unsigned int magic, owner_cpu;
void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
} spinlock_t;
2. spinlock 基本用法:
spinlock_t lock = SPIN_LOCK_UNLOCKED; //初始化自旋锁时,必须使用SPIN_LOCK_UNLOCK 将其设置为未锁定状态。
...
spin_lock(&lock);
/* 临界区代码 */
spin_unlock(&lock);
spin_lock会遇到两种情况:
A. 如果内核中其他地方尚未获得lock,则由当前处理器获得。其他处理器不能再进入lock保护的代码范围。
B. 如果lock已经被另一个处理器获得,则spin_lock进入一个无限循环,重复的检查lock是否已经由spin_unlock释放(即自旋)。如果释放,则获得lock,并进入临界区。
使用自旋锁时应该注意以下两点:
A. 如果获得锁之后不释放,系统将变得不可用。其他处理器迟早需要进入锁对应的临界区,他们会进入无限循环等待锁释放,但等不到。这样就造成了死锁
B. 自旋锁不应该长期持有,因为所有等待锁释放的处理器都会处于不可用状态,无法用于其他工作。如果需要长时间锁住临界区,在这种情形下,信号量就比较适合。
C. 自旋锁当前持有者无法多次获得同一自旋锁,即不能递归。如果已经获得一个锁,而临界区里调用的某个函数试图再次获得该锁,也会造成死锁。
2.1 自旋锁主要为多处理器提供了防止并发访问的保护机制。而在单核处理器上,编译时并不会加入自旋锁,因为他仅仅被当做一个设置内核抢占机制是否被启用的开关。
如果禁止内核抢占,那么编译时自旋锁会被完全剔除出内核,即不需要自旋锁;只有系统支持内核抢占时,自旋锁才有作用,这只是针对单CPU。
2.2 自旋锁可以用在中断处理程序中,而信号量不能使用在中断处理程序中,因为信号量会导致进程睡眠。
在中断处理程序中使用自旋锁时,一定要在获得锁之前,首先禁止本地中断(即当前处理器上的中断请求),否则其他中断处理程序会打断正在持有自旋锁的进程,这样就会导致双重请求死锁。
注意是关闭当前处理器上的中断,而如果中断发生在不同的处理器上,就不会妨碍所得持有者最终释放锁。
内核提供禁止中断同时请求自旋锁的接口:
2.2.1 spin_lock_irqsave()/spin_unlock_restore():
使用方法:
DEFINE_SPINLOCK(mr_lock);
unsigned long flags;
spin_lock_irqsave(&mr_lock, flags);
//临界区
spin_unlock_irqrestore(mr_lock, flags);
spin_lock_irqsave() 保存中断的当前状态,并禁止本地中断,然后再去获得指定的锁 mr_lock.
spin_unlock_restore() 释放指定的锁 mr_lock, 然后恢复中断到加锁前的状态。即时中断最初是被禁止的,代码也不会错误的把它们激活,而是会继续禁止。
如果确定中断在加锁前是激活的,那就不需要在解锁后恢复中断以前的状态了,这时可以无条件的在解锁时激活中断,也就是如下代码所示:
DEFINE_SPINLOCK(mr_lock);
spin_lock_irq(&mr_lock);
//临界区
spin_unlock_irq(&mr_lock);
由于在内核执行路线上,很难搞清楚中断在当前调用点上是不是出于激活状态,所以并不提倡使用 spin_lock_irq()。
2.3 调试自旋锁
配置 CONFIG_DEBUG_SPINLOCK 可以为使用自旋锁代码加入许多调试手段。
激活了该选项,内核就会检查是否使用了未初始化的自旋锁,是否在还没加锁的时候就要进行解锁等,测试代码时,一定要激活该选项。
如果需要进一步调试,可以配上 CONFIG_DEBUG_LOCK_ALLOC 选项。
3.自旋锁操作函数
spinlock_t lock; //定义自旋锁
spin_lock_init(&lock); //动态初始化自旋锁lock
DEFIN_SPINLOCK(lock); //静态初始化自旋锁
spin_lock(&lock); //获得自旋锁lock,如果能立即获得锁,则立即返回,否则它将自旋在那里。
spin_trylock(&lock); //尝试获得自旋锁lock,如果能立即获得锁,则返回真,否则立即返回假,不再原地打转。
spin_unlock(&lock); //与上面两项搭配使用,释放自旋锁lock
spin_is_locked(&lock); //判断指定的锁当前状态是否被锁,如果是被锁状态,返回非0; 否则返回0
与中断的衍生函数:
spin_lock_irq() = spin_lock() + local_irq_disable()
spin_unlock_irq() = spin_unlock() + local_irq_enable()
spin_lock_irqsave() = spin_lock() + local_irq_save()
spin_unlock_irqrestore() = spin_unlock() + local_irq_restore()
spin_lock_bh() = spin_lock() + local_bh_disable() //获得锁的同时,禁止所有下半部的执行。
spin_unlock_bh() = spin_lock() + local_bh_enable() //释放锁的同时,使能所有下半部。
由于中断下半部可以抢占进程上下文,所以当下半部和进程上下文共享数据时,必须要加锁并且禁止下半部执行。
同样,由于中断处理程序可以抢占下半部,所以如果中断处理程序和下半部共享数据,就必须加锁同时禁止中断。
4. 读写自旋锁---rwlock
由自旋锁衍生,针对允许最多有一个写进程,但是可以有多个读执行单元的临界资源,当然读和写不能同时进行。
linux内核为此专门提供了读-写自旋锁,这种自旋锁,为读和写分别提供了不同的锁。
一个或多个读任务可以并发的持有读者锁;用于写的锁只能被一个写任务持有,而且写操作时不能有并发的读操作。
读-写自旋锁的使用方法:
rwlock_t my_rwlock = RW_LOCK_UNLOCKED; //静态初始化
DEFINE_RWLOCK(mr_rwlock);
rwlock_t my_rwlock;
rwlock_init(&my_rwlock); //动态初始化
普通读写锁在读者代码中使用:
void read_lock(rwlock_t *lock);
//临界区
void read_unlock(rwlock_t *lock);
普通读写锁在写者代码中使用:
void write_lock(rwlock_t *lock);
//临界区
void write_unlock(rwlock_t *lock);
通常情况下,读锁和写锁会位于完全分开的代码分支中。对共享资源进行读之前,应该先调用读锁定函数,读完成之后应调用读解锁函数。
其他的读锁操作函数:
void read_lock_irqsave(rwlock_t *lock, unsigned long flags);
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void read_lock_irq(rwlock_t *lock);
void read_unlock_irq(rwlock_t *lock);
void read_lock_bh(rwlock_t *lock);
void read_unlock_bh(rwlock_t *lock);
在对共享资源进行写之前,应该先调用写锁定函数,完成之后应调用写解锁函数。
写锁操作函数:
void write_lock_irqsave(rwlock_t *lock, unsigned long flags);
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void write_lock_irq(rwlock_t *lock);
void write_unlock_irq(rwlock_t *lock);
void write_lock_bh(rwlock_t *lock);
void write_unlock_bh(rwlock_t *lock);
int write_trylock(rwlock_t *lock); //尝试获取读写自旋锁,不管成功失败,都会立即返回。
5. 顺序锁---seqlock
顺序锁是对读写自旋锁的一种优化,即读执行单元不会被写执行单元阻塞。
即读执行单元在共享资源被执行写操作时,仍然可以继续读。但写与写仍然是互斥的。
顺序锁有个限制:要求被保护的共享资源必须不含指针。
5.1 seqlock_t 结构:
typedef struct {
unsigned sequence;
spinlock_t lock;
} seqlock_t;
5.2 定义一个seqlock:
seqlock_t mr_seq_lock = DEFINE_SEQLOCK(mr_seq_lock);
5.3 写执行单元获得顺序锁, 这个和读写自旋锁中的写锁一样,不同的在于读锁:
int write_tryseqlock(seqlock_t *sl);
void write_seqlock(seqlock_t *sl);
void write_sequnlock(seqlock_t *sl);
write_seqlock_irqsave(lock, flags);
write_sequnlock_irqrestore(lock, flags);
write_seqlock_irq(lock);
write_sequnlock_irq(lock);
write_seqlock_bh(lock);
write_sequnlock_bh(lock);
5.4 读执行单元时获得顺序锁,这时跟读写自旋锁中的读锁有很大不同:
unsigned read_seqbegin(const seqlock_t *sl);
int read_seqretry(const seqlock_t *sl, unsigned iv);
read_seqbegin() --- 读执行单元在对被顺序锁sl保护的共享资源进行访问前,需要调用此函数,该函数返回顺序锁sl的当前顺序号。
read_seqretry() --- 读执行单元在访问完被顺序锁sl保护的共享资源后,需要调用该函数来检查,在读访问期间是否有写操作。如果有写操作,需要重新进行读操作。
read_seqbegin_irqsave(lock, flags);
read_seqretry_irqrestore(lock, iv, flags);
5.5 seplock 在如下需求时,最适合使用:
a. 你的数据存在很多读者
b. 你的数据写者很少.
c. 希望写者很少,但是你希望写优先于读,而且不允许读者让写者饥饿。
d. 你的数据很简单,甚至是简单的整型 --- 在某些场合是不能使用原子变量的。
5.6 例子:
使用seqlock 最有说服力的就是jiffies. 该变量存储了Lniux机器从启动到当前的时间。
jiffies是一个64位的变量,记录了自系统启动以来的时钟节拍累加数。
get_jiffies_64()函数,能自动读取全部64位jiffies_64变量,该函数的实现就是用了seq锁:
u64 get_jiffies_64(void)
{
unsigned long seq;
u64 ret;
do {
seq = read_seqbegin(&xtime_lock); //使用了读者seqlock
ret = jiffies_64;
} while (read_seqretry(&xtime_lock, seq));
return ret;
}
EXPORT_SYMBOL(get_jiffies_64);
定时器中断会更新jiffies的值,这时需要使用写者seqlock:
write_seqlock(&xtime_lock);
jiffies_64 += ticks;
write_sequnlock(&xtime_lock);
6. 读-拷贝-更新 ---RCU(Read-Copy Update)
RCU即允许多个读执行单元同时访问被保护的数据,又允许多个读执行单元和多个写执行单元同时访问被保护的数据。
rcu_read_lock()
rcu_read_unlock()
rcu_read_lock_bh()
rcu_read_unlock_bh()
读锁定:同步RCU:
synchronize_rcu()
等等,如果需要了解,可以参考书7.4.4 (Page144)