原文 2015-02-10 01:08:22 发表于 CSDN,这里对以前写的文章做下收录。
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 我也准备了满满的内容和大家分享,内容在文末延伸阅读。
热更新问题
有个问题,如果'old'版本一直都有进程在调用,在此期间,代码热再更新了会发生什么情况?
热更新时,如果模块存在'old'版本代码,erlang 会 kill 掉所有调用这个'old'版本代码的进程,然后移除掉'old'版本代码,'current'版本变成了'old'版本,新的代码就成了'current'版本。
热更新问题重现
-module(t). -compile(export_all). start() -> Pid = spawn(fun() -> do_loop() end), register(t, Pid). do_loop() -> receive Msg -> io:format("~p~n", [Msg]) end, do_loop().
结果如下:
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 掉了。(想知道在哪被 kill,可在code_server 中 do_purge/3找到)
解决热更新问题
如果进程一直在自己loop里面,就会一直跑着'old'版本的代码,这样的后果就是新的代码没有被使用,而且在下一次热更新时进程会被系统kill掉。
怎么解决这个问题,erlang文档还是能找到答案:
To change from old code to current code, a process must make a fully qualified function call. Example:
-module(m). -export([loop/0]). loop() -> receive code_switch -> m:loop(); Msg -> do_something(), loop() end.
就是在热更新后,给这个进程发消息 code_switch ,这样进程会调用 m:loop()
这里,loop()和m:loop()有什么区别呢?
erlang 根据模块划分,函数分本地调用和外部调用,其中,本地调用是调用本模块内的函数,函数可以不导出,调用形式为 Atom(Args) ;外部调用就是调用别的模块函数,函数必须导出,调用形式为 Module:Function(Args).
在 erlang VM 中,进程调用模块的过程是先加载这个模块当前版本的代码再执行,如果进程一直都是本地调用,那么所有操作都是在进程当前运行的代码中完成。换句话,这个过程中进程不会去加载新的代码。打破这种局面的就是外部调用。
延伸阅读
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,"*** cwqqq 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.
以上过程会打印刚刚在源码加上的调试信息,现在从调试信息中获取有用的数据,如下:
*** cwqqq debug *** t:tt/0 code 0x000000000050e620 export 0x00002aaaae96fbd8
*** cwqqq 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 补充结束语中热更检查旧代码说明