[Erlang]进程结构和性能分析

来源:互联网 发布:linux mysql 远程访问 编辑:程序博客网 时间:2024/05/17 03:10

本文试图从进程角度解释Erlang之所以高效的原因,大部分资料来源于论文『Characterizing the Scalability of Erlang VM on Many-core Processors』,并且带有自己的理解,不当之处请多包涵。

Erlang作为一门面向并发的语言(Concurrent Oriented Programming, COP),进程扮演着重要的作用,可以说Erlang就是一门面向进程的语言。归根到底,Erlang的核心概念无非就是进程、模式匹配、消息传递三大法宝。

目前主流的Erlang虚拟机是BEAM(Bogdan/Bjrn’s Erlang Abstract Machine),早期的JAM, old BEAM现都已经废弃不用。Erlang虚拟机是运行在操作系统中的一个多线程进程。Linux下,用POSIX线程库(pthread)实现,多线程共享进程(VM)的内存空间。一般来说,Erlang虚拟机会为每个CPU核分配两个线程,一个负责IO,一个作为调度器负责调度Erlang进程。

Erlang进程是虚拟机级别的进程,它非常轻量,初始化时只有2K左右,Erlang官方文档有给出测试初始进程占用内存大小的程序:

123456789101112131415
Eshell V6.1  (abort with ^G)1> Fun = fun() -> receive after infinity -> ok end end.#Fun<erl_eval.20.90072148>2> Pid = spawn(Fun).<0.35.0>3> {_,Bytes} = process_info(Pid, memory).{memory,2680}4> Bytes div erlang:system_info(wordsize).3355> erlang:process_info(Pid). ...... {total_heap_size,233}, {heap_size,233}, {stack_size,9}, ......

可以看到,一个进程包含堆栈在内只需2680B内存,其中堆(含栈)大小为233个字,64位系统下一个字等于8个字节,堆栈占用1864B。实际上,如果只计算PCB,大约只占300B,相比Linux PCB的1K也轻量不小。另外,Joe Armstrong在『Progamming Erlang』中也有过示范,物理内存充足的情况下,spawn一个进程只需花费微秒数量级的时间。因此,Erlang系统中允许同时存在成千上万的进程。

Erlang Process
图1

图1是Erlang进程的内部组成,每个进程都由独立的进程控制块(PCB, process control block)、栈和私有堆三部分组成。PCB包含的信息有进程ID、堆栈起始地址、mailbox、程序寄存器(PC)、参数寄存器等等,完整定义可以参考Erlang运行时(erts)源代码头文件erlang/erts/emulator/beam/erl_process.h中的process结构体,以下是几个主要字段:

12345678910111213141516171819
struct process {    Eterm* htop;    /* Heap top */    Eterm* stop;    /* Stack top */    Eterm* heap;    /* Heap start */    Eterm* hend;    /* Heap end */    Uint heap_sz;   /* Size of heap in words */    Uint min_heap_size; /* Minimum size of heap (in words). */    Eterm* i;       /* Program counter for threaded code. */    Uint32 status;  /* process STATE */    Eterm id;       /* The pid of this process */    Uint reds;      /* No of reductions for this process  */    Process *next;  /* Pointer to next process in run queue */    Process *prev;  /* Pointer to prev process in run queue */    ErlMessageQueue msg;/* Message queue */#ifdef ERTS_SMP    ErlMessageInQueue msg_inq;#endif    ......};


进程堆和栈共同占用一块连续的内存空间,堆空间由低地址向高地址增长,栈空间由高地址向低地址增长,当堆顶和栈顶一样时,可以判定堆栈空间已满,需要通过垃圾回收空间和或者增长空间。在Erlang进程看来,这块堆栈内存是独占的,进程间彼此隔离;在操作系统进程看来,所有Erlang进程的堆栈空间都在自己的堆空间里。Erlang进程里堆空间和栈空间存放数据类型有所区分,前者主要是一些复合数据,比如元组、列表和大数等,后者主要存放一些简单数据类型以及堆中复合数据的引用。

图2是以列表和元组为例展示了Erlang进程中堆栈的内存布局:

Heap Layout
图2

Erlang是动态类型语言,变量类型需要到运行时才能确定,因此,堆栈中每个数据都有一个Type标签表示其类型。元组在堆中是以Array的形式存储,有字段表示元组大小,并且在栈中有一个指针(引用)指向这块堆空间,因为是连续空间,要取出元组中的数据只需O(1)的复杂度计算内存偏移量即可。对于列表来说,列表元素在堆中是以链表形式存在的,由栈中的一个指针指向列表的第一个元素。相邻列表元素在内存中并不连续,也没有字段表示列表大小,因此要获取列表大小只能通过遍历,这个操作是个O(N)的时间复杂度。对于lists:append(ListB, ListA)这个操作,Erlang做的事情是先复制ListB,遍历ListB,找到列表尾部元素,将下一元素指针从NIL改为ListA第一个元素地址。由此可知,要提高性能,最好是将较长的列表追加到较短的列表上,以减少遍历时间。另外,也不要试图在列表尾部追加元素,原因同上,之前的一篇『Erlang列表操作性能分析』对此已做过分析。如果将ListC当做消息内容发送给其他进程,则整个ListC列表都会复制一份,即使往同一个节点发送多次,复制也会进行多次,这往往会导致消息接收进程占用的内存空间比发送进程大,因为对接收进程来说,每次接到的ListC会被当成不同的数据。

从图2还可以看出,ListA和ListC共享了部分数据,也就说,在一个Erlang进程内部,是存在内存共享的情况的。在Erlang里,变量拥有不变性,一次赋值(模式匹配)成功,它就不会再变,因此,ListA和ListC可以永远安全地共享这些数据。当然,Erlang中的内存共享在其他场景下也会出现,在图1中所示的有两块内存共享区域,一块是二进制数据共享区,用于存储大于64Byte的二进制数据;另一块是存储ETS表用的,ETS可以供每个进程访问,相比真正的共享内存有一些不同,它基于消息复制以记录为单位进行存取;相比数据库,它弱了很多,不支持事务机制。

当进程堆栈空间满时,会触发调度器对进程进行GC,如果GC结束堆栈空间仍然不足,则会分配新空间。Erlang的GC是以进程为单位,对某个进程GC不会影响其他进程的执行,虽然对单个进程来说,存在stop the world的现象,但是从全局来看,其他进程不会受影响,这个特性使得Erlang能够应付大规模高并发的业务场景,基于Elrang的业务系统可以达到软实时的级别。另外,一旦进程生命周期结束,GC可以非常方便地直接回收这个进程占用的所有内存。

综上,从进程的角度的来看,使得Erlang高效主要是以下方面:
1. 进程本身轻量。所以线程池的概念在Erlang语言层面根本不存在。
2. 变量不变性避免了很多无谓的数据复制。如List操作,直接通过修改指针实现append操作。
3. 以进程为单位进行GC。

1 0