一、Linux内核锁保护
(1)为什么要保护
如果模块中的某个全局变量可以被多个进程/中断同时访问,那么就必须要提供加锁机制进行保护。同时写是不允许的。
(2)和加锁保护相关的名词
A、临界区(critical region)
访问要保护的变量的代码段,称为临界区。临界区中同一时间只能一个人进入。临界区的代码可能分散在不同的函数中。如果要对临界区加锁,则必须保证在临界区的所有部分都加锁。如果有遗漏,则加锁是不可靠的。
B、同步(synchronization)
其实就是加锁。同步是保证临界区只进一个人的机制。
C、竞争(race condition)
如果有多个人同时进入临界区,就称为竞争。一般来说,认为出现竞争是错误的。
(3)加锁的原则
A、加锁是自愿的
内核不强迫程序加锁,但一旦使用了锁,驱动设计人员应该保证正确地使用锁,并保证在临界区的每个部分都加上锁。
B、不要用锁来实现功能
锁就是用来避免对变量同时访问,是一种保护机制。如果将加锁相关代码去掉,程序功能应该没有任何变化。
C、应该在一开始设计代码时,就考虑如何加锁
一定不要代码设计完毕后再加锁,很容易出问题。最好在设计私有结构体时,就开始考虑锁的使用。锁一般会包括到私有结构体中,根据设备的复杂程度,有可能需要多把锁。
(4)死锁
如果加锁不恰当,可能出现死锁。有两种常见的死锁情况:自死锁和ABBA死锁。
二、Linux内核中的哪些机制导致必须要加锁
换句话说,如果没有这些机制,就可以不加锁了。也就是说,如果当前内核不可能出现“同时”,则不必加锁。
可能导致同时访问变量的内核机制有:
(1)SMP
(2)中断
(3)内核抢占(preempt)
(4)schedule()
需要了解这些机制,才能够正确使用加锁函数。
三、原子变量(atomic_t)
对于i++类型的临界区,如果用普通的加锁保护,效率太低,因此内核推荐用atomic_t类型的变量来替代传统的int型变量。对atomic_t类型的变量,内核提供更简单、安全的访问。只要访问atomic_t变量,就必须使用内核提供的函数。注意!不同的处理器实现原子操作的方式不同。
#include <linux/atomic.h> //或<asm/atomic.h>
//由于atomic_t的实现和具体处理器相关,因此参考头文件arch/arm/include/asm/atomic.h
//声明并初始化atomic_t变量
atomic_t mycnt = ATOMIC_INIT(5);
//或者
atomic_set(&mycnt, 5);
//获得atomic_t中的计数
int i = atomic_read(&mycnt);
//变量的++
atomic_inc(&mycnt);
//有几个最常见的atomic函数,对应int型的操作:
atomic_add // +=
atomic_sub // -=
atomic_inc // ++
atomic_dec // --
四、自旋锁(spinlock_t)
内核中用的最多的锁,轻量级的锁,一般加解锁和等待锁的时间在ns级。
spinlock_t的特性:
(1)临界区中只能进入一个人
(2)等待的人忙等(只有SMP才会真正忙等)
(3)持有锁的人不能睡眠
结合前面讨论的内核机制,可以知道,spinlock可以针对SMP,中断和内核抢占提供保护。但schdule()不行。也就是说,如果临界区中必须调用kmalloc(size, GFP_KERNEL),或者copy_to/from_user之类可能导致睡眠的函数,则不能用spinlock保护。
#include <linux/spinlock.h>
//声明锁
spinlock_t mylock;
//使用之前要先初始化
spin_lock_init(&mylock);
(1)普通的加解锁
spin_lock(&mylock);
... //临界区
spin_unlock(&mylock);
(2)如果临界区可能被中断处理函数打断,并影响到要保护的变量,则应该在加锁的同时关闭中断。加锁同时关中断,将CPSR的当前值存储到flags中;解锁时打开中断,并将flags的值恢复到CPSR中
unsigned long flags;
spin_lock_irqsave(&mylock, flags);
... //临界区
spin_unlock_irqrestore(&mylock, flags);
spinlock_t的一个典型应用是保护链表;由于内核要求同一时间只能一个人访问list_head链表,因此一般要用spinlock_t保护。
五、互斥锁(mutex)
mutex锁也是内核中非常重要的锁,属于重量级的锁,等待锁的时间常常为ms级。
mutex的特性:
(1)临界区中一个人
(2)睡眠等
(3)持有锁时可以睡眠(必须确保能被唤醒)
如果临界区中包含中断处理函数的代码,由于获得mutex时可能导致睡眠,因此不能用mutex来保护。其他情况都可以。
#include <linux/mutex.h>
//声明mutex锁
struct mutex mylock;
//用之前要先初始化
mutex_init(&mylock);
//加锁和解锁
mutex_lock(&mylock);
//或者
ret = mutex_lock_interruptible(&mylock);
if (ret)
return -ERESTARTSYS;
... //临界区
mutex_unlock(&mylock);
六、信号量(semaphore)
2.6内核早期没有实现mutex锁,用semaphore来实现mutex锁的功能。在现在的内核中,基本上已经不用semaphore来保护临界区。如果内核中某些资源限定了访问人数(比如只允许3个人同时访问),这时候可以用semaphore进行保护。
#include <linux/semaphore.h>
//声明信号量
struct semaphore mysem;
//按照资源的限定初始化信号量
sema_init(&mysem, 3);
down(&mysem);
//或者
ret = down_interruptible(&mysem);
... //访问受限资源的代码
up(&mysem);
七、其他的锁:
rwlock_t(读写锁,照顾读者,允许多个读者同时进入临界区,可能造成写者机饿)
seqlock_t(顺序锁,克服了写者肌饿,但允许读写同时进行,无法保护list_head等)