简析Linux内核中的各种锁:信号量/互斥锁/读写锁/原子锁/自旋锁/内存屏障等

2025-12-12 05:12:50 8828

首先得搞清楚,不同锁的作用对象不同。 下面分别是作用于临界区、CPU、内存、cache 的各种锁的归纳:

一、atomic原子变量/spinlock自旋锁 — —CPU

既然是锁CPU,那就都是针对多核处理器或多CPU处理器。单核的话,只有发生中断会使任务被抢占,那么可以进入临界区之前先关中断,但是对多核CPU光关中断就不够了,因为对当前CPU关了中断只能使得当前CPU不会运行其它要进入临界区的程序,但其它CPU还是可能执行进入临界区的程序。

原子变量:在x86多核环境下,多核竞争数据总线的时候,提供Lock指令锁住总线,保证“读-修改-写”操作在芯片级的原子性。这个好说,我们一般对某个被多线程会访问的变量设置为atomic类型的即可,比如atomic_int x;或atomic x;

自旋锁: 当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取。使用实例如下:

#include

// 定义自旋锁

spinlock_t my_lock;

void my_function(void)

{

spin_lock(&my_lock);

// 访问共享资源的操作

spin_unlock(&my_lock);

}

互斥锁中,要是当前线程没拿到锁,就会出让CPU;而自旋锁中,要是当前线程没有拿到锁,当前线程在CPU上忙等待直到锁可用,这是为了保证响应速度更快。但是这种线程多了,那意味着多个CPU核都在忙等待,使得系统性能下降。 因此一定不能自旋太久,所以用户态编程里用自旋锁保护临界区的话,这个临界区一定要尽可能小,锁的粒度得尽可能小。

为什么自旋锁的响应速度会比互斥锁更快?

在小林coding中说到,自旋锁是通过 CPU 提供的 CAS 函数(Compare And Swap),在「用户态」完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销也小一些。 而互斥锁则不是,前面说互斥锁加锁失败,线程会出让CPU,这个过程其实是由内核来完成线程切换的,因此加锁失败时,1)首先从用户态切换至内核态,内核会把线程的状态从「运行」状态设置为「睡眠」状态,然后把 CPU 切换给其他线程运行;2)当互斥锁可用时,之前「睡眠」状态的线程会变为「就绪」状态(要进入就绪队列了),之后内核会在合适的时间,把 CPU 切换给该线程运行。 然后返回用户态。 这个过程中,不仅有用户态到内核态的切换开销,还有两次线程上下文切换的开销。 线程的上下文切换主要是线程栈、寄存器、线程局部变量等。 而自旋锁在当前线程获取锁失败时不会进行线程的切换,而是一直循环等待直到获取锁成功。因此,自旋锁不会切换至内核态,也没有线程切换开销。 所以如果这个锁被占有的时间很短,或者说各个线程对临界区是快进快出,那么用自旋锁是开销最小的! 自旋锁的缺点前面也说了,就是如果自旋久了或者自旋的线程数量多了,CPU的利用率就下降了,因为上面执行的每个线程都在忙等待— —占用了CPU但什么事都没做。

二、信号量/互斥锁 — —临界区

信号量: 信号量(信号灯)本质是一个计数器,是描述临界区中可用资源数目的计数器。 信号量为3,表示可用资源为3。加入初始信号量为3,某时刻信号量为1,说明可用资源数为1,那么有2个进程/线程在使用资源或者说有两个资源被消耗了(具体资源是什么得看具体情况)。进程对信号量有PV操作,P操作就是进入共享资源区前-1,V操作就是离开共享资源后+1(这个时候信号量就表明还可以允许多少个进程进入该临界区)。 信号量进行多线程通信编程的时候,往往初始化信号量为0,然后用两个函数做线程间同步: sem_wait():等待信号量,如果信号量的值大于0,将信号量的值减1,立即返回。 如果信号量的值为0,则线程阻塞。 sem_post():释放资源,信号量+1 ,相当于unlock,这样执行了sem_wait()的线程就不阻塞了。

要注意:信号量本身也是个共享资源,它的++操作(释放资源)和--操作(获取资源)也需要保护。其实就是用的自旋锁保护的。如果有中断的话,会把中断保存到eflags寄存器,待操作完成,就去该寄存器上读取,然后执行中断。

struct semaphore {

spinlock_t lock; // 自旋锁

unsigned