热更原理

文章参考自:

https://blog.csdn.net/mycwq/article/details/13290757
https://blog.csdn.net/mycwq/article/details/41175237
https://blog.csdn.net/mycwq/article/details/43372687

Part 1: 热更api

Erlang有以下几组API提供选择:

第一种热更新方式:
{Module, Binary, Filename} = code:get_object_code(Module),
code:load_binary(Module, Filename, Binary).

第二种热更新方式:
code:purge(Module), code:load_file(Module).

第三种热更新方式:
code:soft_purge(Module) andalso code:load_file(Module).

前面两种从erlang内部实现上来说是一样的,区别是对外接口不同。第三种和前面两种的区别是,如果当前仍有进程占用模块时是否杀掉进程强制更新,第三种则选择不更新。

三种方式实际上都是经历三个过程

方法作用
do_purge清除旧的模块代码
mod_to_bin获取模块二进制数据
try_load_module加载模块

Part 2: 热更新原理

erlang文档有说明:

The code of a module can exist in two variants in a system: current and old. When a module is loaded into the system for the first time, the code becomes ‘current’. If then a new instance of the module is loaded, the code of the previous instance becomes ‘old’ and the new instance becomes ‘current’.

意思是,erlang每个模块都能保存2份代码,当前版本’current’和旧版本’old’,当模块第一次被加载时,代码就是’current’版本。如果有新的代码被加载,'current’版本代码就变成了’old’版本,新的代码就成了’current’版本

这样,就算代码在热更新,有进程在调用这个模块,执行的代码也不会受影响。热更新后,这个进程执行的代码没有改变,只不过代码被标记成’old’版本。而新的进程调用这个模块时,只会访问’current’版本的代码。而’old’版本的代码如果没有进程再访问,就会在下次热更新被系统清除掉。

erlang用两个版本共存的方法来保证任何时候总有一个版本可用,这样,对外服务就不会停止。

Part 3: 例子

热更新的完整api为:
c:c(Mod): 包含 编译,去除旧代码,加载新的二进制代码

c(Mod) ->
	compile:file(Mod), %% 编译erl成beam文件
	code:purge(Mod), %% 清理模块(同时杀掉运行'old'代码的进程,'current'的不受影响)
	code:load_file(Mod). %% 加载beam代码到vm

c:l(Mod): 少了编译过程

l(Mod) ->
	code:purge(Mod),
	code:load_file(Mod).

一个热更问题

-module(t).
-compile(export_all).

start() ->
	Pid = spawn(fun() -> loop() end),
	register(t, Pid).
	
loop() ->
	receive
		Msg ->
			io:format("~p~n", [Msg])
	end,
	loop().

在shell中, 运行结果如下

7> t:start().
true
8> erlang:monitor(process, whereis(t)).  %%进程监控
#Ref<0.0.0.56>
9> whereis(t).
<0.40.0>
10> l(t).                                %%第1次热更
{module,t}
11> whereis(t).
<0.40.0>
12> l(t).                                %%第2次热更
{module,t}
13> whereis(t).
undefined
14> flush().
Shell got {'DOWN',#Ref<0.0.0.56>,process,<0.40.0>,killed}
ok

热更新2次后,进程就被kill掉了.

解决方法1:
http://erlang.org/doc/reference_manual/code_loading.html#id86381

To change from old code to current code, a process must make a
fully qualified function call.
翻译: 要将当前标记为old的代码改变为current, 进程需要进行一次完整的函数调用(MFA).

-module(t).
-compile(export_all).

start() ->
	Pid = spawn(fun() -> loop() end),
	register(t, Pid).
	
loop() ->
	receive
	    code_switch ->
	        t:loop();
		Msg ->
			io:format("~p~n", [Msg])
	end,
	loop().

就是在热更新后, 例如l(t),给这个进程发消息, code_switch ,这样进程会调用 t:loop().

这里,loop()和t:loop()有什么区别呢?

erlang根据模块划分,函数分本地调用和外部调用,其中,本地调用是调用本模块内的函数,函数可以不导出,调用形式为Atom(Args);外部调用就是调用别的模块函数,函数必须导出,调用形式为 Module:Function(Args).
MFA.

在erlang VM中,进程调用模块的过程是先加载这个模块当前版本的代码再执行,如果进程一直都是本地调用,那么所有操作都是在进程当前运行的代码中完成。换句话,这个过程中进程不会去加载新的代码。打破这种局面的就是外部调用,MFA.

解决方法2: 函数调用都改成MFA形式:

-module(t).
-compile(export_all).

start() ->
	Pid = spawn(fun() -> t:loop() end),
	register(t, Pid).
	
loop() ->
	receive
		Msg ->
			io:format("~p~n", [Msg])
	end,
	t:loop().

这样做会有一些性能问题:

1.外部调用的开销比本地调用大一点。外部调用时通过指针找到这个模块函数的导出地址,当模块热更时,就会修改这个指针指向的地址。内部调用是上下文跳转,对比少了一个指针查找,外加原子锁的开销。

2.外部调用的函数代码加载的时间稍微长一点,需要获取外部函数在导出函数表的地址,避免在执行时才去导出函数表查找函数地址造成开销。

Part 4: 实际应用方法.

目前项目中热更的方法是, 通过make:files对源代码进行编译,
然后通过c:l(Mod)对文件热更.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值