第十章 内核同步方法

内核同步方法

Linux内核提供了一组相当完备调度同步方法。

1. 原子操作

原子操作可以保证指令以原子方式执行——执行过程不被打断。

内核提供了两组原子操作接口:一组针对整数进行操作,一组针对单独的位进行操作。

  • 针对整数的原子操作:只能对atomic_t类型的数据进行处理,atomic_t类型定义在linux/types.h
1
2
3
4
5
6
7
8
9
typedef struct {
int counter;
} atomic_t;
#ifdef CONFIG_64BIT
typedef struct {
long counter;
} atomic64_t;
#endif

使用原子整型操作的声明在asm/atomic.h,所有的操作都在里面,下面是几个简单的操作:

操作 说明
ATOMIC_INIT(i) 初始化
atomic_set(&v, 4) v = 4
atomic_add(2, &v) v = v + 2
atomic_inc(&v) v = v + 1
atomic_dec(&v) v = v - 1
atomic_read(&v) 读取v

原子操作通常是内联函数,往往是通过内嵌汇编指令来实现的,如果某个函数本身就是原子的,它往往被定义成一个宏。

原子性和顺序性

原子性确保指令执行期间不被打断,要么全部执行完,要么都不执行;顺序性确保即使多条指令出现在独立的执行线程中,甚至独立的处理器上,它们执行的顺序依然保持,顺序性通过屏障(barrier)指令实施。

  • 原子位操作:内核提供一组针对位的原子操作,定义在asm/bitops.h

2. 自旋锁

Linux内核中最常见的锁是自旋锁,自旋锁最多只能被一个可执行线程持有。如果一个线程试图获取一个已被持有的自旋锁,这个线程会进行忙循环——旋转等待(会浪费处理器时间)锁重新可用。自旋锁的初衷:在短期内进行轻量级加锁。

另一种处理锁争用的方式:让等待线程睡眠,直到锁重新可用时再唤醒它,这样处理器不必循环等待,可以去执行其他代码,但是这会有两次明显的上下文切换的开销,信号量便提供了这种锁机制。

自旋锁的接口定义在linux/spinlock.h

1
2
3
4
DEFINE_SPINLOCK(mr_lock);
spin_lock(&mr_lock);
/* 临界区 */
spin_unlock(&mr_lock);

注意Linux内核自旋锁不能递归调用。

自旋锁方法列表

操作 说明
spin_lock() 获取指定的自旋锁
spin_lock_irq() 禁止本地中断并获取指定的锁
spin_lock_irqsave() 保存本地中断当前状态,禁止本地中断,获取指定的锁
spin_unlock() 释放指定的锁
spin_unlock_irq() 释放指定的锁,并激活本地中断
spin_unlock_irqrestore() 释放指定的锁,并让本地中断恢复以前状态
spin_lock_init() 动态初始化指定的锁
spin_trylock() 试图获取指定的锁,成功返回0,否则返回非0
spin_is_locked() 测试指定的锁是否已被占用,已被占用返回非0,否则返回0

3. 读写自旋锁

Linux专门提供读写自旋锁,一个或多个读任务可以并发持有读锁;写锁同一时间只能被一个线程持有,而且此时不能有并发的读操作。在申请写锁时,会等待已经加的读锁释放,而读锁可以继续成功地占用锁,因此需要注意写锁饿死。

读写自旋锁操作

方法 描述
read_lock() 获取指定的读锁
read_lock_irq() 禁止本地中断并获取指定读锁
read_lock_irqsave() 存储本地中断当前状态,禁止本地中断并获取指定读锁
read_unlock() 释放指定的读锁
read_unlock_irq() 释放指定的读锁并激活本地中断
read_unlock_irqrestore() 释放指定的读锁,激活本地中断并将本地中断恢复到指定状态
write_lock() 获取指定写锁
write_lock_irq() 禁止本地中断并获取指定写锁
write_lock_irqsave() 存储本地中断当前状态,禁止本地中断并获取指定写锁
write_unlock() 释放指定写锁
write_unlock_irq() 释放指定写锁并激活本地中断
write_unlock_irqrestore() 释放指定写锁并将本地中断恢复到指定状态
write_trylock() 视图获取指定写锁:如果不可用,返回非0值
rwlock_init() 初始化指定的rwlock_t

4. 信号量

Linux中的信号量是一种睡眠锁。如果一个任务视图获得一个已被占用的信号量时,信号量会将其推进一个等待队列,然后让其睡眠。让持有信号量被释放后,处于等待队列中的任务会被唤醒,并获得该信号量。

由于信号量的睡眠特性,所以:

  • 由于争用信号量的进程在等待锁重新变为可用时会睡眠,所以信号量适用于锁会被长时间持有的情况
  • 在锁被短时间持有时,使用信号量就不太合适,因为睡眠、维护等待队列以及唤醒花费的开销可能比锁被占用的全部时间还长
  • 由于执行线程在锁被争用时会睡眠,所以只能在进程上下文中才能获取信号量锁,因为在中断上下文中是不能进程调度的
  • 在占用信号量时不能占用自旋锁

信号量的实现与体系结构相关,具体实现定义在linux/semaphore.h中。

5. 读写信号量

与读写自旋锁类似,信号量也可以区分为读写信号量。

rw_semaphore结构,定义在linux/rwsem.h,所有的读写信号量都是互斥信号量,它们的引用计数为1,对写者互斥,不对读者。

6. 互斥量

mutex为互斥的睡眠锁,在内核中对应数据结构mutex,定义在linux/mutex.h

常用方法

方法 说明
mutex_lock(struct mutex*) 为指定的mutex上锁,如果锁不可用则睡眠
mutex_unlock(struct mutex*) 解锁
mutex_trylock(struct mutex*) 试图获取指定的mutex,成功返回1并获得锁;否则返回0
mutex_is_locked(struct mutex*) 如果锁已被占用返回1;否则返回0

注意:

  • 任何时刻只有一个任务持有mutex
  • 给mutex加锁解锁必须是同一线程,必须在同一个上下文中
  • 不能递归加锁解锁,即不能递归地持有同一把锁,也不能对已经解锁的mutex再次解锁
  • 当持有一个mutex时,进程不可以退出
  • mutex不能在中断或者下半部中使用
需求 建议的加锁方法
低开销加锁 考虑自旋锁
短期锁定 考虑自旋锁
长期加锁 考虑互斥量
中断上下文中加上 自旋锁
持有锁需要睡眠 互斥量

7. completion variables

在内核中一个任务需要发出信号通知另一个任务发生了某个特定事件时使用,是同步两个任务的简单方法。
结构completion定义在linux/completion.h

方法 说明
init_completion(struct completion *) 初始化
wait_for_completion(struct completion*) 等待指定的完成变量接收信号
complete(struct completion*) 发出信号唤醒等待任务

内核抢占相关函数

函数 说明
preempt_disable() 增加抢占计数值,从而禁止内核抢占
preempt_enable() 减少抢占计数,当计数降为0时检查和执行被挂起的需调度的任务
preempt_enable_no_resched() 激活内核抢占但不再检查任何被挂起的需调度任务
preempt_count() 返回抢占计数

8. 顺序和屏障

当处理多处理器之间或硬件之间的同步问题时,有时需要程序代码以指定的顺序发出读内存和写内存指令。

编译器和处理器为了提高效率,可能对读和写重新排序,此时可能需要确保顺序的指令屏障(barriers)

资料:
http://www.cnblogs.com/icanth/archive/2012/06/10/2544300.html
https://www.kernel.org/doc/Documentation/memory-barriers.txt
http://blog.csdn.net/cheng_fangang/article/details/41849067
http://blog.sina.com.cn/s/blog_6d7fa49b01014q86.html