linux进阶40——futex

linux进阶40——futex1.概念Futex是FastUserspacemuTexes的缩写,由HubertusFranke,MatthewKirkwood,IngoMolnarandRustyRussell共同设计完成。Futex按英文翻译过来就是快速用户空间互斥体。其设计思想其实不难理解,在传统的Unix系统中,SystemVIPC(interprocesscommunication),如semaphores,msgqueues,sockets还有文件锁机制(flock())等进程

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺

1. 概念

Futex 是Fast Userspace muTexes的缩写,由Hubertus Franke, Matthew Kirkwood, Ingo Molnar and Rusty Russell共同设计完成。

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。

2. futex系统调用

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

2.1 函数原型

#include <linux/futex.h>
#include <sys/time.h>

int futex(int *uaddr, int op, int val, const struct timespec *timeout,
           int *uaddr2, int val3);

2.2 参数

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

op:存放着操作类型,例如:

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

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

2.3 返回值

RETURN VALUE
       In the event of an error, all operations return -1, and set errno to indicate the error.  The return value on success depends on the operation, as described in the following list:

       FUTEX_WAIT
              Returns 0 if the process was woken by a FUTEX_WAKE call.  See ERRORS for the various possible error returns. (如果进程被 FUTEX_WAKE 调用唤醒,则返回 0,关各种可能的错误返回,请参阅 ERRORS)

       FUTEX_WAKE 
              Returns the number of processes woken up. (返回被唤醒的进程数)

       FUTEX_FD
              Returns the new file descriptor associated with the futex.(返回与 futex 关联的新文件描述符)

       FUTEX_REQUEUE
              Returns the number of processes woken up.(返回被唤醒的进程数)

       FUTEX_CMP_REQUEUE
              Returns the number of processes woken up.(返回被唤醒的进程数)


ERRORS
       EACCES No read access to futex memory.(没有对 futex 内存的读访问)

       EAGAIN FUTEX_CMP_REQUEUE detected that the value pointed to by uaddr is not equal to the expected value val3.  (This probably indicates a race; use the safe FUTEX_WAKE now.) (FUTEX_CMP_REQUEUE 检测到 uaddr 指向的值不等于期望值 val3)

       EFAULT Error retrieving timeout information from user space.(从用户空间检索超时信息时出错)

       EINTR  A FUTEX_WAIT operation was interrupted by a signal (see signal(7)) or a spurious wakeup.(FUTEX_WAIT 操作被信号(请参阅信号(7))或虚假唤醒中断)

       EINVAL Invalid argument.

       ENFILE The system limit on the total number of open files has been reached.(已达到系统对打开文件总数的限制。)

       ENOSYS Invalid operation specified in op.(op中指定的无效操作)

       ETIMEDOUT
              Timeout during the FUTEX_WAIT operation.(FUTEX_WAIT 操作超时)

       EWOULDBLOCK
              op was FUTEX_WAIT and the value pointed to by uaddr was not equal to the expected value val at the time of the call.(op 是 FUTEX_WAIT 并且 uaddr 指向的值不等于调用时的预期值 val。)

2.4 FUTEX_WAIT

这个是futex_op参数上一种选择,即如果uaddr上所指向的futex值和val值相同,则将当前进程/线程会阻塞在uaddr所指向的futex值上,并等待*uaddr的FUTEX_WAKE操作;如果*uaddr != val,那么futex(2)失败并返回伴着EAGAIN错误码。

如果参数timeout非空,那么timeout指向的结构体会指定休眠的时间。(此时间间隔会向上取到系统时间的粒度,并保证不会提前到期)。timeout默认会根据CLOCK_MONOTONIC时钟来计算,但从Linux4.5开始,可以在futex_op上指定FUTEX_CLOCK_REALTIME来选择CLOCK_REALTIME时钟。如果timeout为空,那么调用会无限期阻塞。

2.5 FUTEX_WAKE

FUTEX_WAKE操作可以唤醒最多val个在uaddr指向的futex字上的等待者。最常见的是val为1,表示唤醒一个等待者,或者val为INT_MAX,表示唤醒所有等待者。不保证某个等待者被唤醒。比如说不保证高调度优先级的等待者先于低优先级等待者被唤醒。

3. futex对象

futex对象是一个32位的值,其地址提供给系统调用futex().futex对象在所有平台都是32位。所有futex操作都受这个值(futex)管理。为了在进程间共享一个futex值,futex通常放在共享内存中。这导致在不同的进程中,futex有不一样的虚拟地址的值,不过其指向的物理地址都是一样的。在多线程的程序中,只需将futex值作为全局变量即可被所有线程共享。

4. 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是从用户态开始,由用户态和核心态协调完成的。

5. 进/线程利用futex同步

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

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

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

6. 示例

#define _GNU_SOURCE
#include <linux/futex.h>    //for FUTEX_WAIT FUTEX_WAKE
#include <sys/time.h>       //for struct timespec
#include <sys/syscall.h>    //for SYS_futex
#include <unistd.h>         //for syscall
#include <sys/mman.h>       //for mmap
#include <stdint.h>         //for uint32_t
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>

#define error_handle(msg) do{perror(msg); exit(EXIT_FAILURE);} while(0)

int futex(int* uaddr, int futex_op, int val, const struct timespec *timeout,
            int *uaddr2, int val3) 
{
    return syscall(SYS_futex, uaddr, futex_op, val, timeout, uaddr2, val3);
}

void futex_wait(int *futexp)
{
    while(1) {
        //通过一个循环来避免多个线程占用同一个futex的情况
        if(__sync_bool_compare_and_swap(futexp, 1, 0)){
            break;
        }

        int s = futex(futexp, FUTEX_WAIT, 0, NULL, NULL, 0);
        //判断被唤醒后,再占有futex,此时futexp是否会被futex改为0
        printf("futexp = %d\n", *futexp);
        if(s == -1) {
            error_handle("futex: ");
        } 
    }
}

void futex_post(int* futexp) 
{
    if(__sync_bool_compare_and_swap(futexp, 0, 1)) {
        //这里有一个情况是,将futex值加1后,还没进行下一步唤醒,有新线程过来
        //则新线程会获得futex的所有权
        int s = futex(futexp, FUTEX_WAKE, 1, NULL , NULL, 0);
        if(s == -1) {
            error_handle("futex FUTEX_WAKE:");
        }
    }
}

int main(int argc, char *argv[])
{
    int *futexes = (int *)mmap(NULL, sizeof(uint32_t) * 2, PROT_READ | PROT_WRITE,
            MAP_ANONYMOUS | MAP_SHARED, -1, 0);
    if(futexes == MAP_FAILED) {
        error_handle("mmap:");
    }
    int *futex1 = &futexes[0];
    int *futex2 = &futexes[1];
    *futex1 = 1;    //child 先走
    *futex2 = 0;    //parent 默认阻塞
    int rt = fork();
    if(rt == -1) {
        error_handle("fork");
    }

    if(rt == 0) {   //child
        for(int i = 0; i < 3; ++i) {
            futex_wait(futex1);
            printf("child %d\n", i);
            futex_post(futex2);
        }
    } else {
        for(int i = 0; i < 3; ++i) {
            futex_wait(futex2);
            printf("parent %d\n", i);
            futex_post(futex1);
        }
    }
    return 0;
}

编译运行:

[root@192 futex]# gcc futex.c -o futex
[root@192 futex]# ./futex
child 0
futexp = 1
parent 0
futexp = 1
child 1
futexp = 1
parent 1
futexp = 1
child 2
futexp = 1
parent 2
[root@192 futex]# 

7. 总结

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

参考:

https://blog.csdn.net/jianchaolv/article/details/7544316

https://root1iu.github.io/2019/01/07/%E5%88%9D%E8%AF%86futex/

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/190907.html原文链接:https://javaforall.net

(0)
上一篇 2026年2月11日 下午5:22
下一篇 2026年2月11日 下午6:01


相关推荐

  • HDU4907小技巧

    HDU4907小技巧

    2021年12月2日
    41
  • “undefined reference to“ 问题汇总及解决方法 ——非常非常好的一篇文章

    “undefined reference to“ 问题汇总及解决方法 ——非常非常好的一篇文章转载地址:https://segmentfault.com/a/1190000006049907?utm_source=tuicool&utm_medium=referral在实际编译代码的过程中,我们经常会遇到”undefinedreferenceto”的问题,简单的可以轻易地解决,但有些却隐藏得很深,需要花费大量的时间去排查。工作中遇到了各色各样类似的问题,按照以下几

    2022年5月31日
    101
  • Vue框架快速入门

    Vue框架快速入门Vue是现在最流行的前端框架之一,而且相对于其他两个框架React和Angular来说也更加易学,而且它的作者是国人,中文文档也很完善。当然Vue框架算是比较高级的框架,所以在使用过程中还需要JavaScript、JavaScript2015、WebPack、NodeJS、npm、ESLint、JavaScript单元测试框架等其他知识和框架的使用方法。在学习Vue之前,最好先学习一下这些知识。由

    2022年6月1日
    41
  • portlet编写入门

    portlet编写入门生成下面的两个文件 view jsp 显示欢迎信息 并且提供编辑页面的链接 edit jsp 显示有一个文本框的表单 允许欢迎信息可以被修改 并且包括一个回退到 view jsp 的超级链接 nbsp MVCPortlet 类处理展示我们的 JSPs 因此在本 DEMO 中 我们不需要生成一个单独的 JAVA 类 nbsp 首先我们不希望在同样的页面上出现多个欢迎信息 因此让我们的 elearn007gre

    2026年3月19日
    3
  • 炸裂!手摸手教你如何吃透一个 Java 项目,yyds

    炸裂!手摸手教你如何吃透一个 Java 项目,yyds先说一下大多数新手的情况 就是对着视频敲 Java 项目 其中遇到的 BUG 还能解决 但就是每次敲完一个项目 就感觉很空虚 项目里面的知识点感觉懂了但又好像没懂 应该怎样才能掌握一个项目所用的知识点呢 先分享一位好朋友丁威的经验吧 他是 RocketMQ 技术内幕 一书的作者 他在尝试学习 RocketMQ 之前未曾接触过消息中间件 但硬是通过自己提炼的学习方法 最终成为 RocketMQ 社区的优秀布道师 这让他有了一个非常亮眼的标签 极大提高了职场竞争力 他的总结有以下四点 了解这个项目的使用场

    2026年3月17日
    2
  • windows10产品密钥永久(windows激活密钥)

    使用WindowsKeyViewer一键轻松查看当前计算机的产品密钥。查看当前计算机的产品密钥偶尔需要用到,分享给大家。软件介绍通过使用WindowsKeyViewer,可以轻松确定当前的计算机产品密钥。在某些情况下,例如在重新安装计算机时,了解产品密钥非常重要。通常,可以在Windows安装工具包或计算机的标签上找到产品密钥。该标签经常会损坏并变得难以辨认,并且在使用多台计算机时,安装包…

    2022年4月13日
    63

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号