一、自旋锁
若一个进程要访问临界资源,测试锁空闲,则进程获得这个锁并继续执行;若测试结果表明锁扔被 占用,进程将在一个小的循环内重复“测试并设置”操作,进行所谓的“自旋”,等待自旋锁持有者释 放这个锁。自旋锁与互斥锁类似,但是互斥锁不能用在可能睡眠的代码中,而自旋锁可以用在可睡 眠的代码中,典型的应用是可以用在中断处理函数中。自旋锁的相关操作: 自旋锁。
1、定义自旋锁
spinlock_t spin;
2、 初始化自旋锁
spin_lock_init(lock);
3、获得自旋锁:若能立即获得锁,它获得锁并返回,否则,自旋,直到该锁持有者释放
spin_lock(lock);
4、尝试获得自旋锁:若能立即获得锁,它获得并返回真,否则立即返回假,不再自旋
spin_trylock(lock);
5、 释放自旋锁: 与spin_lock(lock)和spin_trylock(lock)配对使用
spin_unlock(lock);
自旋锁持有期间内核的抢占将被禁止。自旋锁可以保证临界区不受别的CPU和本CPU内的抢占进程打扰, 但是得到锁的代码路径在执行临界区的时候还可能受到中断和底半部(BH)的影响。为防止这种影响,需要用到自旋锁的衍生:
spin_lock_irq() = spin_lock() + local_irq_disable()
spin_unlock_irq() = spin_unlock() + local_irq_enable()
spin_lock_irqsave() = spin_lock() + local_irq_save()
spin_unlock_irqrestore() = spin_unlock() + local_irq_restore()
spin_lock_bh() = spin_lock() + local_bh_disable()
spin_unlock_bh() = spin_unlock() + local_bh_enable()
二、 信号量
信号量(semaphore)是用于保护临界区的一种常用方法,它的使用方式和自旋锁类似。与自 旋锁相同,只有得到信号量的进程才能执行临界区代码。但是,与自旋锁不同的是,当获取不到 信号量时,进程不会原地打转而是进入休眠等待状态。
1、定义信号量:
struct semaphore sem;
2、初始化信号量
void sema_init(struct semaphore *sem, int val); // val一般为0或1
#define init_MUTEX(sem) sema_init(sem, 1)
#define init_MUTEX_LOCKED(sem) sema_init(sem, 0)
下面两个宏是定义并初始化信号量的“快捷方式”: DECLARE_MUTEX(name)、 DECLARE_MUTEX_LOCKED(name) 前者定义一个名为 name 的信号量并初始化为 1;后者定义一个名为 name 的信号量并初始化 为 0。
3、获取信号量
int dowm(struct semaphore* sem);
该函数用于获得信号量 sem,它会导致睡眠,因此不能在中断上下文使用
int down_interrupttible(struct semaphore* sem);
该函数功能与 down 类似,不同之处为,因为 down()而进入睡眠状态的进程不能被信号打断, 但因为 down_interruptible()而进入睡眠状态的进程能被信号打断,信号也会导致该函数返回,这 时候函数的返回值非 0;
int dowm_trylock(struct semaphore* sem);
该函数尝试获得信号量 sem,如果能够立刻获得,它就获得该信号量并返回 0,否则,返回 非 0 值。它不会导致调用者睡眠,可以在中断上下文使用。
4、释放信号量
void up(struct semaphore* sem);
5、使用信号量实现设备只能被一个进程打开 范例:
static DECLARE_MUTEX(xxx_lock);
static int xxx_open(struct inode *inode, struct file *filp) {
if(down_trylock(&xxx_lock))
return -EBUSY;
....
return 0; /*成功*/
}
static int xxx_release(struct inode* inode, struct file* filp) {
up(&xxx_lock);
return 0;
}
6、信号量用于同步:
如果信号量被初始化为 0,则它可以用于同步,同步意味着一个执行单元的继续执行需等待 另一执行单元完成某事,保证执行的先后顺序。
7、完成量用于同步:
- 定义完成量
struct completion my_completion; - 初始化 completion
init_completion(&my_completion);
对 my_completion 的定义和初始化可以通过如下快捷方式实现: DECLARE_COMPLETION(my_completion); - 等待完成量 下列函数用于等待一个 completion 被唤醒:
void wait_for_completion(struct completion *c); - 唤醒完成量 下面两个函数用于唤醒完成量:
void complete(struct completion *c);
void complete_all(struct completion *c);
前者只唤醒一个等待的执行单元,后者释放所有等待同一完成量的执行单元。
三、自旋锁VS信号量:
- 当锁不能被获取到时,使用信号量的开销是进程上下文切换时间 Tsw,使用自旋锁的开 销是等待获取自旋锁(由临界区执行时间决定)Tcs,若 Tcs 比较小,宜使用自旋锁,若 Tcs 很大, 应使用信号量
- 信号量所保护的临界区可包含可能引起阻塞的代码,而自旋锁则绝对要避免用来保护包 含这样代码的临界区。因为阻塞意味着要进行进程的切换,如果进程被切换出去后,另一个进程 企图获取本自旋锁,死锁就会发生
- 信号量存在于进程上下文,因此,如果被保护的共享资源需要在中断或软中断情况下使 用,则在信号量和自旋锁之间只能选择自旋锁。当然,如果一定要使用信号量,则只能通过 down_trylock()方式进行,不能获取就立即返回以避免阻塞。
四、总结:
并发和竞态广泛存在,中断屏蔽、原子操作、自旋锁和信号量都是解决并发问题的机制。中 断屏蔽很少单独被使用,原子操作只能针对整数进行,因此自旋锁和信号量应用最为广泛。 自旋锁会导致死循环,锁定期间不允许阻塞,因此要求锁定的临界区小。信号量允许临界区 阻塞,可以适用于临界区大的情况。 读写自旋锁和读写信号量分别是放宽了条件的自旋锁和信号量,它们允许多个执行单元对共 享资源的并发读。
关注公众号"小败日记",搬砖过程遇到的问题,大家一起探讨,资源共享