1.自旋锁
2.信号量
3.互斥锁
4.RCU
5.原子变量
6.完成量
文章列举了各个互斥机制所要用的api以及在什么情况下用哪种互斥,并未对内核中的互斥和同步机制详细分析,
只供今后写代码时查阅,如果想了解详细机制可参考LKD或<<深入Linux设备驱动程序内核机制>>等书.
自旋锁
spin_lock/spin_unlock
因为只禁止抢占,并未对中断做处理,所以不能在中断上下文用,所以便有了以下变体
spin_lock_irq/spin_unlock_irq
禁止抢占,禁止中断,可以在中断上下文用
spin_lock_irqsave/spin_unlock_irqrestore
禁止抢占,禁止中断的同时保存中断前处理器FLAGS寄存器的状态,在ARM上是保存CPSR寄存器
spin_lock_bh/spin_unlock_bh
相对于spin_lock_irq来说,spin_lock_bh关闭的是softirq
非阻塞
spin_trylock
spin_trylock_irq
spin_trylock_irqsave
spin_trylock_bh
读写者自旋锁
如果系统中有大量对共享资源的读操作,但并不会改写其内容,那么用spin_lock就会大大降低系统性能
所以便有了读写者自旋锁rwlock.唯一与自旋锁不同的是可以允许多个读者同时访问,如果有写操作参与那么得互斥
读取者
read_lock/read_unlock
read_lock_irq/read_unlock_irq
read_lock_irqsave/read_unlock_irqrestore
写入者
write_lock/write_unlock
write_lock_irq/write_unlock_irq
write_lock_irqsave/write_unlock_irqrestore
顺序锁
typedef struct {
unsigned sequence;
spinlock_t lock;
} seqlock_t;
顺序锁seqlock的设计思想是写加锁,读不加
为了保证读取数据的过程中不会由写入者参与,便设置了一个sequence值,读取者在开始读取前读取该值,
读取操作完成后再读取该值,看两值是否一致,如果不一致,说明数据被更新,读取操作无效.因此写入者在
开始写入时要更新sequence值
同样,静态初始化
#define DEFINE_SEQLOCK(x) \
seqlock_t x = __SEQLOCK_UNLOCKED(x)
动态
seqlock_init
例子
//定义一个顺序锁变量demo_seqlock
DEFINE_SEQLOCK(demo_seqlock)
//写入者代码
write_seqlock(&demo_seqlock); //实际写之前调用write_seqlock获取自旋锁,同时更新sequence的值
do_write(); //实际的写入操作
write_unseqlock(&demo_seqlock); //写入结束,释放自旋锁
//读取者代码
unsigned start;
do {
//读取操作前先得到sequence的值赋给start,用以在读操作结束后判断是否发生更新
//注意读操作无需获取锁,但是如果有写操作在进行那么会一直循环读取sequence的值,直到写操作结束
//read_seqbegin是通过判断sequence的最低位实现的,写操作完成那么sequence&0返回0,否则返回1
start = read_seqbegin(&demo_seqlock);
do_read(); //实际的读操作
} while (read_seqretry(&demo_seqlock, start)); //如果有数据更新,再重新读取
如果考虑到中断安全问题,可以用
write_seqlock_irq/write_sequnlock_irq
write_seqlock_irqsave/write_sequnlock_irqrestore
write_seqlock_bh/write_sequnlock_bh
read_seqbegin_irqsave
read_seqretry_irqrestore
顺序锁seqlock和之前的读写者自旋锁rwlock其实是相同的,者是读写互斥,写写互斥,读读不互斥
信号量
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
相对于自旋锁来讲,信号量最大的特点就是允许调用它的线程睡眠
定义信号量 struct semaphore
初始化信号量 sema_init(struct semaphore *sem, int val)
信号量主要是DOWN/UP操作,DOWN操作有,不过驱动使用最频繁的是down_interruptible
down
down_interruptible
down_killable
down_trylock
down_timeout
UP操作只有一个
up
即使不是信号量的拥有者也可以调用up函数来释放一个信号量,这一点是与mutex不同的
其实信号量常见的用途就是实现互斥机制,也就是信号量的count为1,也就是任意时刻只允许一个进程进入临界区,用法是
#define DECLARE_MUTEX(name)
以免与mutex产生混淆,Thomas Gleixner在2010年9月7号改为了
#define DEFINE_SEMAPHORE(name) \
struct semaphore name = __SEMAPHORE_INITIALIZER(name, 1)
所以现在我们用DEFINE_SEMAPHORE
读写者信号量
与读写自旋锁一个意思,者是为了提高系统性能
/*
* the rw-semaphore definition
* - if activity is 0 then there are no active readers or writers
* - if activity is +ve then that is the number of active readers
* - if activity is -1 then there is one active writer
* - if wait_list is not empty, then there are processes waiting for the semaphore
*/
struct rw_semaphore {
__s32 activity;
raw_spinlock_t wait_lock;
struct list_head wait_list;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
};
定义
#define DECLARE_RWSEM(name) \
struct rw_semaphore name = __RWSEM_INITIALIZER(name)
初始化
init_rwsem
DOWN操作
down_read
down_read_trylock
down_write
down_write_trylock
UP操作
up_read
up_write
互斥锁
用信号量实现互斥不是Linux中最经典的用法,于是便有了mutex.
struct mutex {
/* 1: unlocked, 0: locked, negative: locked, possible waiters */
atomic_t count;
spinlock_t wait_lock;
struct list_head wait_list;
#if defined(CONFIG_DEBUG_MUTEXES) || defined(CONFIG_SMP)
struct task_struct *owner;
#endif
#ifdef CONFIG_DEBUG_MUTEXES
const char *name;
void *magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
};
定义并初始化,静态
#define DEFINE_MUTEX(mutexname) \
struct mutex mutexname = __MUTEX_INITIALIZER(mutexname)
动态
struct mutex
mutex_init
DOWN/UP操作,得不到锁就去等待队列睡眠
mutex_lock/mutex_unlock
RCU
Read-Copy_Update,即读/写-复制-更新.Linux提供了很多互斥机制,RCU与其他不同的是它是免锁的.
RCU的应用场景也是读取者/写入者,不同的是RCU不用考虑读/写的互斥问题.
简单原理是,将读取者和写入者要访问的共享数据放在指针p指向的区域,读取者通过p来访问数据,而写入者通过
修改这个区域来更新数据.在具体实现上读取者没有太多的事要做,大量的工作都在写入者一方.免锁的实现双方
必须同时遵守一定的规则.
读取者所做的工作是
禁止抢占,对p指针的引用只能在临界区中
写入者做做的工作是
1.分配新的空间ptr
2.更新数据
3.用新指针ptr替换老指针p
4.调用call_rcu释放老指针所指向的空间
注意第4步释放空间时必须确保没有读取者对老指针的引用,内核是通过判断处理器是否发生进程切换来实现的.因为
读取者是禁止抢占的,所以在临界区不会发生进程切换(就单核而言),如果发生进程切换那么就说明不在临界区了
例子
//假设struct shared_data是读者和写者要共同访问的共享数据
struct shared_data {
int a;
int b;
struct rcu_head rcu;
};
//读取者代码
//读取者调用rcu_read_lock/rcu_read_unlock构建它的读取临界区,所有对指向被保护资源指针的引用都应该只出现在临界区中,
//而且临界区中的代码不能睡眠
static void demo_reader(struct shared_data *ptr)
{
struct shared_data *p = NULL;
rcu_read_lock();
p = rcu_dereference(ptr); //调用rcu_dereference获得指向共享数据的指针
if (p)
do_something...
rcu_read_unlock();
}
//写入者代码
//写入者提供的回调函数,用于释放老指针
static void demo_del_oldptr(struct rcu_head *rh)
{
struct shared_data *p = container_of(rh, struct shared_data, rcu);
kfree(p);
}
static void demo_writer(struct shared_data *ptr)
{
struct shared_data *new_ptr = kmalloc(...);
...
new_ptr->a = 30;
new_ptr->b = 40;
rcu_assign_pointer(ptr, new_ptr); //用新指针更新老指针
call_rcu(ptr->rcu, demo_del_oldptr); //调用call_rcu让内核在确保所有对老指针ptr的引用都结束后回调demo_del_oldptr释放老指针所指向的区域
}
和call_rcu类似的还有一个synchronize_rcu,不过后者会阻塞,它会等待所有对老指针的引用都消失后才执行,所以在中断上下文要用call_rcu
原子变量
如果需要保护的数据只是一个简单的整型变量,那么可以用原子变量
typedef struct {
int counter;
} atomic_t;
例子
atomic_t flag = ATOMIC_INIT(0);
//Task A
void add_flag()
{
atomic_inc(&flag);
}
//Task B
void add_flag()
{
atomic_inc(&flag);
}
完成量
struct completion {
unsigned int done;
wait_queue_head_t wait;
};
静态初始化
#define DECLARE_COMPLETION(work) \
struct completion work = COMPLETION_INITIALIZER(work)
动态
init_completion
等待完成所调用的API
wait_for_completion
wait_for_completion_interruptible
wait_for_completion_timeout
wait_for_completion_interruptible_timeout
完成
complete
complete_all