Erlang虚拟机源码阅读笔录(一)虚拟机的启动

去年在北京一个互联网公司实习,当时项目组需要使用和erlang相关的东西,然后给我一个任务,和另外两个同事一起阅读erlang虚拟机的源码,然后给老大写一份报告,我主要阅读的是启动,指令,进程创建以及调度这块的代码。阅读笔记是早就写好了,却一直没有同步到博客上,本来自己资质尚浅,笔记里面也有诸多错误;再则这份阅读笔录也不是完全原创,也借鉴了诸多大牛和官网上的东西。到现在突然想将其开放到博客上,也主要是觉得目前erlang虚拟机相关的资料太少,而现在云计算技术的兴起,却使得这门语言备受青睐,所以编程需求量是蛮大的。由于自己资质尚浅,笔录中的诸多错误,也希望大牛们多多指正,在此感谢你们的宝贵意见。


erl实际上是一个shell脚本,如图1.1:



 
 
图1.1
其先设置了erlang虚拟机执行所需的一系列环境变量,然后调用可执行文件erlexec。erlexec的入口点在 otp_src_R15B01/erts/etc/common/erlexec.c 文件中。erlexec的main函数首先分析erl传入的参数和环境变量,获取CPU的相关信息,选择正确版本的beam可执行文件,然后将传入的参数整理好,加入一些默认参数,最后通过系统调用execv运行beam的emulator,如图1.2:



 
 
图1.2
因此,erl和erlexec都是加载器,最终执行的Erlang虚拟机进程是名字为beam系列的进程。
Beam进程的入口函数在otp_src_R15B01/erts/emulator/sys/unix/erl_main.c中,main结构十分简单,如图1.3:



 
 
图1.3
erl_start函数是真正的erlang emulator的入口函数,到现在才真正进入到erlang虚拟机的世界。erl_start函数位于 otp_src_R15B01/erts/emulator/beam/erl_init.c 文件,Erlang虚拟机初始化相关的代码基本上都在这个文件中。这个函数大约600行,但是结构简单,大部分代码都是在处理参数。把erl_start的主干理出来,如图1.4:



 
 
图1.4
early_init()函数进行一些非常底层的初始化工作,一些具体的数据结构初始化在调度一节再详细说明。erl_init()处理一些和Erlang虚拟机本身的初始化操作,例如各种数据结构的初始化,重点的有对process的初始化,schedulers的初始化,以及schedulers和cpu的bind关系。在R14中由init_shared_memory()函数执行的功能在R15版本中统一收并在erl_int()函数中,这部门工作主要是对虚拟机的内存垃圾回收机制的初始化处理,对应的函数是erts_init_gc()。
load_preloaded()函数将需要预加载的Erlang模块加载至虚拟机。需要预加载的模块都在 otp_src_R15B01/erts/preloaded/ebin 目录下。由于在build Erlang/OTP的时候,本地应该还没有Erlang编译器,所以这个目录下提供的都是编译好的.beam文件。这些模块的源码位于otp_src_R15B01/erts/preloaded/src 目录。预加载模块在build的时候由工具程序 make_preload 生成C语言文件硬编码在虚拟机中了。如果想要修改预加载的文件,例如在里面加上 erlang:display() 表达式打印调试信息,可以修改src中的文件,然后通过编译器erlc生成.beam文件保存在 otp_src_R15B01/erts/preloaded/ebin目录下覆盖原来的文件,再build即可。
在预加载的文件夹中可以看到,预加载的有以下模块:
erl_prim_loader:主要加载器,负责所有模块的加载
erlang:对虚拟机提供的一些BIF的接口
init:init进程的代码
otp_ring0:Erlang虚拟机中第一个进程的代码,启动init
prim_file:文件操作接口
prim_inet:网络操作接口
prim_zip:压缩文件操作接口
zlib:zlib库
load_preloaded()函数是如何将预加载模块加载到虚拟机的呢?首先看该函数的定义,如图1.5:



 
 
图1.5
该函数首先调用sys_preloaded()函数,sys_preloaded函数的定义如图1.6:



 
 
图1.6
该函数只是简单的返回了一个全局的指针变量pre_loaded,该变量在 otp_src_R15B01/erts/emulator/sys/unix/sys.c中被申明为extern Preload pre_loaded[];
Preload是一个结构体类型,定义如下:
typedef struct preload {
    char *name; /* Name of module */
    int  size; /* Size of code */
    unsigned char* code; /* Code pointer */
} Preload;
这里面有个unsigned char*类型的指针,指向了要加载的可执行代码的内存起始地址,再回到上面所说的所有的预加载的*.beam字节码模块都会被make_preload脚本程序硬编码成可直接执行的二进制编码并存储在一个C的unsigned char*类型的数组中,数组的命名为preloaded_+文件名,如图1.7所示:



 
 
图1.7
在对erlang虚拟机源码进行make编译的时候,make_preload脚本程序被解释执行,make_preload程序将所有的字节码文件硬编码成C代码并存储在preload.c文件中(我的ubuntu系统下该文件的路径为:${erl_root}/otp_src_R15B02/erts/emulator/x86_64-unknown-linux-gnu/preload.c),在这个文件中定义并用二进制码初始化相应的“preloaded_+beam文件名”命名的数组,紧接着make_preload脚本将在生成的preload.c文件中定义一个Preload类型的变量pre_load[],并将其成员变量code指向前面定义的preloaded_+beam文件名数组首地址,如图1.8所示:



 
 
图1.8
     初始化完成的pre_load[]数组如图1.9所示:



 
 
图1.9
     在sys.c文件中通过extern申明,将pre_loaded[]变量引入到erlang虚拟机中,至此,预加载过程结束。
把这些必要模块都加载至虚拟机之后,通过erl_first_process_otp()函数创建了Erlang虚拟机上的第一个进程,调用 otp_ring0 模块中的start/2函数。start/2 函数运行init模块的 boot/1 函数,之后开始Erlang/OTP系统的引导过程。这里先把虚拟机的启动过程分析完再讲述Erlang/OTP的引导过程。
创建了第一个进程之后,进程还不能运行,因为还没有创建调度器。erts_start_schedulers()根据CPU的核心数和用户通过参数设置的数值启动某个数目的调度器线程。每一个调度器都在一个线程中运行。调度器挑选要执行的进程,然后执行进程,当进程的reds用完或进程等待IO挂起的时候再挑选另一个进程执行。后面会详细分析Erlang调度器的工作原理。运行了erts_start_schedulers()函数之后Erlang虚拟机才真正运转起来。
启动调度器之后,调用erts_sys_main_thread()函数,也就是说beam进程的主线程进入了erts_sys_main_thread()函数。下面简单分析一下erts_sys_main_thread()函数。如图1.9:



 
 
图1.9
这个函数很简单,屏蔽浮点数异常、通知信号处理线程已经完成了初始化,然后进入一个死循环等待信号。这个select调用表示永远等待文件IO操作,但是什么文件也不等,只是把线程挂起。但是这个函数在收到信号的时候会返回。这里顺便提一下Erlang虚拟机中的信号处理。在之前初始化的时候,设置了信号处理函数,也就是通过函数 init_break_handler() 设置了一些信号的处理函数。这些信号处理函数收到了信号之后实际上将信号通过管道转发给了一个专门处理信号的线程,之前在调用 early_init() 的时候创建了这个线程,这个信号处理线程运行的函数是 signal_dispatcher_thread_func(),这个函数是一个死循环,等待从管道中读取值。虚拟机的主线程通过 smp_sig_notify() 函数将通知消息写入管道发给信号处理线程。
从Erlang虚拟机处理信号的方式可以看出,这种处理方式也是Erlang提倡的进程间通信方式。
下面分析otp_ring0的start/2调用init的boot/1引导Erlang/OTP系统的过程。
init进程的引导过程
init:boot/1的代码如图1.10:



 
 
图1.10
     第2行将当前进程注册为init,于是我们就有了init进程。第4行启动了一个新的进程ON_LOAD_HANDLER,这个进程处理一些和加载相关的事件。然后对传入的参数做一些处理,Start是erl -s参数传入的要运行的MFA列表,Flags0是调用erl传入的一些标志,Args是erl -extra 传入的一些额外参数。接下来这些参数传入boot/3。下面(图1.11)是boot/3的代码:



 
 
图1.11
boot/3调用do_boot/2,设置State,然后就进入boot_loop/2循环。下面是do_boot/2的代码(图1.12):



 
 
图1.12
do_boot/2创建了一个负责引导过程的进程(do_boot/3,没有register,让我们称为do_boot),boot/2最后进入了一个boot_loop循环,接受来自do_boot进程的消息。现在系统上有3个进程,如图1.13所示:



 
 
图1.13
此时的<0.0.0>在boot_loop循环等待接受<0.2.0>发出的和boot相关的消息,<0.1.0>在等待接收和加载相关的消息。下面看do_boot/3的引导过程。第7-10行从传入的参数中获得加载器相关的参数,然后在第11行调用函数start_prim_loader通过erl_prim_loader模块的start/3函数创建了加载器进程。第13-14行从启动脚本中获得启动指令列表。有关启动脚本的格式参见文档 erl -man script ,启动脚本描述了Erlang运行时系统启动的过程,包含了启动过程要执行的一系列指令。如果启动erl的时候没有带-boot Name参数,那么默认使用start.boot启动脚本。start.boot是由start.script生成的。start.script内容摘要如图1.14所示:



 
 
图1.14
do_boot/3中的BootFile就是这个文件,BootList就是从第3行开始的这个列表。列表中的每一项表示一个动作,这些动作包括preLoaded、progress、path、primLoad、kernelProcess和apply,这些动作在erl -man script文档中有详细的解释。do_boot/3的第23行调用eval_script/8函数负责执行这个列表中的每一个动作。图1.15是eval_script/8的代码节选:



 
 
图1.15
eval_script/8对BootList中的每一个动作进行处理。有一些动作要给init进程发送消息,init进程的boot_loop/2循环接收这些消息。boot_loop/2接收的消息中有两个,如图1.16所示:



 
 
图1.16
BootList最后一条指令是{progress,started},对应了boot_loop/2第3行的消息,在执行完这一条指令之后,eval_script/8结束了执行,因此do_boot/3在结束eval_script/8之后调用start_em/1之后就正常退出了,进程<0.2.0>正常退出,boot_loop/2收到'EXIT'消息,init进程进入loop/1循环。此时,init作为初始化的任务已经完成。这一个默认的启动脚本启动了两个应用程序,kernel和STDLIB,前者是一个普通应用程序,后者只是一个库应用程序。如果erl没有传入-noshell参数,kernel还会启动shell和用户交互。这两个应用程序是Erlang最简系统的基础,前者提供了必要的系统服务,例如文件服务、网络服务和错误日志记录服务等,后者提供了编写程序需要使用的各种工具、数据结构以及OTP相关的重要模块。

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值