加锁和互斥的目的是为了保护共享资源(数据或外设地址)不被多个线程同时访问,而不是保护代码被同时执行
一、信号量
信号量为1表示资源可用,为0表示资源不可用,对信号量的加减主要涉及PV操作,进入临界区调用P操作(down)将信号量减1,推出临界区调用V操作(up)将信号量加1.
1. 信号量的实现
struct semaphore *sem;
void semaphore_init(struct semaphore *sem, int val);
互斥锁是一种简单的信号量,它的值只有0和1,但是信号量的值可以是更大的值,表示有多个线程在使用临界区的资源
DECLARE_MUTEX(name);
DECLARE_MUTEX_LOCKED(name);
void init_MUTEX(struct semaphore *sem);
void init_MUTEX_LOCKED(struct semaphore *sem);
void down(struct semaphore *sem); //
int down_interruptible(struct semaphore *sem); //可以中断
int down_trylock(struct semaphore *sem); // 不可以睡眠
信号量可以指定获取信号的线程是否可以被中断
2. Completions 机制
在使用信号量对临界区进行加锁的时候,如果一旦解锁,那么所有想要进入临界区的线程就会开始对这个临界区的使用权进行抢占,这个时候就会出现线程之间相互竞争的情况,这样就会导致性能受损。所以提出了一种completion机制,它允许一个线程告诉另一个线程工作已经完成.
DECLARE_COMPLETION(my_completion); //定义一个completion
void wait_for_completion(struct completion *c); //这个函数进行一个不可打断的等待. 如果你的代码调 用 wait_for_completion 并且没有人完成这个任务, 结果会是一个不可杀死的进程.
void complete(struct completion *c); //唤醒一个等待线程
void complete_all(struct completion *c); //唤醒所有等待线程
3. 自旋锁
自旋锁只有两个操作,上锁和解锁,自旋锁最大的特点是被加锁的代码不能被中断,也不能睡眠,这样的缺点是当一个线程占用CPU并且没有解锁,那么其他线程将一直处于自旋等待状态,甚至两个线程分别等待对方解锁时,可能导致死锁的出现。
spinlock_t my_lock = SPIN_LOCK_UNLOCKED; //定义一个自旋锁
void spin_lock_init(spinlock_t *lock);
void spin_lock(spinlock_t *lock); //获取一个自旋锁
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags); //获取自旋锁前禁止本CPU的irq中断,并保存中断状态,用于解锁时恢复中断状态
void spin_lock_irq(spinlock_t *lock); //获取自旋锁前禁止本CPU的irq中断
void spin_lock_bh(spinlock_t *lock) //获取自旋锁前禁止本CPU的软中断,一般用于中断的下半部分,比如tasklet,工作队列,schedule,软中断等
void spin_unlock(spinlock_t *lock); //相应的解锁函数
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);
void spin_unlock_irq(spinlock_t *lock);
void spin_unlock_bh(spinlock_t *lock);
4. 读者写者锁
不管是信号量还是自旋锁都有读者和写者锁函数,它允许读者存在多个,但是写者只能有一个,也就是说,在写者执行的时候,必须没有读者,写者具有排他性。
5. 原子变量
一个共享资源是一个简单的整数值,这个整数可能同时被多个设备使用,一个原子变量只是一个简单的变量,如果用一个加锁体制来实现锁操作实在是太奢侈了,所以内核提供了更轻量的原子变量加锁操作。
原子变量的类型是atomic_t
void atomic_set(atomic_t *v, int i);
atomic_t v = ATOMIC_INIT(0);
int atomic_read(atomic_t *v);
void atomic_add(int i, atomic_t *v);
void atomic_sub(int i, atomic_t *v);
void atomic_inc(atomic_t *v);
void atomic_dec(atomic_t *v);
int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
int atomic_sub_and_test(int i, atomic_t *v)
int atomic_add_return(int i, atomic_t *v);
int atomic_sub_return(int i, atomic_t *v);
int atomic_inc_return(atomic_t *v);
int atomic_dec_return(atomic_t *v);
atomic_sub(amount, &first_atomic);
atomic_add(amount, &second_atomic);
6. 原子位操作
void set_bit(nr, void *addr);
void clear_bit(nr, void *addr);
void change_bit(nr, void *addr);
test_bit(nr, void *addr);
int test_and_set_bit(nr, void *addr);
int test_and_clear_bit(nr, void *addr);
int test_and_change_bit(nr, void *addr);
7. seqlock 锁
seqlock 在这种情况下工作, 要保护的资源小, 简单, 并且常常被存取, 并且很少写存取但是必须要快.
基本上, 它们通过允许读者释放对资源的存取, 但是要求这些读者来检查与写者的冲突而工作, 并且当发生这样的冲突时, 重试它们的存取. seqlock 通常不能用在保护包含指针的数据结构, 因为读者可能跟随着一个无效指针而写者在改变数据结构.
seqlock_t lock1 = SEQLOCK_UNLOCKED;
seqlock_t lock2;
seqlock_init(&lock2);
void write_seqlock(seqlock_t *lock)
void write_sequnlock(seqlock_t *lock);
void write_seqlock_irqsave(seqlock_t *lock, unsigned long flags);
void write_seqlock_irq(seqlock_t *lock);
void write_seqlock_bh(seqlock_t *lock);
void write_sequnlock_irqrestore(seqlock_t *lock, unsigned long flags);
void write_sequnlock_irq(seqlock_t *lock);
void write_sequnlock_bh(seqlock_t *lock);
8. 读取-拷贝-更新
读取-拷贝-更新(RCU) 是一个高级的互斥方法
RCU 对它所保护的数据结构设置了不少限制. 它对经常读而极少写的情况做了优化. 被保护的资源应当通过指针来存取, 并且所有对这些资源的引用必须由原子代码持有. 当数据结构需要改变, 写线程做一个拷贝, 改变这个拷贝, 接着使相关的指针对准新的版本 --因此, 有了算法的名子. 当内核确认没有留下对旧版本的引用, 它可以被释放.
作为在真实世界中使用 RCU 的例子, 考虑一下网络路由表. 每个外出的报文需要请求检查路由表来决定应当使用哪个接口. 这个检查是快速的, 并且, 一旦内核发现了目标接口,它不再需要路由表入口项. RCU 允许路由查找在没有锁的情况下进行, 具有相当多的性能好处. 内核中的 Startmode 无线 IP 驱动也使用 RCU 来跟踪它的设备列表.
rcu_read_lock();
rcu_read_unlock();
void call_rcu(struct rcu_head *head, void (*func)(void *arg), void *arg);
给定的 func 在释放资源是安全的时候调用; 传递给 call_rcu 的是给同一个 arg. 常常func 需要的唯一的东西是调用 kfree.
9. 注意事项
a. 不允许一个锁的持有者第二次请求锁
b. 如果一个线程需要获得多个锁,那么其他线程在获取这些锁的时候,必须也要用相同的顺序获取。比如一个线程要获取三个锁L1,L2和L3,那么另一个线程在获取这些锁的时候,也必须以相同的顺序去请求,不能出现L2,L1或者L3,L2的情况。
c. 现在内核中出现了很多种类的锁,主要是一些细粒度的锁,它将根据不同的子系统进行划分,比如输入输出子系统,网络子系统等等,细粒度的锁给系统带来一定的开销和维护难度
d. 使用生产者和消费者的模型,也可以避免锁的使用