LWN:重构futex API!

关注了就能看到更多这么棒的文章哦~

Rethinking the futex API

By Jonathan Corbet&
June 18, 2020
原文来自:https://lwn.net/Articles/823513/
主译:DeepL

Linux的futex()系统调用是一个有点奇怪的功能强大的API,它广泛用于在user space提供底层同步(low-level synchronization)支持,但在GNU C库中却没有对它进行封装。它的实现本来是很简单的,现在已经变得非常复杂了,很少有开发者愿意去深入研究那些代码。不过最近,已经有人开始重构futexes,目前这个工作仅限于一个新的系统调用接口,但计划远不止于此。

内核内要做同步(指互斥保护等)的话有许多种选择,但用户空间的选项一直较少。多年来,唯一真正可用的是System V semaphores,但可以说它们从未受到广泛的欢迎。开发者们一直希望在用户空间中能有一个类似mutex但又不会扼杀性能的选项。

早在2002年,Rusty Russell就提出了一种快速的用户空间mutex机制(fast user-space mutex mechanism),并很快演进为人们现在知道的 "futex"。2003年底发布的2.6.0内核中就出现了这个功能,并马上用在了POSIX线程的并发控制上。最初的实现只有几百行代码。本质来说,futex是一个32位的内存数据,在多个合作的进程之间共享。值为1的话表示futex是可用的,而其他任何值都标志着它是不可用的。希望获取futex的进程会发出一条锁定递减指令(locked decrement instruction),然后验证结果值是否为0;如果是0,则说明获取futex成功,代码可以继续往后执行。释放 futex 则非常简单,只要递增其值即可。

到目前为止,futex的好处是内核完全不参与它们的操作,futex的获取和释放不需要进行系统调用。不过,如果存在对 futex 的争夺,情况就不一样了,这个任务将不得不阻塞住,等待 futex 变得可用。这就是 futex() 系统调用的作用:

int futex(int *uaddr, int futex_op, int val,
          const struct timespec *timeout, /* or: uint32_t val2 */.
          int *uaddr2, int val3)。

最初版本的futex()只实现了两个参数:uaddr(futex的地址)和futexop(+1表示递增futex或者是-1表示递减)。目前最新代码的futexop对应的是FUTEXWAKE (表示futex已被释放,并唤醒等待它的任务),或FUTEXWAIT来阻塞直到futex变得可用

目前futex API中还存在许多其他操作。随着时间的推移,futex接口变得越来越复杂,包括 "robust futex"、adaptive spinning、priority inheritance等等。更多信息请参见本文(https://lwn.net/Articles/360699/ ,有些过时了)的概述,上面链接里的man页面,或者这个底层介绍(https://www.man7.org/linux/man-pages/man7/futex.7.html)。

当前重构futex的工作,主要来源于这几个需求:其一,希望创建一个比futex()更有意义的系统调用接口,futex这个复杂、多路的API,包含有很多不同的参数和特殊情况处理。只要某个系统调用的文档中有这样的条款,大家基本就可以确认这个API的设计不是非常好:

对于那几个blocking操作来说,timeout参数是一个指向timepec结构的指针,指定了操作的超时时间。然而,对于某些操作来说,上面的原型定义并不是这么用的,而是把这个参数中最低位的四个字节存放一个此操作自己定义的一个数值。

过去几年里,当C库的开发人员讨论对外如何暴露futex()时,他们曾提议将其拆分成一组更简单的封装函数。不过这个改动一直没有合入mainline。不过现在,André Almeida提出的futex2()提案做了类似的事情,新增了三个系统调用:

int futex_wait(void *uaddr, unsigned long val, unsigned long flags, ktime_t *timeout);
int futex_wait_time32(void *uaddr, unsigned long val, unsigned long flags,
          old_time32_t *timeout)。
int futex_wake(void *uaddr, unsigned long nr_wake, unsigned long flags)。

这个patch很罕见地提了一个问题:"有没有人开始研究这个接口的实现?". Almeida的这组patch没有增加任何新的功能。事实上,它目前的功能比起现有的futex()API还要弱,缺乏对优先级继承等特性的支持。不过,基本的futex功能是已经实现了的,底层都是调用现有的 futex() 代码。

这组patch的目的显然不是为了展示新功能,而是希望确定下来今后的futex API应该是什么样子的,新功能之后慢慢添加。话说回来,开发者确实也在考虑一些增强功能,并希望后续能落实到位。

其中之一是能够同时等待多个futex,并在其中任何一个变得可用时被唤醒。Gabriel Krisman Bertazi在2019年7月发布了这个功能的patch(针对现有的futex()API实现的),这主要是来自Wine的需求。Wine正在模拟Windows上的一个类似功能。在3月份发布的这组patch帖子所引发的讨论中,Thomas Gleixner轻描淡写地提出,也许是时候设计一个新的futex接口了,在这个接口中可以更容易地添加(和使用)这样的功能。目前的提案就是来自这个建议。

也就是说,新提出的API并没有处理多个futex,但目前这组patch的说明描述中提到了计划新增这个API:

struct futex_wait {
void *uaddr;
unsigned long val;
unsigned long flags;
};

int futex_waitv(struct futex_wait *waiters, unsigned int nr_waiters,
        unsigned long flags, ktime_t *timeout)。

另一个计划实现的特性是能够处理一些常见size的futexes,而不仅仅支持32位。

然后,就是NUMA系统中的性能问题。内核必须维护一个描述当前正在等待的futex的内部数据结构。如果这些结构被保存在错误的NUMA节点上,futex操作可能会触发大量的远程节点cache miss,这将大大降低性能。详情请看本文(https://lwn.net/Articles/685769/ )。Futexes经常被那些都运行在同一个NUMA节点上的线程进程使用,如果内核将其数据结构保存在同一个节点上的话,它们的性能将得到改善。因此,在新的API中计划了一个 "NUMA hint"功能,可以用来提示内核将相关的数据结构保留在一个特定的node上。

虽然关于如何改进内核中的futex功能的思考显然已经进入了一个新的阶段,但用户也不要急着期待它合入。这个子系统的复杂性使得开发者不愿意快速推动改进。他们已经从过去痛苦的经历中得到教训:futex很容易出问题。所以,新API和任何新功能的实现都有可能经历一段长时间的review和修改。"futex "中的 "F "可能代表 "快速",但新的futex功能的实现过程可能不会那么快速。

全文完

LWN文章遵循CC BY-SA 4.0许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注LWN深度文章以及开源社区的各种新近言论~

你可能感兴趣的:(LWN:重构futex API!)