1. 原子操作
原子操作:保证指令以原子方式执行
内核提供整数和位的原子操作。
1.1. 原子整数操作
只能为类型是atomic_t
进行原子整数操作(定义在<linux/types.h>
):
typedef struct {
volatile int counter; //SPARC只能用24位,后8位为锁
} atomic_t
操作可在<asm/atomic.h>
找到,这里不细述。
原子操作只保证原子性,不保证顺序性,后者需要用屏障指令实现。
64位的原子整数位atomic64_t
,定义和操作基本和上面一样。
1.2. 原子位操作
定义在<asm/bitops.h>
。不需要特定的数据类型,操作参数通常为一个指针和一个偏移,即可实现原子位操作。
2. 自旋锁
锁最多只能被一个线程持有,其它线程一直循环等待锁重新可用。
该锁不能长时间持有(否则浪费处理器时间),只适合短期轻量加锁,但避免了上下文切换。
接口定义在<linux/spinlock.h>
中,使用只需:
DEFINE_SPINLOCK(mr_lock);
spin_lock(&mr_lock);
/* Critical Section */
spin_unlock(&mr_lock);
它也可被中断处理程序使用,使用时必须屏蔽本地CPU中断,内核提供了这样的接口(spin_lock_irq/irqsave(&lock, flag)
,区别是是否存储本地中断状态)。
自旋锁不可递归(因为不可重入)
3. 读写自旋锁
读之间共享,读写或者写之间互斥,使用方法类似:
DEFINE_RWLOCK(mr_rwlock);
read/write_lock(&mr_rwlock);
read/write_unlock(&mr_rwlock);
注意,锁升级会带来死锁。
同样,内核也提供接口,供中断处理程序使用。
4. 信号量
与自旋锁不同,信号量为0时,其它请求线程会进入阻塞(睡眠)状态,直到计数大于0时被唤醒。
信号量定义在<asm/semaphore.h>
的struct semaphore
中。OS实验中,我也使用了这个东西(虽然用的C的信号量,但本质一样),可看这里。
信号量初始为1时,即等价为互斥锁(mutex)。
5. 读写信号量
数据结构为rw_semaphore
,定义在<linux/rwsem.h>
中。
它的计数为1,只能有1个写入,同时满足读之间共享,读写/写之间互斥的条件。
可通过down_read/write()
和up_read/write()
获取/释放信号量。
6. 互斥体
行为等价于计数为1的信号量,但接口更加简单。
使用:
DEFINE_MUTEX(name);
mutex_init(&mutex);
mutex_lock(&mutex);
/*Critical section*/
mutex_unlock(&mutex);
它:
- 不可重入(因此不能递归)
- 不能再中断/下半部中使用
- 不可被拷贝/手动初始化/重复初始化
- 只能在一个上下文中加锁和解锁
- 持有时进程不可退出。
但最好优先使用它。
7. 完成变量
使2个任务同步,即一个任务等待另一个任务完成后再继续。
数据结构为struct completion
,数据结构和操作定义在<linux/completion.h>
。
等待线程调用wait_for_completion()
,另一个任务调用completion()
唤醒。
8. BLK:大内核锁
一个全局的自旋锁,特性有:
- 持有BLK时可睡眠,锁会被丢弃,唤醒后锁再次获得;
- BLK支持递归和重入
- BLK只能再进程上下文使用
- 新用户不能使用BLK
它定义在<linux/smp_lock.h>
,现在基本不使用。
9. 顺序锁
简称seq
锁,定义在<linux/seqlock.h>
,提供一种简单的机制,用于读写共享数据。
它维护一个计数器:
-
写入时,获取锁,计数增加1,释放时,锁再加1(成为偶数)
-
写操作还需要获取一个锁,用于写写互斥
-
读取时,不加锁,前后读该锁的计数:
-
之前的读需要读到偶数,若为奇数,则在进入临界区时要等待
进入临界区后,是允许写入的,这不同于读写锁
-
差值为0,则读时没被写操作打断
-
差值为偶数,说明被写入,则需要重读
-
unsigned long seq;
do {
seq = read_seqbegin(&mr_seq_lock);
/*Read data*/
}while(read_seqretry(&mr_seq_lock, seq));
它适用于多个读者和少数写者,且写优先于读。
10. 禁止抢占
由于内核可抢占,任何进程可以停下来让另一个运行。而某些代码段我们不想被抢占。自旋锁可以解决,但不需要自旋锁时,也可关闭内核抢占:
preempt_disable();
/*preemption disabled*/
preempt_enable();
/*preemption enabled*/
它通过引用计数实现,因此可重入。
11. 顺序屏障
处理多处理器的同步问题是,需要按顺序发出读和写指令,但编译器和CPU会打乱顺序以提高效率(乱序执行),这时候需要内存屏障。
读屏障rmb()
:确保跨越该屏障的读不会重排序(之前的读不会排到后面,同理后面的读不会排到前面)
写屏障wmb()
:确保跨越该屏障的写不会重排序(之前的写不会排到后面,同理后面的写不会排到前面)
读依赖屏障read_barrier_depends()
:确保跨越屏障的具有数据依赖的读取动作不会重排序
读写屏障mb()
:确保跨越屏障的读写操作不会重排序
内核也提高SMP的屏障。
屏障在不同体系结构上,效果会不同。