Linux 内核同步
为什么要内核同步
防止共享资源的并发访问导致数据不一致
并发执行的来源
- 中断
- 进程切换
- 多处理器
内核同步的方法
主要就是 信号量 和 自选锁 两种加锁方式
原子操作
内核中定义了原子量类型atomic_t
, 可以通过下面的函数来操作原子量
原子数操作 | 说明 |
---|---|
ATOMIC_INIT (int) |
初始化原子量 |
atomic_read (atomic_t *) |
读取原子量 |
atomic_set (atomic_t *, int) |
设置原子量 |
atomic_add (int, atomic_t *) |
原子量加 |
atomic_sub (int, atomic_t *) |
原子量减 |
atomic_inc (atomic_t *) |
原子量加1 |
atomic_dec (atomic_t *) |
原子量减1 |
atomic_inc_and_test (atomic_t *) |
原子量加1, 并判断是否为0 |
atomic_dec_and_test (atomic_t *) |
原子量减1, 并判断是否为0 |
atomic_add_negative (int, atomic_t *) |
原子量加, 并判断是否为负数 |
atomic_sub_and_test (int, atomic_t *) |
原子量减, 并判断是否为0 |
atomic_add_return (int, atomic_t *) |
原子量加, 并返回加后的值 |
atomic_sub_return (int, atomic_t *) |
原子量减, 并返回减后的值 |
atomic_inc_return (atomic_t *) |
原子量加1, 并返回加后的值 |
atomic_dec_return (atomic_t *) |
原子量减1, 并返回减后的值 |
内核中还有对位操作的原子操作函数,这些函数可以操作任何类型的数据. 在这些函数前面加上两个__
是他们的非原子版本, 在需要的时候它们可能比原子的版本更快.
原子位操作函数 | 说明 |
---|---|
set_bit (int nr, void *addr) |
原子的设置addr指向的地址的第nr位 |
clear_bit (int nr, void *addr) |
原子的清除addr指向的地址的第nr位 |
change_bit (int nr, void *addr) |
原子的改变addr指向的地址的第nr位 |
test_and_set_bit (int nr, void *addr) |
原子的测试addr指向的地址的第nr位是否为1,如果是1,则设置为1,返回1,否则返回0 |
test_and_clear_bit (int nr, void *addr) |
原子的测试addr指向的地址的第nr位是否为1,如果是1,则清除为0,返回1,否则返回0 |
test_and_change_bit (int nr, void *addr) |
原子的测试addr指向的地址的第nr位是否为1,如果是1,则清除为0,返回1,否则返回0 |
test_bit (int nr, void *addr) |
原子的返回addr指向的地址的第nr位 |
自旋锁
自旋锁最多被一个线程持有, 如果一个线程尝试获取一个已经被持有的自旋锁, 它会一直自旋等待, 直到自旋锁被释放.
由于等待自旋锁会浪费处理器时间,降低系统性能,所以自旋锁的持有时间不能太长,所以自旋锁适用于短时期的轻量级加锁.
在中断处理程序正好就是这样的场景,并且中断处理程序不能睡眠也只能用自旋锁.
死锁
两个情况:
- 一个锁: 一个线程获得锁了后又尝试获取锁. 或者获得了锁后睡眠,最坏的情况就是不在被唤醒然后释放锁.
- 多个锁: 线程要获取两个锁,但两个线程刚好持有了一个锁. 解决办法: 获取多个锁时应当一直以同样的顺序获得.
自旋锁API
自旋锁操作函数 | 说明 |
---|---|
spin_lock_init (spinlock_t *) |
初始化自旋锁 |
spin_lock (spinlock_t *) |
获取自旋锁 |
spin_unlock (spinlock_t *) |
释放自旋锁 |
spin_lock_irq (spinlock_t *) |
获取自旋锁并关闭中断 |
spin_unlock_irq (spinlock_t *) |
释放自旋锁并开启中断 |
spin_lock_irqsave (spinlock_t *, unsigned long flags) |
获取自旋锁并保存中断状态 |
spin_unlock_irqrestore (spinlock_t *, unsigned long flags) |
释放自旋锁并恢复中断状态 |
spin_trylock (spinlock_t *) |
非阻塞尝试获取自旋锁,成功返回1,失败返回0 |
读着写者自旋锁
允许任意数量的读者同时进入临界区, 但是只允许一个写者进入临界区, 且写者有优先权, 当有写者等待时, 不允许读者进入临界区.
rwlock_t my_rwlock = RW_LOCK_UNLOCKED; /* Static way */
rwlock_t my_rwlock;
rwlock_init(&my_rwlock); /* Dynamic way */
void read_lock(rwlock_t *lock);
void read_lock_irqsave(rwlock_t *lock, unsigned long flags);
void read_lock_irq(rwlock_t *lock);
void read_lock_bh(rwlock_t *lock);
void read_unlock(rwlock_t *lock);
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void read_unlock_irq(rwlock_t *lock);
void read_unlock_bh(rwlock_t *lock);
void write_lock(rwlock_t *lock);
void write_lock_irqsave(rwlock_t *lock, unsigned long flags);
void write_lock_irq(rwlock_t *lock);
void write_lock_bh(rwlock_t *lock);
int write_trylock(rwlock_t *lock);
void write_unlock(rwlock_t *lock);
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void write_unlock_irq(rwlock_t *lock);
void write_unlock_bh(rwlock_t *lock);
补充: lockmeter 工具可以用来检测自旋锁的性能.
信号量
信号量可以使等待资源线程进入休眠状态,适用于占用资源比较久的场合
信号量会引起休眠,中断不能休眠,所以信号量不能用于中断
如果共享资源的持有时间比较短,不适合使用信号量,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的优势,此时使用自旋锁。
信号量API
函数 | 说明 |
---|---|
DEFINE_SEMAPHORE (sem); |
定义一个信号量 |
sema_init (sem, int val); |
初始化信号量 |
down (sem); |
获取信号量 |
down_trylock (sem); |
尝试获取信号量,成功返回0,失败返回非0 |
down_interruptible (sem); |
获取信号量,可以被信号(signal,比如ctrl+c 产生的SIGINT)中断 |
up (sem); |
释放信号量 |
读写信号量
函数 | 说明 |
---|---|
sema_init_rwsem (struct rw_semaphore *sem) |
初始化读写信号量 |
down_read (struct rw_semaphore *sem) |
获取读信号量 |
down_read_trylock (struct rw_semaphore *sem) |
尝试获取读信号量 |
up_read (struct rw_semaphore *sem) |
释放读信号量 |
down_write (struct rw_semaphore *sem) |
获取写信号量 |
down_write_trylock (struct rw_semaphore *sem) |
尝试获取写信号量 |
up_write (struct rw_semaphore *sem) |
释放写信号量 |
downgrade_write (struct rw_semaphore *sem) |
降级写信号量为读信号量 |
互斥量
Linux 提供了专门的互斥体mutex (等效信号量为1)
函数 | 说明 |
---|---|
DEFINE_MUTEX (mutex); |
定义一个互斥体 |
mutex_init (mutex); |
初始化互斥体 |
mutex_lock (mutex); |
获取互斥体 |
mutex_trylock (mutex); |
尝试获取互斥体,成功返回0,失败返回非0 |
mutex_unlock (mutex); |
释放互斥体 |
mutex_is_locked (mutex) |
判断mutex是否被获取,是返回1,不是返回0 |
Completions 机制
一种常见的情况: 当前线程等待某个动作结束(可能是创建一个线程,对一个进程的请求,或者基于硬件的动作). completions 是Linux内核针对这种场景提供的一种轻量级同步机制.
<linux/completion.h>
DECLARE_COMPLETION(my_completion);
//or
struct completion my_completion;
init_completion(&my_completion);
void wait_for_completion(struct completion *c);
void complete(struct completion *c); //多个等待者的情况只会唤醒一个
void complete_all(struct completion *c);//唤醒所有等待者, 一个等待者情况和complete一样