Erlang热部署 – 模块更新

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生成的opcodei_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定义)。从这里也可以看出来,除非有进程在引用旧版本的代码,模块更新后,其它进程是无法再通过任何方式引用到旧版本的代码(模块导出函数列表中的地址已更新)。

 

你可能感兴趣的:(erlang)