OS / linux / 互斥锁实现原理(futex)

零、前言

所谓的锁,在计算机里本质上就是一块内存空间。当这个空间被赋值为 1 的时候表示加锁了,被赋值为 0 的时候表示解锁了,仅此而已。

多个线程抢一个锁,就是抢着要把这块内存赋值为 1 。在一个多核环境里,内存空间是共享的,每个核上各跑一个线程,那如何保证一次只有一个线程成功抢到锁呢?你或许已经猜到了,这必须要硬件的某种保证。

在单核的情况下,关闭 CPU 中断,使其不能暂停当前请求而处理其他请求,从而达到赋值“锁”对应的内存空间的目的。

在多核的情况下,使用锁总线和缓存一致性技术(详情看这里),可以实现在单一时刻,只有某个CPU里面某一个核能够赋值“锁”对应的内存空间,从而达到锁的目的。

实际上 mutex 底层的实现 API 为 Futex

一、什么是 Futex ?

Futex 是 Fast Userspace Mutexes 的缩写。

Futex 按英文翻译过来就是快速用户空间互斥体。其设计思想其实不难理解,在传统的 Unix 系统中,System V IPC(inter process communication),如 semaphores,msgqueues,sockets 还有文件锁机制(flock())等进程间同步机制都是对一个内核对象操作来完成的,这个内核对象对要同步的进程都是可见的,其提供了共享的状态信息和原子操作。当进程间要同步的时候必须要通过系统调用(如semop())在内核中完成。可是经研究发现,很多同步是无竞争的,即某个进程进入互斥区,到再从某个互斥区出来这段时间,常常是没有进程也要进这个互斥区或者请求同一同步变量的。但是在这种情况下,这个进程也要陷入内核去看看有没有人 和它竞争,退出的时侯还要陷入内核去看看有没有进程等待在同一同步变量上。这些不必要的系统调用(或者说内核陷入)造成了大量的性能开销。

为了解决上述这个问题,Futex 就应运而生,Futex 是一种用户态和内核态混合的同步机制。首先,同步的进程间通过 mmap 共享一段内存,futex 变量就位于这段共享的内存中且操作是原子的,当进程尝试进入互斥区或者退出互斥区的时候,先去查看共享内存中的 futex 变量,如果没有竞争发生,则只修改 futex,而不用再执行系统调用了。当通过访问 futex 变量告诉进程有竞争发生,则还是得执行系统调用去完成相应的处理(wait 或者 wake up)。简单的说,futex 就是通过在用户态的检查,(motivation)如果了解到没有竞争就不用陷入内核了,大大提高了 low-contention 时候的效率。 Linux 从 2.5.7 开始支持 Futex 。

二、Futex 系统调用

Futex 是一种用户态和内核态混合机制,所以需要两个部分合作完成,linux 上提供了 sys_futex 系统调用,对进程竞争情况下的同步处理提供支持。其原型和调用号为

    #include <linux/futex.h>
    #include <sys/time.h>
    int futex (int *uaddr, int op, int val, const struct timespec *timeout,int *uaddr2, int val3);
    #define __NR_futex              240

虽然参数有点长,其实常用的就是前面三个,后面的 timeout 大家都能理解,其他的也常被 ignore。

uaddr 就是用户态下共享内存的地址,里面存放的是一个对齐的整型计数器。

op 存放着操作类型。定义的有 5 种,这里我简单的介绍一下两种,剩下的感兴趣的自己去 man futex

  •     FUTEX_WAIT:原子性的检查 uaddr 中计数器的值是否为 val,如果是则让进程休眠,直到 FUTEX_WAKE 或者超时(time-out)。也就是把进程挂到 uaddr 相对应的等待队列上去。

  •     FUTEX_WAKE:最多唤醒 val 个等待在 uaddr 上进程。

可见 FUTEX_WAIT 和 FUTEX_WAKE 只是用来挂起或者唤醒进程,当然这部分工作也只能在内核态下完成。有些人尝试着直接使用 futex 系统调用来实现进程同步,并寄希望获得 futex 的性能优势,这是有问题的。应该区分 futex 同步机制和 futex 系统调用。futex 同步机制还包括用户态下的操作,我们将在下节提到。

三、Futex 同步机制

所有的 futex 同步操作都应该从用户空间开始,首先创建一个 futex 同步变量,也就是位于共享内存的一个整型计数器。

当进程尝试持有锁或者要进入互斥区的时候,对 futex 执行"down"操作,即原子性的给 futex 同步变量减 1。如果同步变量变为 0,则没有竞争发生, 进程照常执行。如果同步变量是个负数,则意味着有竞争发生,需要调用 futex 系统调用的 futex_wait 操作休眠当前进程。

当进程释放锁或者要离开互斥区的时候,对 futex 进行"up"操作,即原子性的给 futex 同步变量加 1 。如果同步变量由 0 变成 1 ,则没有竞争发生,进程照常执行。如果加之前同步变量是负数,则意味着有竞争发生,需要调用 futex 系统调用的 futex_wake 操作唤醒一个或者多个等待进程。

这里的原子性加减通常是用 CAS(Compare and Swap)完成的,与平台相关。CAS的基本形式是:CAS(addr、old、new),当 addr 中存放的值等于 old 时,用 new 对其替换。在 x86 平台上有专门的一条指令来完成它:cmpxchg 。

可见:futex 是从用户态开始,由用户态和核心态协调完成的。

四、进 / 线程利用 futex 同步

进程或者线程都可以利用 futex 来进行同步。

对于线程,情况比较简单,因为线程共享虚拟内存空间,虚拟地址就可以唯一的标识出 futex 变量,即线程用同样的虚拟地址来访问 futex 变量。

对于进程,情况相对复杂,因为进程有独立的虚拟内存空间,只有通过 mmap() 让它们共享一段地址空间来使用 futex 变量。每个进程用来访问 futex 的 虚拟地址可以是不一样的,只要系统知道所有的这些虚拟地址都映射到同一个物理内存地址,并用物理内存地址来唯一标识 futex 变量。

五、小结

  1. Futex 变量的特征:1)位于共享的用户空间中;2)是一个32位的整型;3)对它的操作是原子的。
  2. Futex 在程序 low-contention 的时候能获得比传统同步机制更好的性能。
  3. 不要直接使用 Futex 系统调用。
  4. Futex 同步机制可以用于进程间同步,也可以用于线程间同步。

关于 Futex 部分转载于:https://www.cnblogs.com/ck1020/p/6666298.html

(SAW:Game Over!)
 

  • 1
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值