Futex系统调用,Futex机制,及具体案例分析

article/2025/9/30 14:47:54

Futex

    • 1、背景
      • 1.1 自己实现锁
        • 1.1.1 自旋锁
        • 1.1.2 sleep+自旋
        • 1.1.3 小结
      • 1.2 futex
        • 1.2.1 什么是Futex
        • 1.2.2 futex诞生之前
        • 1.2.3 futex诞生之后
    • 2、Futex系统调用
    • 3、Futex机制
    • 4、具体案例分析
      • 4.1 在Bionic中的实现
      • 4.2 C语言实现
    • 5、参考及扩展阅读

首先要区分一下futex系统调用和futex机制。futex系统调用是操作系统提供给上层的系统调用接口。而futex机制是使用futex接口实现的一种锁。

1、背景

线程同步可以说在日常开发中是用的很多,但对于其内部如何实现的,一般人可能知道的并不多。本篇文章将从如何实现简单的锁开始,介绍linux中的锁实现futex的优点及原理。

1.1 自己实现锁

1.1.1 自旋锁

最容易想到可能是自旋:


volatile int status=0;void lock(){while(!compareAndSet(0,1)){}//get lock}void unlock(){status=0;
}boolean compareAndSet(int except,int newValue){//cas操作,修改status成功则返回true
}

上面的代码通过自旋和cas来实现一个最简单的锁。

这样实现的锁显然有个致命的缺点:耗费cpu资源。没有竞争到锁的线程会一直占用cpu资源进行cas操作,假如一个线程获得锁后要花费10s处理业务逻辑,那另外一个线程就会白白的花费10s的cpu资源。(假设系统中就只有这两个线程的情况)。

1.1.2 sleep+自旋

你可能从一开始就想到了,当竞争锁失败后,可以将用Thread.sleep将线程休眠,从而不占用cpu资源:


volatile int status=0;void lock(){while(!compareAndSet(0,1)){sleep(10);}//get lock}void unlock(){status=0;
}

上述方式我们可能见的比较多,通常用于实现上层锁。该方式不适合用于操作系统级别的锁,因为作为一个底层锁,其sleep时间很难设置。sleep的时间取决于同步代码块的执行时间,sleep时间如果太短了,会导致线程切换频繁(极端情况和yield方式一样);sleep时间如果设置的过长,会导致线程不能及时获得锁。因此没法设置一个通用的sleep值。就算sleep的值由调用者指定也不能完全解决问题:有的时候调用锁的人也不知道同步块代码会执行多久。

1.1.3 小结

对于锁冲突不严重的情况,用自旋锁会更适合,试想每个线程获得锁后很短的一段时间内就释放锁,竞争锁的线程只要经历几次自旋运算后就能获得锁,那就没必要等待该线程了,因为等待线程意味着需要进入到内核态进行上下文切换,而上下文切换是有成本的并且还不低,如果锁很快就释放了,那上下文切换的开销将超过自旋。

目前操作系统中,一般是用自旋+等待结合的形式实现锁:在进入锁时先自旋一定次数,如果还没获得锁再进行等待。

1.2 futex

1.2.1 什么是Futex

Futex 是Fast Userspace muTexes的缩写,由Hubertus Franke, Matthew Kirkwood, Ingo Molnar and Rusty Russell共同设计完成。几位都是linux领域的专家,其中可能Ingo Molnar大家更熟悉一些,毕竟是O(1)调度器和CFS的实现者。

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。

linux底层用futex实现锁,futex由一个内核层的队列和一个用户空间层的atomic integer构成。当获得锁时,尝试cas更改integer,如果integer原始值是0,则修改成功,该线程获得锁,否则就将当期线程放入到 wait queue中(即操作系统的等待队列)。
上述说法有些抽象,如果你没看明白也没关系。我们先看一下没有futex之前,linux是怎么实现锁的。

1.2.2 futex诞生之前

在futex诞生之前,linux下的同步机制可以归为两类:用户态的同步机制 和内核同步机制。 用户态的同步机制基本上就是利用原子指令实现的自旋锁。关于自旋锁其缺点也说过了,不适用于大的临界区(即锁占用时间比较长的情况)。

内核提供的同步机制,如semaphore等,使用的是上文说的自旋+等待的形式。 它对于大小临界区和都适用。但是因为它是内核层的(释放cpu资源是内核级调用),所以每次lock与unlock都是一次系统调用,即使没有锁冲突,也必须要通过系统调用进入内核之后才能识别。

理想的同步机制应该是没有锁冲突时在用户态利用原子指令就解决问题,而需要挂起等待时再使用内核提供的系统调用进行睡眠与唤醒。换句话说,在用户态的自旋失败时,能不能让进程挂起,由持有锁的线程释放锁时将其唤醒?
如果你没有较深入地考虑过这个问题,很可能想当然的认为类似于这样就行了(伪代码):

void lock(int lockval) {//trylock是用户级的自旋锁while(!trylock(lockval)) {wait();//释放cpu,并将当期线程加入等待队列,是系统调用}
}boolean trylock(int lockval){int i=0; //localval=1代表上锁成功while(!compareAndSet(lockval,0,1)){if(++i>10){return false;}}return true;
}void unlock(int lockval) {compareAndSet(lockval,1,0);notify();
}

上述代码的问题是trylock和wait两个调用之间存在一个窗口:如果一个线程trylock失败,在调用wait时持有锁的线程释放了锁,当前线程还是会调用wait进行等待,但之后就没有人再将该线程唤醒了。

1.2.3 futex诞生之后

	 //uaddr指向一个地址,val代表这个地址期待的值,当*uaddr==val时,才会进行waitint futex_wait(int *uaddr, int val);//唤醒n个在uaddr指向的锁变量上挂起等待的进程int futex_wake(int *uaddr, int n);

futex_wait真正将进程挂起之前会检查addr指向的地址的值是否等于val,如果不相等则会立即返回,由用户态继续trylock。否则将当期线程插入到一个队列中去,并挂起。

futex内部维护了一个队列,在线程挂起前会线程插入到其中,同时对于队列中的每个节点都有一个标识,代表该线程关联锁的uaddr。这样,当用户态调用futex_wake时,只需要遍历这个等待队列,把带有相同uaddr的节点所对应的进程唤醒就行了。

另外,futex是支持多进程的,当使用futex在多进程间进行同步时,需要考虑同一个物理内存地址在不同进程中的虚拟地址是不同的。

2、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 futexFUTEX_WAIT: 原子性的检查uaddr中计数器的值是否为val,如果是则让进程休眠,直到FUTEX_WAKE或者超时(time-out)。也就是把进程挂到uaddr相对应的等待队列上去。FUTEX_WAKE: 最多唤醒val个等待在uaddr上进程。/*

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

4、具体案例分析

4.1 在Bionic中的实现

源码:http://www.androidos.net.cn/android/5.0.1_r1/xref/bionic/libc/bionic/pthread_mutex.cpp

在这里插入图片描述

4.2 C语言实现

futex 的逻辑可以用如下C语言表示

int val = 0;
void lock()
{int cif ((c = cmpxchg(val, 0, 1)) != 0) {if (c != 2)c = xchg(val, 2);while (c != 0) {futex_wait((&val, 2);c = xchg(val, 2);}}
}   void unlock()
{   if (atomic_dec(val) != 1){val = 0;    futex_wake(&val, 1);}
}
/*
val 0: unlock
val 1: lock, no waiters
val 2: lock , one or more waitersLinux提供了很多操作原子变量的API。以arch/arm/include/asm/atomic.h为例。
#define atomic_xchg(v, new) (xchg(&((v)->counter), new))-----------把new赋值给原子变量v,返回原子变量v的旧值。Linux内核中的cmpxchg函数
在Linux内核中,提供了比较并交换的函数cmpxchg,代码在include/asm-i386/cmpxchg.h中,函数的原型是:
cmpxchg(void *ptr, unsigned long old, unsigned long new);
函数完成的功能是:将old和ptr指向的内容比较,如果相等,则将new写入到ptr中,返回old,如果不相等,则返回ptr指向的内容。
*/

5、参考及扩展阅读

:https://github.com/farmerjohngit/myblog/issues/6
:深入解析Android 5.0系统


http://chatgpt.dhexx.cn/article/RFwmquqA.shtml

相关文章

深度讲解futex问答(上)

一、什么是futex&#xff1f; futex是Fast Userspace muTEX的缩写&#xff0c;该机制是由Rusty Russell、Hubertus Franke和Mathew Kirkwood在2.5.7版本的内核中引入&#xff0c;虽然名字中有互斥锁&#xff08;mutex&#xff09;的含义&#xff0c;但实际它是一种用于用户空间…

数组中的字节数

##sizeof查看定义的数组所占用字节数 #include<iostream> int main(){using namespace std;int Inter [10];short sh [10];char ch [10];long lg [10];float fl [10];double dou [10];cout << "int style has " << sizeof Inter << " …

int转byte数组以及相关原理

前言 本文由int转byte数组这样的题目代码引发的思考&#xff0c;其中涉及到多个让我混淆的地方。 直接上代码 public byte[] toBytes(int number){byte[] bytes new byte[4];bytes[3] (byte)number;bytes[2] (byte) ((number >> 8) & 0xFF);bytes[1] (byte) ((…

java的byte数组的不同写法

经常看到java中对byte数组的不同定义&#xff0c;粗略整理的一下&#xff1a; 一个字节&#xff08;byte&#xff09;&#xff1d;8位&#xff08;bit&#xff09;&#xff0c;“byte数组”里面全部是“byte”&#xff0c;即每一个byte都可以用二进制、十六进制、十进制来表示。…

byte数组快速拷贝,byte数组合并,System.arraycopy详解

博客来源&#xff1a; 项目过程中用到byte[]数组相加问题&#xff0c;给出两个byte[] 需要合并成一个byte[]进行计算…那么需求来了……数据量达10W级&#xff0c;怎么合并 调用系统自带方法&#xff08;System.arraycopy&#xff09; 参考程序 org.junit.Test public void f…

程序、进程、线程的区别

程序、进程、线程的区别 进程是程序的实体&#xff0c;而线程又是进程的实体。进程又是线程的容器。 程序、进程、线程三者区别如下: 1.程序&#xff1a;程序并不能单独执行&#xff0c;是静止的&#xff0c;只有将程序加载到内存中&#xff0c;系统为其分配资源后才能够执…

操作系统-进程与线程的区别

操作系统-进程与线程的区别 1.什么是进程 简单的讲&#xff0c;进程是执行的程序&#xff0c;资源分配的最小单位。 进程包括&#xff1a;文本段&#xff08;程序代码&#xff09;、程序计数器的值、CPU寄存器的内容、堆、栈、数据段 进程的状态&#xff1a;新的、就绪、运行…

进程和线程的区别(重点)

来源&#xff1a;http://www.cnblogs.com/lmule/archive/2010/08/18/1802774.html 简而言之,一个程序至少有一个进程,一个进程至少有一个线程. 线程的划分尺度小于进程&#xff0c;使得多线程程序的并发性高。 另外&#xff0c;进程在执行过程中拥有独立的内存单元&#xff0c…

进程与线程的区别和联系

程序并不能单独执行&#xff0c;只有将程序加载到内存中&#xff0c;系统为他分配资源后才能够执行&#xff0c;这种执行的程序称之为进程&#xff0c;也就是说进程是系统进行资源分配和调度的一个独立单位&#xff0c;每个进程都有自己单独的地址空间。所以说程序与进程的区别…

进程和线程的区别和联系

我们都知道计算机的核心是CPU,它承担了所有的计算任务,而操作系统是计算机的管理者,它负责任务的调度,资源的分配和管理,统领整个计算机硬件;应用程序是具有某种功能的程序,程序是运行于操作系统之上的。 进程 进程是一个具有一定独立功能的程序在一个数据集上的一次动…

进程与线程的区别及联系

目录 1. 操作系统功能简介 2. 进程 2.1 认识进程 2.2 进程操作系统中如何管理 2.3 PCB如何描述 2.3.1 pid 2.3.2 内存指针 2.3.3 文件描述符表 2.3.4 进程调度相关属性 3. 内存管理 4. 线程 4.1 认识线程 4.2 进程与线程的关系 4.3 线程安全问题 1.操作系统功能简…

Linux进程与线程的区别

进程与线程的区别&#xff0c;早已经成为了经典问题。自线程概念诞生起&#xff0c;关于这个问题的讨论就没有停止过。无论是初级程序员&#xff0c;还是资深专家&#xff0c;都应该考虑过这个问题&#xff0c;只是层次角度不同罢了。一般程序员而言&#xff0c;搞清楚二者的概…

win10安装时,提示“我们无法创建新的分区,也找不到现有分区”

win10安装时&#xff0c;提示“我们无法创建新的分区&#xff0c;也找不到现有分区”&#xff0c;如图所示&#xff1a; 解决办法&#xff1a; 将win10安装包&#xff08;ios文件&#xff09;解压&#xff0c;将以下文件复制到系统盘&#xff0c;然后重启电脑&#xff0c;自动…

我们无法创建新分区。【错误:0x80042468】

一台服务器6块1.8T SAS 10K&#xff0c;做RAID10. 安装windows server 2012R2系统在分区时报错&#xff0c;总有个分区不能创建成功。&#xff08;正常安装系统后&#xff0c;磁盘管理也有一个磁盘不能创建新的分区&#xff09; 提示“我们无法创建新分区。【错误&#xff1a;0…

重装系统“无法创建新的分区也找不到现有分区”

如题&#xff0c;一开始我使用分区工具对硬盘进行分区后再进入安装系统&#xff0c;一直出现这个问题报错。 (因我没拍照片&#xff0c;从网上找的图片&#xff0c;侵权请联系我) 后来我删除硬盘分区&#xff0c;直接在这里进行分区&#xff0c;就可以装了。如图&#xff0c…

解决无法创建新的分区,也找不到现有的分区。

注意&#xff1a;问题多发生在m2装系统&#xff08;要拔出其他硬盘&#xff09; 原创b站理想与天阳

关于windows安装过程中“我们无法创建新的分区,也找不到现有的分区”问题解决办法

最近在安装电脑系统过程中碰到了这个问题&#xff0c;首先说明下我电脑bios已经设置了uefi引导启动&#xff0c;硬盘分区格式也是GPT格式&#xff0c;还是出现这个问题有点纳闷&#xff0c;后面折腾了好久才找到解决办法&#xff1a; 即在对磁盘进行分区的时候不要创建ESP分区…

系统安装无法创建新的系统分区的解决方法

在安装Windows7时&#xff0c;想必有很多人都安碰到这样的情况吧!在安装界面里选择安装时&#xff0c;却出现“安装程序无法创建新的系统分区&#xff0c;也无法定位现有系统分区” 安装程序无法创建新的系统分区&#xff0c;也无法定位现有分区 网上提供的另外解决方法大全&a…

服务器系统安装提示无法创建新的系统分区,提示无法创建新的分区是怎么回事_安装win10系统无法新建分区的解决办法...

不少朋友在装win10的过程中&#xff0c;可能会遇到“我们无法创建新的分区&#xff0c;也找不到现有的分区”的提示&#xff0c;那么我们应该如何操作来解决此问题呢?下面就给大家讲解安装win10系统无法新建分区的解决办法。 安装win10系统无法新建分区的解决办法&#xff1a;…