Joe Armstrong在描述Erlang的设计要求时,就提到了软件维护应该能在不停止系统的情况下进行。在实践中,我们也因为这种不停止服务的热更新获益良多。那么Erlang是如何做到热更新的呢?这就是本文要讨论的问题。
在前面的文章也说到了。erlang VM为每个模块最多保存2份代码,当前版本'current'和旧版本'old',当模块第一次被加载时,代码就是'current'版本。如果有新的代码被加载,'current'版本代码就变成了'old'版本,新的代码就成了'current'版本。erlang用两个版本共存的方法来保证任何时候总有一个版本可用,对外服务就不会停止。
前言
为什么代码热更新时不影响进程运行?
为什么进程要使用外部调用(M:F/A)才能切换到新代码?
为什么可以同时使用2个版本的代码?
为什么只能一个模块一个模块热更?
....
我们总会有很多疑问,但一切的答案都在源码上。现在深入剖析下erlang热更新实现机制,相信你的疑惑可以找到答案。
源码剖析
以下是erlang热更新的三个过程:
c(Mod) ->
compile:file(Mod), %% 编译erl成beam文件
code:purge(Mod), %% 清理模块(同时杀掉运行'old'代码的进程,'current'的不受影响)
code:load_file(Mod). %% 加载beam代码到vm
热更新加载beam代码到vm,这一步是调用了 erlang:load_module() 实现,文章重点说下这个函数。(以R16B02作说明)
%% erlang:load_module/2
load_module(Mod, Code) ->
case erlang:prepare_loading(Mod, Code) of
{error,_}=Error ->
Error;
Bin when erlang:is_binary(Bin) ->
case erlang:finish_loading([Bin]) of
ok ->
{module,Mod};
{Error,[Mod]} ->
{error,Error}
end
end.
以上主要是2个过程:
1、 erlang:prepare_loading() 预加载beam的操作,是一个解析beam的过程
2、erlang:finish_loading() 实现代码加载到vm的过程
预加载beam
现在看下erlang:prepare_loading() ,这是个bif函数,实现预加载beam:
/*
* beam_bif_load.c prepare_loading_2函数,实现 erlang:prepare_loading()
*/
BIF_RETTYPE prepare_loading_2(BIF_ALIST_2)
{
byte* temp_alloc = NULL;
byte* code;
Uint sz;
Binary* magic;
Eterm reason;
Eterm* hp;
Eterm res;
if (is_not_atom(BIF_ARG_1)) {
error:
erts_free_aligned_binary_bytes(temp_alloc);
BIF_ERROR(BIF_P, BADARG);
}
// 复制原始的beam文件数据
if ((code = erts_get_aligned_binary_bytes(BIF_ARG_2, &temp_alloc)) == NULL) {
goto error;
}
magic = erts_alloc_loader_state();
sz = binary_size(BIF_ARG_2);
// 预加载beam(解析beam,加载数据,生成导出函数)
reason = erts_prepare_loading(magic, BIF_P, BIF_P->group_leader,
&BIF_ARG_1, code, sz);
// 释放beam数据空间
erts_free_aligned_binary_bytes(temp_alloc);
if (reason != NIL) {
hp = HAlloc(BIF_P, 3);
res = TUPLE2(hp, am_error, reason);
BIF_RET(res);
}
hp = HAlloc(BIF_P, PROC_BIN_SIZE);
res = erts_mk_magic_binary_term(&hp, &MSO(BIF_P), magic);
erts_refc_dec(&magic->refc, 1);
BIF_RET(res);
}
下面是解析beam的过程:
/*
* beam_load.c erts_prepare_loading函数,实现beam解析,加载数据,生成导出函数
*/
Eterm erts_prepare_loading(Binary* magic, Process *c_p, Eterm group_leader,
Eterm* modp, byte* code, Uint unloaded_size)
{
Eterm retval = am_badfile;
LoaderState* stp;
stp = ERTS_MAGIC_BIN_DATA(magic);
stp->module = *modp;
stp->group_leader = group_leader;
#if defined(LOAD_MEMORY_HARD_DEBUG) && defined(DEBUG)
erts_fprintf(stderr,"Loading a module\n");
#endif
/*
* Scan the IFF file.
*/
CHKALLOC();
CHKBLK(ERTS_ALC_T_CODE,stp->code);
// 检查beam文件格式,生成模块相关信息
if (!init_iff_file(stp, code, unloaded_size) ||
!scan_iff_file(stp, chunk_types, NUM_CHUNK_TYPES, NUM_MANDATORY) ||
!verify_chunks(stp)) {
goto load_error;
}
/*
* 读取代码块头部信息,检查版本支持,获取label和函数个数
*/
CHKBLK(ERTS_ALC_T_CODE,stp->code);
define_file(stp, "code chunk header", CODE_CHUNK);
if (!read_code_header(stp)) {
goto load_error;
}
/*
* 初始化代码信息
*/
stp->code_buffer_size = 2048 + stp->num_functions;
stp->code = (BeamInstr *) erts_alloc(ERTS_ALC_T_CODE,
sizeof(BeamInstr) * stp->code_buffer_size);
stp->code[MI_NUM_FUNCTIONS] = stp->num_functions;
stp->ci = MI_FUNCTIONS + stp->num_functions + 1;
stp->code[MI_ATTR_PTR] = 0;
stp->code[MI_ATTR_SIZE] = 0;
stp->code[MI_ATTR_SIZE_ON_HEAP] = 0;
stp->code[MI_COMPILE_PTR] = 0;
stp->code[MI_COMPILE_SIZE] = 0;
stp->code[MI_COMPILE_SIZE_ON_HEAP] = 0;
/*
* 读取原子表
*/
CHKBLK(ERTS_ALC_T_CODE,stp->code);
define_file(stp, "atom table", ATOM_CHUNK);
if (!load_atom_table(stp)) {
goto load_error;
}
/*
* 读取导入函数表
*/
CHKBLK(ERTS_ALC_T_CODE,stp->code);
define_file(stp, "import table", IMP_CHUNK);
if (!load_import_table(stp)) {
goto load_error;
}
/*
* 读取匿名函数
*/
CHKBLK(ERTS_ALC_T_CODE,stp->code);
if (stp->chunks[LAMBDA_CHUNK].size > 0) {
define_file(stp, "lambda (fun) table", LAMBDA_CHUNK);
if (!read_lambda_table(stp)) {
goto load_error;
}
}
/*
* 读取数据表
*/
CHKBLK(ERTS_ALC_T_CODE,stp->code);
if (stp->chunks[LITERAL_CHUNK].size > 0) {
define_file(stp, "literals table (constant pool)", LITERAL_CHUNK);
if (!read_literal_table(stp)) {
goto load_error;
}
}
/*
* 读取line信息
*/
CHKBLK(ERTS_ALC_T_CODE,stp->code);
if (stp->chunks[LINE_CHUNK].size > 0) {
define_file(stp, "line table", LINE_CHUNK);
if (!read_line_table(stp)) {
goto load_error;
}
}
/*
* 加载代码块,生成label
*/
CHKBLK(ERTS_ALC_T_CODE,stp->code);
stp->file_name = "code chunk";
stp->file_p = stp->code_start;
stp->file_left = stp->code_size;
if (!load_code(stp)) {// 加载代码
goto load_error;
}
CHKBLK(ERTS_ALC_T_CODE,stp->code);
if (!freeze_code(stp)) {
goto load_error;
}
/*
* 读取和确认导出函数
*/
CHKBLK(ERTS_ALC_T_CODE,stp->code);
define_file(stp, "export table", EXP_CHUNK);
if (!read_export_table(stp)) {
goto load_error;
}
/*
* Good so far.
*/
retval = NIL;
load_error:
if (retval != NIL) {
free_loader_state(magic);
}
return retval;
}
然后生成导出函数:
/*
* beam_load.c read_export_table函数,生成导出函数
*/
static int read_export_table(LoaderState* stp)
{
int i;
BeamInstr* address;
GetInt(stp, 4, stp->num_exps);
if (stp->num_exps > stp->num_functions) {
LoadError2(stp, "%d functions exported; only %d functions defined",
stp->num_exps, stp->num_functions);
}
stp->export
= (ExportEntry *) erts_alloc(ERTS_ALC_T_PREPARED_CODE,
(stp->num_exps * sizeof(ExportEntry)));
/* beam文件导出函数表的格式
& 4 bytes 'ExpT' chunk ID
* 4 bytes size total chunk length
* 4 bytes n number of entries
* xx bytes ... Function entries (each 3 * 4 bytes): Function, Arity, Label
*/
for (i = 0; i < stp->num_exps; i++) {
Uint n;
Uint value;
Eterm func;
Uint arity;
GetInt(stp, 4, n);
GetAtom(stp, n, func);
stp->export[i].function = func;
GetInt(stp, 4, arity);
if (arity > MAX_REG) {
LoadError2(stp, "export table entry %d: absurdly high arity %d", i, arity);
}
stp->export[i].arity = arity;
GetInt(stp, 4, n);
if (n >= stp->num_labels) {
LoadError3(stp, "export table entry %d: invalid label %d (highest defined label is %d)", i, n, stp->num_labels);
}
value = stp->labels[n].value;
if (value == 0) {
LoadError2(stp, "export table entry %d: label %d not resolved", i, n);
}
stp->export[i].address = address = stp->code + value;
/*
* Find out if there is a BIF with the same name.
*/
if (!is_bif(stp->module, func, arity)) {
continue;
}
/*
* This is a stub for a BIF.
*
* It should not be exported, and the information in its
* func_info instruction should be invalidated so that it
* can be filtered out by module_info(functions) and by
* any other functions that walk through all local functions.
*/
if (stp->labels[n].patches) {
LoadError3(stp, "there are local calls to the stub for "
"the BIF %T:%T/%d",
stp->module, func, arity);
}
stp->export[i].address = NULL;
address[-1] = 0;
address[-2] = NIL;
address[-3] = NIL;
}
return 1;
load_error:
return 0;
}
热更代码
紧接着看下erlang:finish_loading(),是个bif函数,实现代码更新
/*
* beam_bif_load.c finish_loading_1()函数,实现更新代码到VM
*/
BIF_RETTYPE finish_loading_1(BIF_ALIST_1)
{
int i;
int n;
struct m* p = NULL;
Uint exceptions;
Eterm res;
int is_blocking = 0;
int do_commit = 0;
/*
* 获取代码修改权限,失败等下次调度再执行(保证同时只有一个进程能修改代码)
*/
if (!erts_try_seize_code_write_permission(BIF_P)) {
ERTS_BIF_YIELD1(bif_export[BIF_finish_loading_1], BIF_P, BIF_ARG_1);
}
/*
* 在加载代码前检验要加载的代码
*/
n = list_length(BIF_ARG_1);
if (n == -1) {
ERTS_BIF_PREP_ERROR(res, BIF_P, BADARG);
goto done;
}
p = erts_alloc(ERTS_ALC_T_LOADER_TMP, n*sizeof(struct m));
for (i = 0; i < n; i++) {
Eterm* cons = list_val(BIF_ARG_1);
Eterm term = CAR(cons);
ProcBin* pb;
if (!ERTS_TERM_IS_MAGIC_BINARY(term)) {
ERTS_BIF_PREP_ERROR(res, BIF_P, BADARG);
goto done;
}
pb = (ProcBin*) binary_val(term);
p[i].code = pb->val;
p[i].module = erts_module_for_prepared_code(p[i].code);
if (p[i].module == NIL) {
ERTS_BIF_PREP_ERROR(res, BIF_P, BADARG);
goto done;
}
BIF_ARG_1 = CDR(cons);
}
/*
* 目前只支持单个模块热更,以后可能会支持多个模块
*/
if (n > 1) {
ERTS_BIF_PREP_ERROR(res, BIF_P, SYSTEM_LIMIT);
goto done;
}
/*
* 到这里,代码检查已经完成,现在准备加载代码
* 要检查模块是否有旧代码,同时阻塞其他线程
*/
res = am_ok;
erts_start_staging_code_ix();// 使用下一个 code_index 的准备操作(后面讲解)
for (i = 0; i < n; i++) {
p[i].modp = erts_put_module(p[i].module);
}
for (i = 0; i < n; i++) {
if (p[i].modp->curr.num_breakpoints > 0 ||
p[i].modp->curr.num_traced_exports > 0 ||
erts_is_default_trace_enabled()) {
/* tracing involved, fallback with thread blocking */
erts_smp_proc_unlock(BIF_P, ERTS_PROC_LOCK_MAIN);
erts_smp_thr_progress_block();
is_blocking = 1;
break;
}
}
if (is_blocking) {
for (i = 0; i < n; i++) {
if (p[i].modp->curr.num_breakpoints) {
erts_clear_module_break(p[i].modp);
ASSERT(p[i].modp->curr.num_breakpoints == 0);
}
}
}
// 检查旧代码是否还在使用(状态 not_purged)
exceptions = 0;
for (i = 0; i < n; i++) {
p[i].exception = 0;
if (p[i].modp->curr.code && p[i].modp->old.code) {
p[i].exception = 1;
exceptions++;
}
}
if (exceptions) {
res = exception_list(BIF_P, am_not_purged, p, exceptions);
} else {
/*
* 现在开始加载代码(到这里就不会失败了)
*/
exceptions = 0;
for (i = 0; i < n; i++) {
Eterm mod;
Eterm retval;
erts_refc_inc(&p[i].code->refc, 1);
retval = erts_finish_loading(p[i].code, BIF_P, 0, &mod); // 加载代码到VM
ASSERT(retval == NIL || retval == am_on_load);
if (retval == am_on_load) {
p[i].exception = 1;
exceptions++;
}
}
if (exceptions) {
res = exception_list(BIF_P, am_on_load, p, exceptions);
}
do_commit = 1;
}
done:
// 加载代码完成,切换code index,恢复进程状态(前面阻塞了其他线程)
return staging_epilogue(BIF_P, do_commit, res, is_blocking, p, n);
}
再看erts_finish_loading,实现加载代码到VM
/*
* beam_load.c erts_finish_loading函数
*/
Eterm erts_finish_loading(Binary* magic, Process* c_p,
ErtsProcLocks c_p_locks, Eterm* modp)
{
Eterm retval;
LoaderState* stp = ERTS_MAGIC_BIN_DATA(magic);
/*
* 准备更新导出函数表(没有加锁保护,确保SMP下其他线程已经被阻塞)
*/
ERTS_SMP_LC_ASSERT(erts_initialized == 0 || erts_has_code_write_permission() ||
erts_smp_thr_progress_is_blocking());
/*
* 下面这一步,'current'版本代码将变成了'old'版本,新的代码就成了'current'版本
* 如果存在'old'版本代码,操作将失败
*/
CHKBLK(ERTS_ALC_T_CODE,stp->code);
retval = insert_new_code(c_p, c_p_locks, stp->group_leader, stp->module,
stp->code, stp->loaded_size);
if (retval != NIL) {
goto load_error;
}
/*
* 修正导出函数表入口
*/
CHKBLK(ERTS_ALC_T_CODE,stp->code);
final_touch(stp);
/*
* 加载完成(顺道打印调试信息)
*/
CHKBLK(ERTS_ALC_T_CODE,stp->code);
#if defined(LOAD_MEMORY_HARD_DEBUG) && defined(DEBUG)
erts_fprintf(stderr,"Loaded %T\n",*modp);
#if 0
debug_dump_code(stp->code,stp->ci);
#endif
#endif
stp->code = NULL; /* Prevent code from being freed. */
*modp = stp->module;
/*
* 如果存在 on_load 函数,抛出 on_load 必须运行的信号
*/
if (stp->on_load) {
retval = am_on_load;
}
load_error:
// 释放代码数据
free_loader_state(magic);
return retval;
}
下面看下insert_new_code函数,应该是热更新最核心的函数了。
/*
* beam_load.c insert_new_code函数,实现加载代码到VM
*/
static Eterm insert_new_code(Process *c_p, ErtsProcLocks c_p_locks,
Eterm group_leader, Eterm module, BeamInstr* code,
Uint size)
{
Module* modp;
Eterm retval;
// 使'current'版本代码将变成了'old'版本
if ((retval = beam_make_current_old(c_p, c_p_locks, module)) != NIL) {
erts_dsprintf_buf_t *dsbufp = erts_create_logger_dsbuf();
erts_dsprintf(dsbufp,
"Module %T must be purged before loading\n",
module);
erts_send_error_to_logger(group_leader, dsbufp);
return retval;
}
/*
* 更新模块表,同时使新的代码就成为'current'版本
*/
erts_total_code_size += size;
modp = erts_put_module(module);
modp->curr.code = code;
modp->curr.code_length = size;
modp->curr.catches = BEAM_CATCHES_NIL; /* Will be filled in later. */
/*
* 更新模块代码地址范围(为了实现通过指令指针快速查找到函数)
*/
erts_update_ranges(code, size);
return NIL;
}
到这里,erlang的热更新实现代码基本讲完了,再来回顾下erlang代码热更新的过程。
1.编译erl成beam文件
2.清理模块(同时杀掉运行'old'代码的进程,'current'的不受影响)
3.加载beam代码到vm
而这里讨论了erlang代码更新的过程:
1、beam解析,生成代码,和导出函数。
2、加载代码,更新导出函数表,模块版本交替,切换主版本
探讨erlang热更新机制
首先要明白一个概念,erlang VM的实现基于寄存器,有400多条指令,这些指令包括了算术运算、比较和逻辑运算,操作字符串、元组和列表,堆栈的分配和释放,类型判断(数字、列表、元组等),跳转,异常处理,调用和返回,进程消息发送和接收,等待和超时等等。
erlang会将所有代码生成为基本指令操作,就是说执行一个函数,其实就是调转到某个地址后执行若干条指令。也就是基于指令地址寻址,然后执行一系列的指令,所以,只要把函数入口地址指向另外一个函数,就可以实现代码切换,形象点就是调用一个同名不同地址的函数。(这个利用c函数指针实现的,而指令跳转利用goto或者switch-case实现)
那么,修改代码不影响其他进程执行?为何进程还能访问旧代码?
VM利用 code index为代码保存了多个副本,进程当前执行的指令上下文都不会改变,使得进程执行不会受代码更新的影响。其中,只是函数入口地址变了,执行本地调用就不改变code index,执行原来函数的指令集合,外部调用就会获取最新的code index,执行到新的指令集合。关于code index 我也准备了满满的内容和大家分享。
延伸阅读
code index(代码索引)
VM为每份代码都保存了“多个副本”,然后通过一个全局的 code index 确认当前使用的是哪个版本。code index 作用是当 beam 代码正在修改时(如加载,更新,或删除),允许 erlang 进程同时访问执行代码而不用加锁。code index 同时作用于 export / module / beam_catches / beam_ranges 这几个模块的结构数据。
code index 有3个状态: active 、staging,和另外一个未明确使用的状态(可以理解成“上一个的active”,或者是“下一个staging”,可能以后会用到)。其中,active 表示当前使用的版本;staging表示下一个版本,仅在更新 beam 代码时使用到。当代码更新完成后 staging 将切换成 active,那active 就变成了“上一个active状态”。代码改变时就一直重复这个过程。
这里要明确一点,code index跟模块的 'current' 和'old' 版本不是一个概念,实际上是不相干的两个东西。
如何理解code index 的用途?
这个要从函数的调用过程说起,下面简单写个例子,保存为test.erl
-module(test).
-compile(export_all).
t() ->
t2().
t1() ->
?MODULE:t2().
t2() ->
erlang:memory().
编译,生成opcode
1> c(test).
{ok,test}
2> erts_debug:df(test).
ok
打开生成的 test.dis
04C84308: i_func_info_IaaI 0 test t 0
04C8431C: i_call_only_f test:t2/0
04C84324: i_func_info_IaaI 0 test t1 0
04C84338: i_call_ext_only_e test:t2/0
04C84340: i_func_info_IaaI 0 test t2 0
04C84354: i_call_ext_only_e erlang:memory/0
04C8435C: i_func_info_IaaI 0 test module_info 0
04C84370: move_cr test x(0)
04C84378: allocate_tt 0 1
04C84380: call_bif_e erlang:get_module_info/1
04C84388: deallocate_return_Q 0
04C84390: i_func_info_IaaI 0 test module_info 1
04C843A4: move_rx x(0) x(1)
04C843AC: move_cr test x(0)
04C843B4: allocate_tt 0 2
04C843BC: call_bif_e erlang:get_module_info/2
04C843C4: deallocate_return_Q 0
可以看出,如果是本地函数调用,opcode是i_call_only_f ;如果是外部调用,opcode则是i_call_ext_only_e
下面从源码解释这两种调用的区别:
/*
* beam_emu.c process_main() 线程入口函数,实现VM调度
* 以下截取 函数调用 处理过程 (已删除调试代码)
*/
OpCase(i_call_only_f): {
SET_I((BeamInstr *) Arg(0));
Dispatch();
}
OpCase(i_call_ext_only_e):
Dispatchx();
Dispatch() 和Dispatchx() 都是宏,再看下这两个的代码:
# define Dispatch() DispatchMacro()
# define Dispatchx() DispatchMacrox()
也就是下面2个宏:
/*
* 检查是否有调度机会,有的话通过 I寄存器 跳转到指向的执行指令地址,没有的话切换上下文
*/
#define DispatchMacro() \
do { \
BeamInstr* dis_next; \
dis_next = (BeamInstr *) *I; \
CHECK_ARGS(I); \
if (FCALLS > 0 || FCALLS > neg_o_reds) { \
FCALLS--; \
Goto(dis_next); \
} else { \
goto context_switch; \
} \
} while (0)
#define DispatchMacrox() \
do { \
if (FCALLS > 0) { \
Eterm* dis_next; \
SET_I(((Export *) Arg(0))->addressv[erts_active_code_ix()]); \
dis_next = (Eterm *) *I; \
FCALLS--; \
CHECK_ARGS(I); \
Goto(dis_next); \
} else if (ERTS_PROC_GET_SAVED_CALLS_BUF(c_p) \
&& FCALLS > neg_o_reds) { \
goto save_calls1; \
} else { \
SET_I(((Export *) Arg(0))->addressv[erts_active_code_ix()]); \
CHECK_ARGS(I); \
goto context_switch; \
} \
} while (0)
前面也介绍了code index,可以知道,i_call_only_f 和i_call_ext_only_e 的主要区别是后者会重新获取最新的代码。也就是说,外部调用会执行到最新的代码。
这里需要说明几个关键信息,否能很难理解代码:
/*
* I寄存器:指向下一条流程化指令的地址
*/
register BeamInstr *I REG_I = NULL;
/*
* 剩余的reds数量,到达0时函数过程不再被执行,而是返回到调度器
*/
register Sint FCALLS REG_fcalls = 0;
#define Arg(N) I[(N)+1]
#define SET_I(ip) \
ASSERT(VALID_INSTR(* (Eterm *)(ip))); \
I = (ip)
#if defined(NO_JUMP_TABLE) // 没有跳转表,使用 switch-case
# define Goto(Rel) {Go = (int)(Rel); goto emulator_loop;}
#else // 有跳转表,使用 goto
# define Goto(Rel) goto *((void *)Rel)
#endif
结合以上的定义,很多内容都很好理解,但是((Export *) Arg(0))->addressv[erts_active_code_ix()] 这个还是需要解释。
这里要知道addressv 的定义,这个字段是导出函数的代码地址, 在这里,实际地址指向了 ((Export*) Arg(0))-> code[3], 所以通知这个地址也可以知道是哪个函数调用。erts_active_code_ix() 表示了当前使用的 code index
/*
* 导出函数的数据结构(export.h)
*/
typedef struct export
{
void* addressv[ERTS_NUM_CODE_IX]; // 函数代码地址
BeamInstr fake_op_func_info_for_hipe[2]; /* MUST be just before code[] */
/*
* code[0]: 模块名
* code[1]: 函数名
* code[2]: 参数个数
* code[3]: 'address'字段没有指向它的时候是 0 ;
* 否则就是函数流程化代码的指令地址
* code[4]: 指向bif函数地址 (仅BIFs),
* 或者是指向函数流程化代码 (当 on_load 还没被执行时),
* 或者是指向code[3] (如果是 breakpont指令时),
* 默认是 0
*/
BeamInstr code[5];
} Export;
估计还有同学很难明白,现在直接修改erlang源码演示这个问题:
OpCase(i_call_ext_only_e):
// 加多下面这段代码
do {
BeamInstr* fp1 = (BeamInstr *) (((Export *) Arg(0))->addressv[erts_active_code_ix()]);
erts_fprintf(stderr,"*** mycwq debug *** %T:%T/%d code %p export %p\n",
(Eterm)fp1[-3], (Eterm)fp1[-2], fp1[-1], fp1[0], Arg(0));
} while(0);
Dispatchx();
重新编译erlang源码,然后写个测试例子,内容如下,保存为 test.erl
-module(test).
-compile(export_all).
t2() ->
t:tt().
start() ->
Pid = spawn(fun() -> do_loop() end),
register(t, Pid).
do_loop() ->
receive
Msg ->
io:format("~p~n", [Msg])
end,
t2(),
do_loop().
再写个程序,测试用, t.erl
-module(t).
-compile(export_all).
tt() ->
erlang:memory(),ok.
执行步骤如下(会有很多调试信息出来)
1> c(test).
2> c(t).
3> test:start().
4> whereis(test)!any.
5> l(t).
6> whereis(test)!any.
以上过程会打印刚刚在源码加上的调试信息,现在从调试信息中获取有用的数据,如下:
*** mycwq debug *** t:tt/0 code 0x000000000050e620 export 0x00002aaaae96fbd8
*** mycwq debug *** t:tt/0 code 0x000000000050c4d0 export 0x00002aaaae96fbd8
这里看出以上函数是一样,而且export 指针是一样的,都是0x00002aaaae96fbd8 ;只是执行的代码地址不同。如此说明,VM利用code index来切换代码版本
jump table (跳转表)
关于跳转表就写个例子容易解释,以下是c处理一个条件匹配的结构:
switch( Condition){
case Condition_A :
....
case Condition_B :
....
case Condition_C:
....
} ;
这种结构比较低效,经过汇编后,会产生如下的汇编代码:
cmp ....
jnz ....
cmp ....
jnz ....
cmp ....
jnz ....
比如,你有成千上万的 case 需要处理,而不幸的是最后一个case才匹配到,那么处理这个条件前会经过上万次的 cmp 与 jnz。
所以,高级编译器(如GCC)会引入跳转表的概念,把类似的条件进行聚类,减少比较的次数,然后从跳转表找到相应的位置跳转。
https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html
另外,erlang对跳转表的应用不限于c层面上,在erlang层面也利用跳转表的思想和二分法来提交匹配效率(如case匹配)
结束语
最后,说个题外话,能否使用beam当作ets用?
如果不经常改可以考虑,正好利用到beam并发读效率,像是配置文件可以这么干。但是如果是动态内容,需要经常更新,就不能这么干了。首先,编译beam也有时间开销,数据越多编译时间越长。虽然热更代码时,当前的进程受到调度,但更新过程是bif操作,不会被切出,只要进程获得调度机会,在这个更新beam过程中,其他进程是不受调度的,erlang虽然保证热更新不影响所有进程执行,但是如果beam文件足够大,就会影响进程的并发性。而且,目前code index是全局性,就是说VM不可能并发修改代码。另外,热更代码时要检查是否有进程使用旧代码,就会遍历所有的进程,检查栈和外堆、消息队列。删除旧代码时会遍历整个导出函数表
所以,beam的并发修改比较弱,不适合存储频繁改变的数据。
2015/2/10 修改结束语中beam加载调度说明
2015/4/7 补充结束语中热更检查旧代码说明
参考:http://blog.youkuaiyun.com/mycwq/article/details/43372687