Erlang 运行时系统 —— 进程结构详解

原文地址: Characterizing the Scalability of Erlang VM on Many-core Processors (第三章 第 1 节)

现在的 BEAM (Binary Erlang Abstract Machine)是源自 Turbo Erlang 的 Erlang 标准虚拟机 。它是一种基于寄存器的抽象机。第一次实验性的实现是 1998 年 master degree project 的结果 —— SMP(parallel) VM。从 2006 年起,SMP VM 被囊括进官方发行版中。

SMP Erlang 虚拟机是一个多线程程序。在 Linux 上,它使用 POSIX 线程库。多个线程在一个系统进程中共享内存。Erlang 调度器是一个用于调度和执行 Erlang 进程和端口的线程。因此它既是一个调度器也是一个 worker。进程和端口的调度与执行是相互交错的。每一个调度器有一个存储就绪进程和与其相关端口的运行队列。在多核处理器上,Erlang 虚拟机通常被配置为每核一个调度器或每个硬件线程(如果硬件多线程受支持)一个调度器。

Erlang 运行时提供的许多特性常常和操作系统相关,比如,内存管理、进程调度和网络。在本章其余部分,我们会介绍和分析现在 SMP 虚拟机(之前提到的 R13B)的不同部分,这些和在多核处理器上的可伸缩性息息相关,包括进程结构、消息传递、调度、同步性和内存管理。

每个进程包括一个进程控制块(PCB),一个栈和一个私有堆。一个 PCB 是一个包含了诸如进程 ID、堆和栈的位置、参数寄存器和程序计数器的数据结构。此外,或许在每次垃圾回收之后会有些小的堆碎片被合并进主堆中。堆碎片会在堆中没有足够空闲内存或垃圾回收不能获得更多空闲内存时被使用。比如,当一个进程正在发送消息到另一个进程,如果目标进程没有足够的堆空间容纳这个消息,在 SMP 虚拟机中发送进程不会为目标进程调用垃圾回收(译注:可能是想表达目标进程会用堆碎片存储这个消息)。此外,大于 64 字节的二进制型会被存储在所有进程共享的通用堆中。这和 ETS 表一样。图 3.1 阐明了这些主要的内存区域(还有一些其他内存区域未列出,如原子表)。

OnePiece Sunny

图 3.1 堆结构

如图 3.1,Erlang 进程的栈和堆位于同一块被一起分配和管理的连续内存。从操作系统的进程或线程来看,这块区域在自己的堆中,这意味着 Erlang 进程的堆和栈实际存储在虚拟机的堆中。在这块内存中,堆起于低地址并向高地址增长,栈则与之相反。对堆顶和栈顶的检查可以发现堆溢出的情况。

堆被用于存储如元组、列表或大整数等复合数据结构,栈则被用于存储简单的数据和指向复合数据的引用(或指针)。没有从堆指向栈的指针使垃圾回收变得不那么困难。图 3.2 展示了一个列表和元组是如何存储在栈和堆中的例子(译注:从图 3.2 中可以看出,列表的列表的引用似乎不会出现在栈中)。

OnePiece Sunny

图 3.2 列表和元组的布局

Erlang 是一门动态类型语言。一个变量在运行时与一种类型关联。变量的数据类型不能在编译期确定。数据的内部实现中用多个标签指示类型。在一个字(在 32 位机中长度为 32 位,64 位机中长度为 64 位)中至少有 2 或 6 个有效位用做一个标签。对于一个元组,栈上的值包含一个指向堆上对象的指针,这个对象存储在连续的内存区域。这块内存可以是任意合法 Erlang 类型,甚至是元组或列表,这其中也包括一个用于指示元组长度的头。一个元组是一个数组,它的元素可以被快速定位。

另一方面,列表内部实现为一个链表,且没有用来记录列表长度的头。列表的每个元素依随指向下一个元素的指针,除了最后一个元素是个空指针。两个元素(的列表)可能会在堆中分散为其他数据类型。列表在 Erlang 中用途广泛,因为它们可以用于插入、连接和分裂。图 3.2 展示了将 List A 插入到 List B 中构造而成的 List C 的内存布局。首先,List B 的所有数据被复制,尾指针改为指向 List A 的第一个元素。如果 List B 很长,这个操作会花费很长的时间。因此将短列表插入到长列表会更好。编写高效率的 Erlang 应用,适当的列表操作是必不可少的。从列表的结构同样可以知道,得到列表长度需要遍历所有元素。

List C 的结构表明在进程中某些内存会共享。但这不是发生在进程间的。如果 List C 在一个发送到其他进程的消息中,整个列表会被复制。在接收进程中的这个消息不会有一个指向发送进程中 List A 的指针。此外,如果接着 List A 被发送到接收进程,List A 会再一次复制。这将导致接收者使用的内存多于发送者。(译注:List A 复制两次,List C 复制一次)

一个 Erlang 进程起于低容量栈和堆,这是为了系统中可容纳大量进程。堆和栈的大小可配置且默认为 233 个字。Erlang 进程通常是短暂的且只有小量数据。当堆中没有足够的空间来容纳进程时,垃圾回收执行,如果释放的内存少于需求的内存,每个进程的堆会独立进行垃圾回收。因此当一个调度器为一个进程执行垃圾回收时,其它调度器可以执行其它进程。私有堆用于消息传递的开销很高,因为消息从发送者端经复制发出到接收者。然而因为这个架构,垃圾回收对整个系统的影响较小,原因是每个进程都进行独立的垃圾回收,且当进程退出时,它的内存只需简单回收即可。对于私有堆,Erlang 虚拟机可编译为使用混合架构(hybrid architecture)。在混合模式中,私有数据存储在私有堆中,消息则存储在面向所有进程的通用堆中。在这个模式中,消息不需要复制,通过传递消息指针传递消息的开销为常数时间。混合模式使用时存在着一个问题:如果垃圾回收并非巨细靡遗,对通用消息堆的垃圾回收会延迟所有进程的执行。并且垃圾回收耗时会很长,因为 root set 中存着所有进程的工作数据。它需要一个渐增式垃圾回收机能(incremental garbage collection mechanism)。现在 Erlang 虚拟机的混合模式版本还是试验性的且不随 SMP 相互工作。它同时也缺少编译支持,编译器需要预测哪些变量可能会作为消息发送,并且指派它们到通用堆中。