erlang 热更新实现分析

原文 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 补充结束语中热更检查旧代码说明

发表评论

邮箱地址不会被公开。 必填项已用*标注