【Linux】内核的锁机制——互斥锁,自旋锁

在这里插入图片描述

博客主页:PannLZ
系列专栏:《Linux系统之路》
欢迎关注:点赞收藏✍️留言

文章目录

  • 1.互斥锁
    • 1.1Mutex(互斥锁)
    • 1.2互斥锁API
    • 1.3使用例子
    • 1.4一些规则
  • 2.自旋锁
    • 2.1示例
  • 3.自旋锁和互斥锁的比较:


锁机制有助于不同线程或进程之间共享资源。锁机制可防止过度访问,例如,当一个进程在读取数据时,另一个进程要在同一个地方写入数据,或者两个进程访问同一设备(如相同的GPIO)。内核提供了几种锁机制:

  • 互斥锁
  • 信号量
  • 自旋锁

1.互斥锁

1.1Mutex(互斥锁)

**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.2互斥锁API

**使用互斥锁只需要几个基本的函数

//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组合键),也不会返回。

1.3使用例子

struct mutex my_mutex;
mutex_init(&my_mutex);
/* 在工作或线程内部*/
mutex_lock(&my_mutex);
access_shared_memory();
mutex_unlock(&my_mutex);

1.4一些规则

一些互斥锁必须严格遵守的规则(查看内核源码中的include/linux/mutex.h):

  • 一次只能有一个任务持有互斥锁;这其实不是规则,而是事实。
  • 多次解锁是不允许的。
  • 它们必须通过API初始化。
  • 有互斥锁的任务不可能退出,因为互斥锁将保持锁定,可能的竞争者会永远等待(将睡眠)。
  • 不能释放锁定的内存区域。
  • 持有的互斥锁不得重新初始化。
  • 由于它们涉及重新调度,因此互斥锁不能用在原子上下文中,如Tasklet和定时器。

与wait_queue一样,互斥锁也没有轮询机制。每次在互斥锁上调用mutex_unlock时,内核都会检查wait_list中的等待者。如果有等待者,则其中的一个(且只有一个)将被唤醒和调度;它们唤醒的顺序与它们入睡的顺序相同

2.自旋锁

像互斥锁一样,自旋锁也是互斥机制,它只有两种状态——锁定(已获取),解锁(已释放)

“自旋"这个词在自旋锁(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()释放锁

2.1示例

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上发生抢占,这样就可以保证在获取锁的过程中,当前进程不会被其他进程抢占

3.自旋锁和互斥锁的比较:

自旋锁和互斥锁用于处理内核中并发访问,它们有各自的使用对象。

  • 互斥锁保护进程的关键资源,而自旋锁保护IRQ处理程序的关键部分。
  • 互斥锁让竞争者在获得锁之前睡眠,而自旋锁在获得锁之前一直自旋循环(消耗CPU)。
  • 鉴于上一点,自旋锁不能长时间持有,因为等待者在等待取锁期间会浪费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 检查到锁已经被释放,就会立即获取这个锁,并进入临界区。

你可能感兴趣的:(Linux系统之路,linux,运维,服务器)