linux自旋锁和futex

深入理解Linux自旋锁与futex机制
本文详细探讨了Linux系统中的两种同步机制——自旋锁和futex。自旋锁在多线程环境下用于保护临界区,防止资源争抢,而futex是一种轻量级的进程间同步原语,它结合了用户空间和内核空间的操作,减少了上下文切换的开销。通过实例解析,揭示了这两种机制在并发控制中的应用和优势。


自 旋锁最初是为了在多处理器系统(SMP)使用而设计的,但是只要考虑到并发问题,单处理器在运行可抢占内核时其行为就类似于SMP。因此,自旋锁对于 SMP和单处理器可抢占内核都适用。可以想象,当一个处理器处于自旋状态时,它做不了任何有用的工作,因此自旋锁对于单处理器不可抢占内核没有意义,实际 上,非抢占式的单处理器系统上自旋锁被实现为空操作,不做任何事情。

在Linux下,信号量和线程互斥锁的实现都是通过futex系统调用。

futex(快速用户区互斥的简称)是一个在Linux上实现锁定和构建高级抽象锁如信号量和POSIX互斥的基本工具。它们第一次出现在内核开发的2.5.7版;其语义在2.5.40固定下来,然后在2.6.x系列稳定版内核中出现。

Futex 是fast userspace mutex的缩写,意思是快速用户空间互斥体。Linux内核把它们作为快速的用户空间的锁和信号量的预制构件提供给开发者。Futex非常基础,借助其自身的优异性能,构建更高级别的锁的抽象,如POSIX互斥体。大多数程序员并不需要直接使用Futex,它一般用来实现像NPTL这样的系统库。

Futex 由一块能够被多个进程共享的内存空间(一个对齐后的整型变量)组成;这个整型变量的值能够通过汇编语言调用CPU提供的原子操作指令来增加或减少,并且一个进程可以等待直到那个值变成正数。Futex 的操作几乎全部在应用程序空间完成;只有当操作结果不一致从而需要仲裁时,才需要进入操作系统内核空间执行。这种机制允许使用 futex 的锁定原语有非常高的执行效率:由于绝大多数的操作并不需要在多个进程之间进行仲裁,所以绝大多数操作都可以在应用程序空间执行,而不需要使用(相对高代价的)内核系统调用。 


补充:

锁定一个自旋锁的函数有四个:

void spin_lock(spinlock_t *lock);      

最基本得自旋锁函数,它不失效本地中断。

void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);

在获得自旋锁之前禁用硬中断(只在本地处理器上),而先前的中断状态保存在flags中

void spin_lockirq(spinlock_t *lock);

在获得自旋锁之前禁用硬中断(只在本地处理器上),不保存中断状态

void spin_lock_bh(spinlock_t *lock);

在获得锁前禁用软中断,保持硬中断打开状态


----------------------------------------------------------------
插播一段关于x86原子操作指令的说明:

cmpxchg 比较交换指令,其语义为:

int CompareAndExchange(int *ptr, int old, int new)
{
int actual = *ptr;
if (actual == old)
*ptr = new;
return actual;
}


Intel白皮书上的说明如下:
(* Accumulator = AL, AX, EAX, or RAX depending on whether a byte, word, doubleword, or
quadword comparison is being performed *)
IF accumulator = DEST
    THEN
        ZF ← 1;
        DEST ← SRC;
    ELSE
        ZF ← 0;
        accumulator ← DEST;
FI;

自旋锁是一个互斥设备,它只有两个值:“锁定”和“解锁”。它通常实现为某个整数值中的某个位。希望获得某个特定锁得代码测试相关的位。如果锁可用,则“锁定”被设置,而代码继续进入临界区;相反,如果锁被其他人获得,则代码进入忙循环(而不是休眠,这也是自旋锁和一般锁的区别)并重复检查这个锁,直到该锁可用为止,这就是自旋的过程。“测试并设置位”的操作必须是原子的,这样,即使多个线程在给定时间自旋,也只有一个线程可获得该锁。

自 旋锁最初是为了在多处理器系统(SMP)使用而设计的,但是只要考虑到并发问题,单处理器在运行可抢占内核时其行为就类似于SMP。因此,自旋锁对于 SMP和单处理器可抢占内核都适用。可以想象,当一个处理器处于自旋状态时,它做不了任何有用的工作,因此自旋锁对于单处理器不可抢占内核没有意义,实际 上,非抢占式的单处理器系统上自旋锁被实现为空操作,不做任何事情。


使用此原子操作可以实现自旋锁,之前有一篇文章中描述了实现:

void lock(lock_t *lock) {
while (CompareAndExchange(&lock->flag, 0, 1) == 1)
; // spin
}
void unlock(lock_t *lock) {
lock->flag = 0;
}


关于smp下的原子操作的一些说明:
  原子操作是不可分割的,在执行完毕不会被任何其它任务或事件中断。在单处理器系统(UniProcessor)中,能够在单条指令中完成的操作都可以认为是" 原子操作",因为中断只能发生于指令之间。这也是某些CPU指令系统中引入了test_and_set、test_and_clear等指令用于临界资源互斥的原因。在对称多处理器(Symmetric Multi-Processor)结构中就不同了,由于系统中有多个处理器在独立地运行,即使能在单条指令中完成的操作也有可能受到干扰。
  在x86 平台上,CPU提供了在指令执行期间对总线加锁 的手段。CPU芯片上有一条引线#HLOCK pin,如果汇编语言的程序中在一条指令前面加上前缀"LOCK" ,经过汇编以后的机器代码就使CPU在执行这条指令的时候把#HLOCK pin的电位拉低,持续到这条指令结束时放开,从而把总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的原子性。
  当然,并不是所有的指令前面都可以加lock前缀的,只有ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG,DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, 和 XCHG指令前面可以加lock指令,实现原子操作。
----------------------------------------------------------------

广告回来了,我们继续。

futex保存在用户空间的共享内存中,并且通过原子操作进行操作。在大部分情况下,资源不存在争用的情况下,进程或者线程可以立刻获得资源成功,实际上就没有必要调用系统调用,陷入内核了。实际上,futex的作用就在于减少系统调用的次数,来提高系统的性能。

线程互斥锁pthread_mutex_t的实现原理:

pthread_mutex_lock:
atomic_dec(pthread_mutex_t.value);
if(pthread_mutex_t.value!=0)
   futex(WAIT)
else
   success

pthread_mutex_unlock:
atomic_inc(pthread_mutex_t.value);
if(pthread_mutex_t.value!=1) 
   futex(WAKEUP) 
else
   success

信号量sem_t的实现原理(直接从glibc/nptl/DESIGN-sem.txt中摘的):

sem_wait(sem_t *sem)
{
  for (;;) {

    if (atomic_decrement_if_positive(sem->count))
      break;

    futex_wait(&sem->count, 0)
  }
}

sem_post(sem_t *sem)
{
  n = atomic_increment(sem->count);
  // Pass the new value of sem->count
  futex_wake(&sem->count, n + 1); 
}


对比,pthread_mutex_unlock()和sem_post()的实现,我们发现一个不同点,sem_post()无论如何都会调用 futex_wake(),进行系统调用。但是pthread_mutex_unlock()却符合futex的初衷,只有在需要仲裁的时候才调用 futex_wake()。那么什么是仲裁条件呢?

前面说过信号量和线程互斥锁语义上的区别在于信号量的value>=0,而线程互斥锁的value可以为负数。
对于lock操作,这两个倒是没有多少差别。信号量只要value>0就可以获得资源,线程互斥锁需要value=1。
但是对于unlock操作,这两个就有一些差别了。信号量和线程互斥锁,都会增加对应的value。如果加1后,value为1,对于线程互斥锁来讲,实际上表明资源可用,并且之前没有其他的线程在等待这个资源;否则说明还有其他线程在等待这个资源,需要调用futex系统调用唤醒它们。但是对于信号量,由于value必须>=0。那么加1后,即使value为1,也无法判定现在没有其他的进程或线程正在等待资源,所以必须调用futex系统调用。 例如:

#include
#include
#include

sem_t sem_a;
void *task1();

int main(void)
{
 int ret=0;
 pthread_t thrd1;
 pthread_t thrd2;
 sem_init(&sem_a,0,1);
 ret=pthread_create(&thrd1,NULL,task1,NULL); //创建子线程
 ret=pthread_create(&thrd2,NULL,task1,NULL); //创建子线程
 pthread_join(thrd1,NULL); //等待子线程结束
 pthread_join(thrd2,NULL); //等待子线程结束
}

void *task1()
{
  int sval = 0;
  sem_wait(&sem_a); //持有信号量
  sleep(5); //do_nothing
  sem_getvalue(&sem_a,&sval);
  printf("sem value = %d/n",sval);
  sem_post(&sem_a); //释放信号量
}

上面sem的value初始化为1,但是有两个线程争用资源。那么第一个线程获得资源成功,当它unlock的时候,sem的value变为1。但是,这个时候,实际上还有一个线程在等待资源。因此,必须要进行futex_wake()系统调用,唤醒等待资源的线程。

感兴趣的同学可以使用strace跟踪一下,进行验证。要注意忽略程序运行初始化的那个futex_wake ;-)


<think>我们被要求解释自旋锁的概念、工作原理应用场景,并参考了提供的引用内容。同时,我们需要在回答中自然地添加引用标识,并在最后生成相关问题。 自旋锁的定义: 自旋锁是一种同步机制,用于保护共享资源,避免多线程竞争。当一个线程尝试获取自旋锁时,如果锁已经被其他线程持有,那么该线程不会立即进入睡眠状态(阻塞),而是会循环检查(自旋)直到锁被释放。 工作原理: 1. 尝试获取锁:线程通过原子操作(如CAS)尝试获取锁。 2. 如果锁可用(未被持有),则获取锁并进入临界区。 3. 如果锁不可用,线程不会放弃CPU,而是循环检查锁的状态(自旋),直到锁被释放。 4. 当持有锁的线程完成临界区操作后,通过原子操作释放锁。 自旋锁的特点: - 忙等待:线程在等待锁时不会阻塞,而是占用CPU循环检查。 - 适用于短临界区:因为长时间自旋会浪费CPU资源。 - 在用户态实现:无需陷入内核,因此没有上下文切换的开销。 引用中的关键点: 引用[1]提到自旋锁是高效的轻量级同步机制,用于多线程资源竞争。 引用[2]说明自旋锁分为公平非公平两种,非公平锁不保证先来先服务,公平锁通过队列保证顺序。 引用[3]指出自旋锁在获取锁时如果锁已被持有,则自旋等待,直到持有者释放锁。同时提到自旋锁可能存在的问题:死锁(如递归调用)长时间自旋浪费CPU。 引用[4]强调自旋锁避免了进程上下文的调度开销,因此适用于线程阻塞时间很短的场合。同时指出单核单线程CPU不适合自旋锁(因为持有锁的线程无法运行),但现在的单核CPU通常支持多线程(超线程技术),所以这个问题有所缓解。 实现机制(以非公平自旋锁为例): 通常使用一个原子变量(如整数或布尔值)表示锁的状态。例如,0表示空闲,1表示被占用。 获取锁: while (true) { if (原子操作将锁状态从0改为1成功) break; } 释放锁:将锁状态原子地设置为0。 应用场景: 1. 临界区操作非常短(如几条指令),且线程数不多于CPU核心数(避免其他线程无法释放锁)。 2. 内核中断处理程序(不能睡眠)。 3. 在用户态实现高性能的同步原语(如并发数据结构)。 注意事项: 1. 自旋锁在单核系统上需要配合抢占式调度使用(否则自旋线程会一直占用CPU,导致持有锁的线程无法运行)。 2. 自旋时间过长会浪费CPU资源,因此需要设置自旋上限或自适应自旋。 与互斥锁的区别: 互斥锁在获取锁失败时,线程会进入阻塞状态,让出CPU,直到锁被释放时被唤醒。这涉及上下文切换(用户态到内核态),开销较大,但等待期间不占用CPU。自旋锁则一直占用CPU,但避免了上下文切换。因此,对于短临界区,自旋锁效率更高;对于长临界区,互斥锁更合适。 根据以上分析,我们组织回答如下:</think>### 自旋锁详解:概念、原理与应用场景 #### 一、自旋锁的核心概念 自旋锁(Spinlock)是一种**轻量级线程同步机制**,用于解决多线程环境下的资源竞争问题。其核心特点是:当线程尝试获取锁失败时,不会立即进入阻塞状态,而是通过**循环检查(自旋)** 持续尝试获取锁,直到成功为止[^1][^3]。 数学描述: 设锁状态 $S \in \{0,1\}$,其中: - $S=0$ 表示锁空闲 - $S=1$ 表示锁被占用 线程获取锁的操作可表示为原子操作: $$\text{CAS}(S, 0, 1) = \begin{cases} \text{成功} & \text{if } S=0 \\ \text{自旋重试} & \text{if } S=1 \end{cases}$$ #### 二、工作原理与实现机制 ##### 1. 基本工作流程 ```mermaid graph TD A[线程尝试获取锁] --> B{锁是否空闲?} B -- 是 --> C[获取锁成功] B -- 否 --> D[CPU自旋等待] D --> B C --> E[执行临界区代码] E --> F[释放锁] ``` ##### 2. 关键实现技术 - **原子操作**:使用 CPU 原子指令(如 x86 的 `LOCK CMPXCHG`)确保锁状态修改的原子性[^1] - **内存屏障**:通过 `std::memory_order_acquire/release` 保证内存可见性 - **忙等待策略**:伪代码实现: ```cpp while (!atomic_compare_exchange_weak(&lock, 0, 1)) { // 自旋期间可插入CPU优化指令 __asm__ ("pause"); // x86架构优化 } ``` ##### 3. 公平性策略 - **非公平锁**(默认):线程按运行时状态竞争,可能导致饥饿[^2] - **公平锁**:通过队列(如 MCS 锁)实现先进先出(FIFO)调度[^2] $$Q = \{T_1, T_2, ..., T_n\} \quad \text{(线程等待队列)}$$ #### 三、与互斥锁的关键区别 | 特性 | 自旋锁 | 互斥锁 | |--------------|----------------------------|----------------------------| | **等待方式** | 忙等待(CPU循环) | 阻塞等待(线程休眠) | | **开销** | 无上下文切换开销 | 需内核介入,开销较大 | | **适用场景** | 短临界区(通常 < 1μs) | 长临界区 | | **CPU使用** | 等待时占用CPU | 等待时不占用CPU | | **实现层级** | 用户态实现 | 需内核支持 | 数学效率对比: 设临界区执行时间 $t_c$,上下文切换时间 $t_s$ - 当 $t_c < t_s$ 时:自旋锁效率更高[^4] - 当 $t_c > t_s$ 时:互斥锁更优 #### 四、典型应用场景 1. **操作系统内核** - 中断处理程序(不可睡眠场景) - 短资源保护(如页表更新) 2. **高性能并发数据结构** ```cpp // 线程安全计数器 void increment() { spinlock_acquire(&lock); count++; // 短操作 spinlock_release(&lock); } ``` 3. **用户态高性能库** - Windows SRW Lock(轻型读写锁)[^4] - Linux futex 快速路径 4. **多核CPU环境** 要求:CPU核心数 ≥ 竞争线程数,避免死锁[^4] #### 五、局限性及解决方案 1. **CPU 空转问题** - 优化:自旋阈值(如 Linux 的 `ticket_spinlock`) - 方案:$$ \text{自旋} \times N \text{次} \rightarrow \text{切换为阻塞} $$ 2. **单核CPU限制** 单核单线程环境下易死锁(需禁用或配合抢占调度)[^4] 3. **优先级反转风险** 解决方案:优先级继承协议(如 Linux 的 `rt_mutex`) 4. **缓存抖动** 优化:队列自旋锁(如 MCS 锁)减少总线争用 #### 六、现代实现优化 1. **自适应自旋** 动态调整自旋时间: $$ T_{\text{spin}} = \alpha \times T_{\text{prev\_wait}} $$ 2. **层级锁** 减少缓存同步开销: ```c struct qnode { bool locked; struct qnode *next; } ``` 3. **硬件辅助** - Intel TSX(事务同步扩展) - ARM LDREX/STREX 指令 > **关键设计原则**:临界区执行时间应远小于线程切换时间($t_c \ll t_s$),否则应选用互斥锁[^3][^4]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值