博客主页:PannLZ
系列专栏:《Linux系统之路》
欢迎关注:点赞收藏✍️留言
锁机制有助于不同线程或进程之间共享资源。锁机制可防止过度访问,例如,当一个进程在读取数据时,另一个进程要在同一个地方写入数据,或者两个进程访问同一设备(如相同的GPIO)。内核提供了几种锁机制:
- 互斥锁
- 信号量
- 自旋锁
**Mutex(互斥锁)**是较常用的锁机制。它在include/linux/mutex.h
中的结构定义:
struct mutex {
/* 1: 解锁, 0: 锁定, negative: 锁定, 可能的等待
*/
atomic_t count;
spinlock_t wait_lock;
struct list_head wait_list;
[...]
};
竞争者从调度器的运行队列中删除,放入处于睡眠状态的等待链表(wait_list)中。然后内核调度并执行其他任务。当锁被释放时,等待队列中的等待者被唤醒,从wait_list
移出,然后重新被调度。
**使用互斥锁只需要几个基本的函数
//1)声明
//静态声明
DEFINE_MUTEX(my_mutex);
//动态声明
struct mutex my_mutex;
mutex_init(&my_mutex)
//2)获取与释放
//锁定
void mutex_lock(struct mutex *lock);
int mutex_lock_interruptible(struct mutex *lock);
int mutex_lock_killable(struct mutex *lock);
//解锁
void mutex_unlock(struct mutex *lock);
//检查互斥锁是否锁定
int mutex_trylock(struct mutex *lock);
//还没有锁定,则它获取互斥锁,并返回1;否则返回0。
int mutex_trylock(struct mutex *lock);
与等待队列的可中断系列函数一样,建议使用
mutex_lock_interruptible()
,它使驱动程序可以被所有信号中断,而对于mutex_lock_killable()
,只有杀死进程的信号才能中断驱动程序。调用
mutex_lock()
时要非常小心,只有能够保证无论在什么情况下互斥锁都会释放时才可以使用它。在用户上下文中,建议始终使用**mutex_lock_interruptible()
**来获取互斥锁,因为mutex_lock()即使收到信号(甚至是Ctrl+C组合键),也不会返回。
struct mutex my_mutex;
mutex_init(&my_mutex);
/* 在工作或线程内部*/
mutex_lock(&my_mutex);
access_shared_memory();
mutex_unlock(&my_mutex);
一些互斥锁必须严格遵守的规则(查看内核源码中的include/linux/mutex.h):
- 一次只能有一个任务持有互斥锁;这其实不是规则,而是事实。
- 多次解锁是不允许的。
- 它们必须通过API初始化。
- 有互斥锁的任务不可能退出,因为互斥锁将保持锁定,可能的竞争者会永远等待(将睡眠)。
- 不能释放锁定的内存区域。
- 持有的互斥锁不得重新初始化。
- 由于它们涉及重新调度,因此互斥锁不能用在原子上下文中,如Tasklet和定时器。
与wait_queue一样,互斥锁也没有轮询机制。每次在互斥锁上调用mutex_unlock时,内核都会检查wait_list中的等待者。如果有等待者,则其中的一个(且只有一个)将被唤醒和调度;它们唤醒的顺序与它们入睡的顺序相同。
像互斥锁一样,自旋锁也是互斥机制,它只有两种状态——锁定(已获取),解锁(已释放)。
“自旋"这个词在自旋锁(Spinlock)的上下文中,是指当一个线程尝试获取一个已经被其他线程持有的锁时,这个线程会进入一个循环不断地检查锁是否已经被释放。这个过程就像一个旋转的陀螺,不停地旋转检查,所以被称为"自旋”。
需要获取自旋锁的所有线程都会激活循环,直到获得该锁为止(退出循环)。这是互斥锁和自旋锁的不同之处。
由于自旋锁在循环过程中会大量消耗CPU,因此在可以快速获取时再应该使用它,尤其是当持有自旋锁的时间比重新调度时间少时。
只要持有自旋锁的代码正在运行,内核就会禁止抢占。禁止抢占可以防止自旋锁持有者被移出运行队列,这会导致等待进程长时间自旋而消耗CPU。
只要有一个任务持有自旋锁,其他任务就可能在等待的时候自旋。用自旋锁时,必须确保不会长时间持有它。
在单核机器上使用自旋锁是没有任何意义的。如果一个线程持有了一个自旋锁,然后被操作系统调度出去,那么其他尝试获取这个锁的线程就会进入忙等待的状态,不断地检查锁是否已经被释放但是,因为只有一个处理器,所以持有锁的线程无法得到处理器,也就无法释放锁,这就导致了一个死锁的情况,所有的线程都在等待锁被释放,但是没有线程能够运行来释放锁。最佳情况下,系统可能会变慢,最糟情况下,和互斥锁一样会造成死锁。正是因为这个原因,内核在处理单个处理器上的spin_lock(spinlock_t *lock)调用时将禁止抢占。在单个处理器(核)系统上,应该使spin_lock_irqsave()
和spin_unlock_ irqrestore()
,它们分别禁用处理器上中断,防止中断并发。
由于事先并不知道所写驱动程序运行在什么系统上,因此建议使用spin_lock_irqsave (spinlock_t*lock, unsigned long flags)
获取自旋锁,该函数会在获取自旋锁之前,禁止当前处理器(调用该函数的处理器)上中断。spin_lock_irqsave
在内部调用local_irq_save (flags)
和preempt_disable()
,前者是一个依赖于体系结构的函数,用于保存IRQ状态,后者禁止在相关CPU上发生抢占。然后应该用spin_unlock_irqrestore()
释放锁
spinlock_t my_spinlock;
spin_lock_init(my_spinlock);
static irqreturn_t my_irq_handler(int irq, void *data)
{
unsigned long status, flags;
spin_lock_irqsave(&my_spinlock, flags);
status = access_shared_resources();
spin_unlock_irqrestore(&gpio->slock, flags);
return IRQ_HANDLED;
}
IRQ状态[IRQ全称是**“Interupt ReQuest”,即“中断请求”。当电脑内的周边硬件需要处理器去执行某些工作时,该硬件就会发出一个硬件信号**,通知处理器工作,而这个信号就是IRQ,
spin_lock_irqsave
函数中,local_irq_save(flags)
这个函数会保存当前的IRQ状态,也就是保存当前处理器的中断使能状态。然后,它会禁止当前处理器上的所有中断,以防止在获取锁的过程中被中断.抢占:抢占是指当一个进程在执行时,操作系统因为某些原因(比如有更高优先级的进程需要运行),会暂停当前进程,将CPU分配给其他进程。spin_lock_irqsave
函数中,
preempt_disable()`这个函数会禁止在当前CPU上发生抢占,这样就可以保证在获取锁的过程中,当前进程不会被其他进程抢占
自旋锁和互斥锁用于处理内核中并发访问,它们有各自的使用对象。
从一个例子理解第二点:
#include
spinlock_t my_lock;
// 初始化自旋锁
spin_lock_init(&my_lock);
void thread_A(void) {
spin_lock(&my_lock); // A 尝试获取锁
// 访问临界区
spin_unlock(&my_lock); // A 释放锁
}
void thread_B(void) {
spin_lock(&my_lock); // B 尝试获取锁
// 访问临界区
spin_unlock(&my_lock); // B 释放锁
}
/*****************************************************/
#include
struct mutex my_lock;
// 初始化互斥锁
mutex_init(&my_lock);
void thread_A(void) {
mutex_lock(&my_lock); // A 尝试获取锁
// 访问临界区
mutex_unlock(&my_lock); // A 释放锁
}
void thread_B(void) {
mutex_lock(&my_lock); // B 尝试获取锁
// 访问临界区
mutex_unlock(&my_lock); // B 释放锁
}
在上面的互斥锁例子中,线程 A 首先尝试获取锁,如果锁是可用的(也就是说,没有其他线程持有这个锁),那么 A 就会获取到这个锁,并进入临界区。此时,如果线程 B 也尝试获取这个锁,因为 A 已经持有这个锁,所以 B 会被阻塞(放入等待队列),直到 A 释放锁为止。
在上面的自旋锁例子中,线程 A 首先尝试获取锁,如果锁是可用的(也就是说,没有其他线程持有这个锁),那么 A 就会获取到这个锁,并进入临界区。此时,如果线程 B 也尝试获取这个锁,因为 A 已经持有这个锁,所以 B 会进入一个忙等待的状态,不断地检查锁是否已经被释放。当 A 完成临界区的访问后,它会释放这个锁。此时,B 检查到锁已经被释放,就会立即获取这个锁,并进入临界区。