《超越POSIX:一个时代的终结?》阅读笔记

我越来越觉得,操作系统的设计问题,其实就是怎么让用户程序运行的问题。

国内在2023年出了一个翻译的文章,叫《超越POSIX:一个时代的终结?》。原文叫《Transcending POSIX: The End of an Era?》,2022年发表在usenix.rog网站上。它应该会对设计操作系统有很好的指导作用,所以译文和原文我都读了一遍,以下是阅读笔记。

文件系统及其接口

文件系统抽象主要来自于Multics。Multics的文件系统接口既支持同步I/O,也支持异步I/O。POSIX刚开始只支持同步I/O,后来才支持了异步I/O。

“一切皆文件”的基础是:仅把文件看成一串字节而不对它再进行更多的解释。这样操作系统才能把硬件设备表示为特殊的文件,然后像操作普通文件那样操作I/O设备。

文件系统抽象虽然可以方便地集成I/O设备,但是也可能成为快速I/O设备的瓶颈。

进程

早期Unix就有了进程抽象,它抽象了应用程序的执行。应用程序里包含了程序代码、寄存器的值、打开的文件。当操作系统把应用程序里的执行部分加载进内存,它就成了进程。

进程抽象的技术基础是multi-programming(多程序运行),这个技术出现在20世纪50年代中期。

注:multi-programming的意思是多个程序在一个CPU上运行。国内把它翻译为多道程序或多道程序设计,我认为都不准确。

进程是以处理器为中心的。但是其它计算设备的流行正在挑战这种假设(计算设备如GPU、TPU、用于外置计算的专用加速器等)。

虚拟内存

虚拟内存最早是在Atlas Supervisor操作系统上引入的,随后出现的Multics也支持了虚拟内存,20世纪70年代末虚拟内存被添加到Unix。

虚拟内存抽象解耦了两个相互关联的概念:地址空间,内存空间。地址空间是用于寻址内存的标识符,内存空间是存储数据的物理位置。

当前硬件和应用程序的趋势正在挑战它的核心假设。

进程间通信(IPC)

早期版本的Unix支持的IPC是信号和管道。

由于管道和信号能力受限,BSD添加了网络插座(socket)。但它只是成为了网络平台的IPC机制,在本地IPC上并未广泛应用。

mmap接口也被也被设想为一种IPC机制,但并不流行。POSIX还添加了其它的IPC机制(信号量,专用IPC接口,消息队列),但在很大程度上都被特定生产商的IPC机制所取代了。

线程

线程是最后引入POSIX标准的。

多线程可以充分利用多个计算核心提供的并行能力。你也许会想复刻多个进程来利用并行能力,这么做的缺点是IPC机制使得效率低下。

POSIX线程可以有多种实现方式:1对1, N对1, N对M。要想高性能就得在用户空间处理并行性,但主流POSIX OS却选择的1对1方式。即使是使用了大量线程的应用程序架构(如SEDA),也因为线程开销而效率低下。因此,许多高性能应用使用的是每个核一个线程的模型,它们提供了自己的接口来提升并发性。

POSIX I/O

read/write系统调用:需要从内核的页缓存里复制数据,同步接口是快速I/O的瓶颈,应用程序通过多个线程使用它们才能实现应用级的并发和并行。

mmap接口:比read/write快,因为它避免了系统调用开销,也不用在内核和用户空间复制数据。但错误处理会更复杂。

DIO:使用了read/write系统调用,并且还绕过了页缓存。但需要在用户空间进行缓冲区管理和缓存。

AIO:每个I/O至少需要两次系统调用,使用io_submit系统调用异步提交I/O,使用io_getevents系统调用轮询I/O完成。但每次系统调用都要复制104字节(描述符和完成情况元数据),并且AIO的系统调用在多种情况下会阻塞。

io_uring接口:使用了两个无锁的单生产者单消费者队列来进行内核和用户空间的通信。一个队列用于I/O提交,它被应用程序写被内核读。另一个队列用于I/O完成,它被内核写被应用程序读。io_uring实例可配置为中断驱动、轮询或内核轮询。

绕过POSIX I/O

POSIX I/O模型是在内核处理I/O,然后再把数据传给用户空间。这个模型在高到达率下不好扩展。早期绕过POSIX I/O接口的方法就是BSD包过滤(BPF)。BPF在内核里运行了一个伪机器来进行包过滤,这样就可以进行用户级的包捕获了。eBPF构建在BPF之上,它允许用户程序执行沙箱程序。这个沙箱程序可能运行在内核的虚拟机器上,也可能运行在可以运行此程序的硬件上。这就使得应用程序可以外置I/O活动,比如网络协议处理,比如在用户空间实现文件系统。其它绕过内核的方法还有DPDK和SPDK。

外置计算

过去几十年,CPU是中心,也是主要的计算资源。现在的情况是,把CPU外置计算到专用协处理器和加速器已成为主流。

然而,POSIX没有处理协处理器或加速器的机制。所有非CPU的计算元素都被视为I/O设备。应用程序需要使用集成在内核的用户空间API控制加速器。这些API不得不处理内存及资源管理等事物,因为POSIX不原生支持这类硬件。

比POSIX更高层的抽象

POSIX抽象提供了写出可移植应用程序的抽象方式。但是现代应用程序很少运行在单个机器上。它们越来越多使用远程过程调用(RPC)、HTTP和REST API、分布式键-值存储、数据库,这些都是用高级语言实现的,运行在托管的运行时上。这些托管的运行时和框架所暴露的接口隐藏了其底层的POSIX抽象和接口。因此,从现代系统和服务的角度来看,POSIX抽象层级太低且绑定在单个机器上。

然而,云平台和无服务平台面临的问题是:它们的API是分散的且特定于平台,难以写出可移植的应用程序。而且这些API仍然是以CPU为中心的,因此它们难以有效利用专用加速器和分散的硬件,从而不得不使用定制解决方案。例如,JavaScript应用程序和底层是解耦的。但它的运行时依旧以CPU为中心,这使它难以外置运行在加速器上。确切来说,我们需要一种语言,可以让编译器和语言的运行时有效利用那些过剩的硬件资源,这些硬件资源出现在硬件栈的不同位置。想像一下,如果POSIX不是以CPU为中心的,那些设备的硬件设计会有多么不同。

结束语

作为操作系统的抽象和接口,POSIX已成为标准。它的设计有两个驱动因素:硬件约束和应用场景。现如今,I/O和计算之间的天平正在向I/O倾斜,这就是协处理器和专用加速器逐渐成为主流的部分原因。因此,我们认为POSIX的时代已经结束,我们需要超越POSIX的设计,从更高的层面重新思考抽象和接口。操作系统接口也必须改变,以支持那些更高层级的抽象。