Linux 锁机制
引入原因
目前 Linux 系统是一个多进程的系统,对于不同进程之间要保证数据能够稳定访问,kernel 给每个进程都分配了自己的专有虚拟地址,这可以保证数据只在自己的进程中访问。但是有时候又不得不进行进程间的数据访问,比如:
- 数据需要拷贝到其他进程进行处理
- 多进程访问同一变量
- 进程必须彼此等待
既然进程间通信不可避免,那自然而然就会有竞态条件和临界区的概念,也别被名字所吓到,竞态条件就是多进程竞争访问某段数据的情况,而临界区就是这某段数据,为了保护这段数据,Linux 便引入了锁机制。有个问题,既然为了防止这段数据不被竞态访问,那我直接关闭抢占不就可以了,其实并不是这样,这段数据只是不让其他进程访问,那我在临界区进行中断还是可行的,所以关闭中断的方式是不可行的。
那 Linux 为什么要引入多种锁机制,这其实又和性能相关,各种锁需要的性能是不同的,为了让性能最大化,我们需要根据不同的情况,选择不同的锁机制,主要还是为了压榨 cpu 的那一点性能。以上,便是 Linux 锁机制的引入原因
原子锁
- 申明一个原子变量,atmoic_t temp = ATOMIC_INIT(i);//i 为初始值,atmoic_t 其实为 int
- linux 提供的 api
atomic_read(atomic_t *v) 读取原子变量的值
atomic_set(atomic_t *v, int i) 将 v 设置为 i
atomic_add(int i, atomic_t *v) 将 i 加到 v
atomic_add_return(int i, atomic_t *v) 将 i 加到 v ,并返回结果
atomic_sub(int i, atomic_t *v) 从 v 减去 i
atomic_sub_return(int i, atomic_t *v) 从 v 减去 i ,并返回结果
atomic_sub_and_test(int i, atomic_t *v) 从 v 减去 v 。如果结果为0则返回 true ,否则返回 false
atomic_inc(atomic_t *v) 将 v 加1
atomic_inc_and_test(atomic_t *v) 将 v 加1。如果结果为0则返回 true ,否则返回 false
atomic_dec(atomic_t *v) 从 v 减去1
atomic_dec_and_test(atomic_t *v) 从 v 减去1。如果结果为0则返回 true ,否则返回 false
atomic_add_negative(int i, atomic_t *v) 将 i 加到 v 。如果结果小于0则返回 true ,否则返回 false
atomic_add_negative(int i, atomic_t *v) 将 i 加到 v 。如果结果为负则返回 true ,否则返回 false
该部分的实现原理基本都是汇编构成,和 soc 平台强相关,这边就不进行分析了。一个问题,为什么只有 int ,无浮点操作,其实这就跟汇编有关系了。
自旋锁
自旋锁用于保护短的代码段,仅包含简短的 c 代码。自旋锁通过 spinlock_t 数据结构实现,基本可以通过 spin_lock 以及 spin_unlock 达到保护临界区的目的。一个简单的 spin lock 使用方法
//声明锁
spinlock_t lock;
//初始化
lock = SPIN_LOCK_UNLOCKED;
//或者
//spin_lock_init(&lock);
spin_lock(&lock);
...
临界区
...
spin_unlock(&lock);
除了基本的 spin_lock,还有 spin_lock_irqsave/spin_lock_bh
spin_lock_irqsave 不仅会禁止内核抢占,还会关闭 irq ,同时保存 irq 状态,之前关闭的中断还是关的,之前开的中断就还是开的
spin_lock_bh 则除了关闭内核抢占,还会关闭软中断。
spin_lock 的设计其实是针对 SMP 而产生的,在单 cpu 中,由于关闭了抢占,不会产生自旋等待的情况,而在 smp 系统的情况下,由于其他 soc 可能会要求持锁,才会产生自旋的现象。在使用 spin_lock 时,一定要确保当前锁不会在中断处理函数中使用
信号量
DECLARE_MUTEX(mutex)
...
down(&mutex);
/* 临界区 */
up(&mutex);
- down_interruptible 工 作 方 式 与 down 相 同 , 但 如 果 无 法 获 得 信 号 量 , 则 将 进 程 置 于 TASK_INTERRUPTIBLE 状态。因此,在进程睡眠时可以通过信号唤醒。
- down_trylock 试图获取信号量。如果失败,则进程不会进入睡眠等待信号量,而是继续正常 执行。如果获取了信号量,则该函数返回 false 值,否则返回 true。
读写锁
一般大家对读写锁应该有一个认知,当读数据比修改数据频繁,我们可以采用读写锁。通常,任意数目的进程都可以并发读取数据结构,而写访问只能限于一个进程。
读者/写者自旋锁定义为 rwlock_t 数据类型。必须根据读写访问,以不同的方法获取锁。
- 进程对临界区进行读访问时,在进入和离开时需要分别执行 read_lock 和 read_unlock 。内核 会允许任意数目的读进程并发访问临界区。
- write_lock 和 write_unlock 用于写访问。内核保证只有一个写进程(此时没有读进程)能够 处于临界区中。
互斥量
尽管信号量可用于实现互斥量的功能,信号量的通用性导致的开销通常是不必要的。因此,内核 包含了一个专用互斥量的独立实现,它们不依赖信号量。
//声明一个互斥量
struct mutex lock;
//初始化
mutex_init(&lock);
//上锁
mutex_lock(&lock);
//解锁
mutex_unlock(&lock);
RCU
RCU的约束条件
- 对共享资源的访问在大部分时间应该是只读的,写访问应该相对很少。
- 在RCU保护的代码范围内,内核不能进入睡眠状态。
- 受保护资源必须通过指针访问。
rcu_read_lock();
p = rcu_dereference(ptr);
if (p != NULL) {
awesome_function(p);
}
rcu_read_unlock();
总结
锁机制的底层实现基本都由汇编构成,驱动开发者需要的是在各种情况下,灵活运用各种锁,达到性能最优。