1 自旋锁

当获取锁失败时,线程会处于忙等待状态,不会导致此线程睡眠;可以用在中断上下文。
先获取锁,进入临界区操作完毕,释放锁。获取锁和释放锁的操作都是原子操作,指令在处理器上运行时时相当于单条指令,不会被分割打断。

  • 自旋锁上锁的函数内部,会调用preempt_disable 关闭内核抢占的。程序员只需关心是否需要关闭中断即可;

涉及到需要关闭中断可以使用如下函数
spin_lock_irqsave //内部顺序,关闭中断相应,关闭内核抢占,获取锁
spin_unlock_irqsave //内部顺序,释放锁,开启中断相应,开启内核抢占
spin_lock_irq
spin_lock_bh 关闭软中断相应;

  • 自旋锁的非阻塞版本,如果尝试获取自旋锁失败,线程不会阻塞在那忙等待,而是返回0;获取锁成功返回1;

spin_trylock
spin_trylock_irq
spin_trylock_irqsave

1.1 自旋锁源码解析

pin_lock()的实现
#define spinlock(lock) _spin_lock(lock) //lock数据类型为*spinlock_t,虽然没有用到--。 void lockfunc spin_lock(spinlock_t *lock)
{
preempt_disable();//关抢占
spin_acquire(&lock->dep_map, 0, 0, _RET_IP
);//空操作
LOCK_CONTENDED(lock, _raw_spin_trylock, _raw_spin_lock);
}


LOCK_CONTENDED是个宏定义,在当前lock为普通自旋锁时,会以lock为参数运行_raw_spin_lock()函数,_raw_spin_lock()定义如下:
#define _raw_spin_lock(lock)
raw_spin_lock(&(lock)->raw_lock);
raw_spin_lock()的实现是跟体系结构相关的,下面来看看在x86里面的实现:
static inline void
raw_spin_lock(raw_spinlock_t *lock)
{
asm volatile(
“\n1:\t”
LOCK_PREFIX “ ; decl %0\n\t”
“jns 2f\n”
“3:\n”
“rep;nop\n\t”
“cmpl $0,%0\n\t”
“jle 3b\n\t”
“jmp 1b\n”
“2:\t” : “=m” (lock->slock) : : “memory”);
}
指令前缀LOCK_PREFIX表示执行这条指令时将总线锁住,不让其他处理器方位,以此来保证这条指令执行的“原子性”,%0表示lock->slock,第一句话表示将lock->slock减一。第二句话进行判断,如果减一之后大于或等于零则表示加锁成功,则调到标号2处,代码2后面没有继续执行的代码了,因此会返回。如果减一之后小于零,则表示之前已经有进程进行了加锁操作,则跳到标号3处执行,将lock->slock与0进行比较,如果小于零则再次跳到3处执行,即循环执行标号3处的指令。直到加锁者释放锁将lock->slock设为1,此时会跳到标号1处进行加锁操作。

2 读写锁 rwlock

基于自旋锁的优化,只有读读并行,与写有关的都串行,即写有排他性;

  • 适用场合,大量读并发,少数写操作的情况下;

    2.1 读写锁的原理

    读写锁本质上是一个内存计数器,初始化成一个很大的值(第57-58行)0x01000000,表示最多可以有这么多个读者同时获取锁。
    获取读锁时,计数器减1,根据符号位判断是否为负数:

  • 负数,表示已经有写者,读锁获取失败。此时计数器的值小于等于0,先将计数器加1,然后循环判读,直到值大于1,说明写锁释放了,读锁获取成功;

  • 非负数,表示无写者,获取读锁成功。

获取写锁时,计数器减0x01000000,判断是否为0:

  • 等于0,则表示没有其他的读者或者写者,获取写锁成功。
  • 不为0,则有其他的读者或者写者,获取写锁失败。此时,将计数器加0x01000000,循环判断计数器值,直到为0x01000000。为初值0x01000000表示没有了其他的读者或者写者,可以尝试获取写锁了。

    3 信号量

    struct semaphore {
    raw_spinlock_t lock; //自旋锁保护counter操作
    unsigned int count;
    struct list_head wait_list;
    ……
    }
    struct list_head wait_list,管理所有再该信号量上睡眠的进程,在获取信号量失败时,会该进程从链表尾部插入,并且将进程设置成睡眠状态;当信号量释放时,会将链表中的第一个进程从链表队列中删除,并唤醒这个进程;

  • 获取信号量

down_interruptible(struct semaphore *sem)
可以通过发送信号唤醒,如果被用户发送的信号唤醒,会返回相应的错误码;

  • 释放信号量

up(struct semaphore *sem)

3.1 互斥锁与信号量的区别

互斥锁访问共享资源是无序的,信号量是有序的(信号量的waitlist链表管理睡眠的进程,唤醒时从链表首部逐一唤醒);

4、顺序锁

写写互斥,写者先获取锁,读者也要等待;

typedef struct{
unsigned sequence;
spinlock_t lock;
}seqlock_t
设计思想:读者不加锁,写者加锁;读者在进入临界区之前获取sequence值并记录,在退出临界区时再次读取sequence值,将前后两次的值比较,不一致说明,这期间有写者操作临界区,此次读无效。

4.1 实现原理

  • seqlock_init 初始化顺序锁,将sequence的值设置为0;
  • 在读者获取顺序锁时,判断sequence的值是否为偶数(sl->sequence & 1,比较最低位)

|—偶数,说明没有写者持锁或者写者已经完成写操作,获取sequence值并记录,进入临界区操作;在读者释放锁时,再次读取sequence值,比较这两次的值是否为相等,如果不等,说明在读的期间,有写者进入临界区操作,此次读操作无效,需要重新读;
|—奇数,写者正在写操作,读者忙等待

  • 在写者获取顺序锁,实际上是获取顺序锁内的自旋锁;

|—获取成功,将sequence值+1,进入临界区操作;在写者释放锁时,再次将sequence的值+1;
|—获取失败,忙等待;

4.2 顺序锁和读写锁的比较

1)相同:两者都是写写互斥
2)不同:顺序锁,写锁优先级高与读锁,在读者先进入临界区后,写者还可以正常进入临界区操作,读者需要重启读操作;
读写锁,写者要进入临界区必须要等当前临界区内的所有读者都退出临界区后,才能写。

4.3 使用场景

写操作响应速度高于读操作的情况下。

5、RCU锁 (read-copy-update)

加锁,解锁都要涉及到内存操作,对系统开销大。RCU无需考虑读写操作的互斥问题。

5.1 RCU原理

读RCU锁,rcu_read_lock

  • 关闭内核抢占
  • 构建临界区,通过指针指向共享数据区,读操作通过该指针完成。

读者有个明确规则,对该指针的引用只能在临界区内。

写RCU锁,rcu_wirte_lock

  • 重新分配一个共享数据区,将老数据拷贝到新数据区,并且在新数据修改;
  • 用新数据的指针,代替老数据区的指针,此后读者再获取的就是新数据的指针
  • 老数据区指针不能马上释放,需要向每个cpu注册一个回调函数,回调函数实现老数据区的释放。
  • 指向老数据区指针的释放时机,确保系统内所有引用老数据区的读者完成后。

因为读者在获取rcu时,关内核抢占,且对老数据区应用的指针都必须在临界区内,当所有cpu至少发生一次进程切换时,说明没有对老指针的引用。

6、原子变量

原子操作,需要借助体系结构相关的汇编指令完成;
通过atomic_t声明声明原子变量
typedef struct {
int conuter;
}atomic_t

原子操作函数:static inline void atomic_inc(atomic_t *v)