自旋锁_读写自旋锁_顺序锁_读-拷贝-更新

自旋锁用于保护短的代码,其中只包含少量C语句,因此很快会执行完毕。
进程上下文切换时,不适合用自旋锁,应该用信号量。
 
1. 自旋锁
自旋锁最多只能被一个可执行线程持有,
如果一个执行线程试图获得一个已经被别的线程所持有的自旋锁,处理器会继续执行这个线程,一直进行忙循环-自旋,等待锁重新可用。
注意:这里是自旋,处理器继续执行该线程,而不是睡眠;信号量就会睡眠。也正因为不会睡眠,所以自旋锁不应该被长时间持有。
自旋锁的初衷就是:在短期内进行轻量级加锁。如果使用另外的锁机制,让请求线程睡眠(如信号量),直到所重新可用时再唤醒它,这样处理器就不必循环等待,可以去执行其他代码。
这样同时会带来一定的开销---这里有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, 然后恢复中断到加锁前的状态。即时中断最初是被禁止的,代码也不会错误的把它们激活,而是会继续禁止。

2.2.2 spin_lock_irq()/spin_unlock_irq();

如果确定中断在加锁前是激活的,那就不需要在解锁后恢复中断以前的状态了,这时可以无条件的在解锁时激活中断,也就是如下代码所示:

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)


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值