Erlang的热部署做的很完善,参见Release Handling,这篇文章只关心最基本的模块更新。模块是Erlang程序组织的最基本单元。如下代码就是一个最简单的hello模块(为了说明问题,我们添加了一个init函数):
-module(hello). -export([init/0, hello/1]). init() -> Db = dict:new(), dict:store(name, "jzh", Db). hello(Db) -> Value = dict:fetch(name, Db), io:format("hello: ~p!~n", [Value]).
运行一下:
1> c(hello). {ok,hello} 2> Db = hello:init(). {dict,1,16,16,8,80,48, {[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]} {{[],[],[],[],[], [[name,106,122,104]], [],[],[],[],[],[],[],[],[],[]}}} 3> hello:hello(Db). hello: "jzh"! ok
(c命令的作用是编译和加载指定模块)
hello:hello打印出hello的字样,但是,假如我们现在希望输出hi,而不是hello,要怎么做呢?Java里面一般需要重新编译和运行程序,但是先前的运行状态也会丢失(dict中存储的name),需要重新运行init方法。
我们将hello改成hi,并在同一个控制台直接调用hello:hello(未调用hello:init),看看结果是怎么样的:
4> c(hello). {ok,hello} 5> hello:hello(Db). hi: "jzh"! ok
可以看到,代码更新后打印出hi字样(保存在dict中的name,并没有因为新代码的加载而失效)。
从上面的示例可以看到,Erlang可以在运行时进行代码的替换,而不会影响到运行时状态(数据)。在Java中要完成类似的功能比较难,那Erlang背后实现这种功能的原理是什么呢?原来,在任何时候,一个模块都可以有两个版本加载到运行时系统,一个旧版本,一个当前版本。在加载一个模块时,运行时系统会先检查这个模块是否已经有一份代码存在,如果存在,则把已存在的代码作为旧的代码,新加载的代码作为当前版本。如果有第三份代码加载进系统,则原来的旧版本代码会被清理掉,跟此版本代码相关的所有进程都会被结束;原来的当前代码变为当前的旧版本代码,第三份代码成为当前版本。
函数调用有两种方式:一种完全限定调用(Mod:Func),包括模块内、模块之间这种方式的调用,以及模块之间通过import指令导入其它模块,再直接通过函数名调用;一种直接调用(Func),只存在于模块内的函数调用。完全限定的函数调用总是引用当前版本代码。旧版本代码只可能通过直接调用的方式引用到。
-module(m). -export([loop/0]). loop() -> receive code_switch -> m:loop(); Msg -> ... loop() end.
如上代码,如果有代码更新,并且进程没有收到code_switch消息,那么进程就会通过loop调用一直在引用旧版本代码;而收到code_switch消息后,进程便通过m:loop引用到当前版本代码。
(这一块,《Erlang编程指南》软件升级一章,幕后一节对原理讲的有点复杂)
从虚拟机实现角度来看,这两者的区别在哪里呢?看一下如下代码:
-module(test). -import(hello, [hello/0]). test2() -> ok. test() -> test2(), test:test2(), hello(), hello:hello(), ok.
我们来看看test函数生成的opcode:
i_call_f test:test2/0 i_call_ext_e test:test2/0 i_call_ext_e hello: hello/0 i_call_ext_e hello: hello/0
可以看出,直接调用test2生成的opcode是i_call_f,而其它的全限定调用(包括hello()调用)都生成的是i_call_ext_e指令。这两者有什么区别?i_call_f指令会直接跳到相应函数的入口(进程生成后,地址确定)并执行,而i_call_ext_e会根据模块导出函数列表中的地址跳转(详见[$OTP_SRC/erts/emulator/beam/beam_emu.c --> process_main)。这个地址在模块更新时会更新为当前版本代码导出函数的地址(详见[$OTP_SRC/erts/emulator/beam/beam_bif_load.c --> load_module_2] ,导出函数地址通过[$OTP_SRC/erts/emulator/beam/export.h]中Export结构的address定义)。从这里也可以看出来,除非有进程在引用旧版本的代码,模块更新后,其它进程是无法再通过任何方式引用到旧版本的代码(模块导出函数列表中的地址已更新)。