前言, 为了支持热补丁, Erlang有一个称为code server的东东, 这个code server基本上就是一个VM 进程维护着一个ETS 表. code server 可以同时在内存中保存单个模块的两个不同的版本,并且每个版本都允许运行. 当调用c(Module) , l(Module)之后, 新版本就自动加载进去了. r
既然内存中同时存在新旧两个版本, 程序如何区分这两个新旧版本呢, Erlang通过local 和external 调用来区分, 所有的local调用都进入旧版本, external 调用则调用新版本. 这里的external调用是指全名调用, Module:Function(Args)
.
有一点需要注意的是,如果加载了第三个版本,并且进程还运行在第一个版本上,那么该进程就会被VM杀掉, 因为内存中只能保存两个版本,最旧的版本已经不存在了,所以不能有进程继续运行在其中。
#方案一: 消息触发代码替换
#test_hot_swap.erl (新版本打印信息更新为"This is a upgraded version ~n")
-module(test_hot_swap).
-export([loop/0]).
loop() ->
receive
upgrade ->
code:purge(?MODULE), %%清除旧代码,
compile:file(?MODULE), %%重新编译
code:load_file(?MODULE), %%重新加载
?MODULE:loop(); %%必须加上?MODULE才会触发调用新版本,否则仍然调用旧版本, code server维护两个版本,通过这个来区分调用
run ->
io:format("This is a old version ~n"), %%新版本打印信息更新为"This is a upgraded version ~n".
?MODULE:loop();
_ ->
?MODULE:loop()
end.
11> c(test_hot_swap).
{ok,test_hot_swap}12>
12> Pid = spawn(test_hot_swap, loop, []).
<0.84.0>
13> Pid ! run.
This is a old version %%旧版本打印, 代码还没升级
hello
14>
14> Pid ! upgrade. %%发消息触发代码升级
upgrade
15> Pid ! run.
This is a upgraded version %%新版本打印, 代码已经完成升级
run
#方案二: shell命令手工编译, 加载, 进行代码替换
Eshell V8.1 (abort with ^G)
1>
1> c(test_hot_swap).
{ok,test_hot_swap}
3> Pid = spawn(test_hot_swap, loop, []).
<0.66.0>
4> Pid ! run.
This is a old version %%旧版本打印, 代码还没升级
run
5> c(test_hot_swap).
{ok,test_hot_swap}
6> Pid ! run.
This is a old version %%旧版本打印, 新代码还没加载
run
7> l(test_hot_swap).
{module,test_hot_swap}
8> Pid ! run.
This is a upgraded version %%新版本打印, 代码已经完成升级
run
9>
#关于必须使用全名调用才能使用新加载的code, 下面贴出了OTP文档的描述, 具体原因还没见到解释的地方.
"To change from old code to current code, a process must make a fully
qualified function call." [0] That is, a module:function() call, rather
than just a function() call.
Also in that document:
"If a third instance of the module is loaded, the code server removes
(purges) the old code and any processes lingering in it is terminated.
Then the third instance becomes 'current' and the previously current
code becomes 'old'."
So, if you reload a module three times (I'm not sure if you have to
actually change the contents of the .beam file between each load), any
processes running that code that haven't yet made a fully qualified
function call will be killed.
#另外一个例子,拷贝自Stackoverflow
z.erl (old):
-module(z).
-version("0").
-export([start_link/0, boing/0]).
-behavior(gen_server).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], [{debug, [trace]}]).
boing() -> gen_server:call(?MODULE, boom).
init([]) -> {ok, 0}.
handle_call(boom, _From, Num) -> {reply, Num, Num+1};
handle_call(_Call, _From, State) -> {noreply, State}.
handle_cast(_Cast, State) -> {noreply, State}.
handle_info(_Info, State) -> {noreply, State}.
terminate(_Reason, _State) -> ok.
code_change(_OldVsn, State, _Extra) -> {ok, State}.
z.erl (new):
-module(z).
-version("1").
-export([start_link/0, boing/0]).
-behavior(gen_server).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], [{debug, [trace]}]).
boing() -> gen_server:call(?MODULE, boom).
init([]) -> {ok, {tschak, 0}}.
handle_call(boom, _From, {tschak, Num}) -> {reply, Num, {tschak, Num+1}};
handle_call(_Call, _From, State) -> {noreply, State}.
handle_cast(_Cast, State) -> {noreply, State}.
handle_info(_Info, State) -> {noreply, State}.
terminate(_Reason, _State) -> ok.
code_change("0", Num, _Extra) -> {ok, {tschak, Num}}.
Start the shell, and compile the old code. Notice the gen_server is started with debug trace.
1> c(z).
{ok,z}
2> z:start_link().
{ok,<0.38.0>}
3> z:boing().
*DBG* z got call boom from <0.31.0>
*DBG* z sent 0 to <0.31.0>, new state 1
0
4> z:boing().
*DBG* z got call boom from <0.31.0>
*DBG* z sent 1 to <0.31.0>, new state 2
1
Works as expected: returns the Int, and new state is Int+1.
Now replace z.erl with the new one, and execute the following steps.
5> compile:file(z).
{ok,z}
6> sys:suspend(z).
ok
7> code:purge(z).
false
8> code:load_file(z).
{module,z}
9> sys:change_code(z,z,"0",[]).
ok
10> sys:resume(z).
ok
What you just did: 5: compiled the new code. 6: suspended the server. 7: purged older code (just in case). 8: loaded the new code. 9: invoked code change in process 'z' for module 'z' from version "0" with [] passed as "Extra" to code_change. 10: resumed the server.
Now if you run some more tests, you can see, that the server works with the new state format:
11> z:boing().
*DBG* z got call boom from <0.31.0>
*DBG* z sent 2 to <0.31.0>, new state {tschak,3}
2
12> z:boing().
*DBG* z got call boom from <0.31.0>
*DBG* z sent 3 to <0.31.0>, new state {tschak,4}
3
##博客仅作个人记录##