在分析erlang:send的bif时候发现了一个BIF_TRAP这一系列宏。参考了Erlang自身的一些描述,这些宏是为了实现一种叫做Trap的机制。Trap机制中将Erlang的代码直接引入了Erts中,可以让C函数直接"使用"这些Erlang的函数。
先让我们思考下为什么Erlang为什么要实现Trap机制?让我先拿最近比较火的Go来说下,Go本身是编译型的和Erlang这种OPCode解释型的性质是不同的。Go的Runtime中很多函数本身也是用C语言实现的,为了胶和Go代码和C代码,Go的Runtime中使用了大量的汇编去操作Go函数的堆栈和C语言的堆栈。于此同时,为了进行Go的协作线程切换,又要使用大量的汇编语言去修改Go函数的堆栈。这样做需要Runtime的编写者对C编译器很熟悉,对相应平台的硬件ABI相当熟悉,更关键的是大大的分散了Runtime作者的精力,不能让Runtime作者的精力放在垃圾回收和协程调度。从另一方面,我们也可以分析出来为什么GO很难实现像Erlang那种软实时的公平调度了。
Erlang实现Trap机制,我个人认为有以下几个原因:
将用C函数实现比较困难的功能用Erlang来实现,直接引入到Erts中。
延迟执行,将和Driver相关的操作或者需要通过OTP库进行决策的事情,交给Erlang来实现。
主动放弃CPU,让调度进行再次调度。这个相当于让BIF支持了yield,防止C函数执行时间过长,不能保证软实时公平调度。
Erlang又是怎么实现Trap机制的?Erlang的Trap机制是通过使用Trap函数,BIF_TRAP宏和调度器协作来完成的。下面让我以erlang:send这个BIF和beam_emu中的部分代码来说下Trap的流程。
我们先看下进入BIF的代码:
OpCase(call_bif_e): { Eterm (*bf)(Process*, Eterm*, BeamInstr*) = GET_BIF_ADDRESS(Arg(0)); Eterm result; BeamInstr *next; PRE_BIF_SWAPOUT(c_p); c_p->fcalls = FCALLS - 1; if (FCALLS <= 0) { save_calls(c_p, (Export *) Arg(0)); } PreFetch(1, next); ASSERT(!ERTS_PROC_IS_EXITING(c_p)); reg[0] = r(0); result = (*bf)(c_p, reg, I); ASSERT(!ERTS_PROC_IS_EXITING(c_p) || is_non_value(result)); ERTS_VERIFY_UNUSED_TEMP_ALLOC(c_p); ERTS_HOLE_CHECK(c_p); ERTS_SMP_REQ_PROC_MAIN_LOCK(c_p); PROCESS_MAIN_CHK_LOCKS(c_p); //如果mbuf不空,且overhead已经超过了二进制堆的大小,那么需要进行一次垃圾回收 if (c_p->mbuf || MSO(c_p).overhead >= BIN_VHEAP_SZ(c_p)) { Uint arity = ((Export *)Arg(0))->code[2]; result = erts_gc_after_bif_call(c_p, result, reg, arity); E = c_p->stop; } HTOP = HEAP_TOP(c_p); FCALLS = c_p->fcalls; //看是否直接得道了结果 if (is_value(result)) { r(0) = result; CHECK_TERM(r(0)); NextPF(1, next); //没有结果,返回了THE_NON_VALUE } else if (c_p->freason == TRAP) { //设置进程的接续点 SET_CP(c_p, I+2); //设置改变scheduler正在执行的指令 SET_I(c_p->i); //重新进场,更新快存 SWAPIN; r(0) = reg[0]; Dispatch(); }
所有Erlang代码要调用BIF操作的时候,都会产生一个call_bif_e的Erts指令。当调度器执行到这个指令的时候,先要找到BIF函数的所在地址,然后通过C语言调用执行BIF获得result,同时根据约定如果result存在则直接放入快存x0(r(0))然后继续执行,如果没有返回值同时freason是TRAP,那么我们就触发TRAP机制。
再让我们看下erl_send的部分代码
switch (result) { case 0: /* May need to yield even though we do not bump reds here... */ if (ERTS_IS_PROC_OUT_OF_REDS(p)) goto yield_return; BIF_RET(msg); break; case SEND_TRAP: BIF_TRAP2(dsend2_trap, p, to, msg); break; case SEND_YIELD: ERTS_BIF_YIELD2(bif_export[BIF_send_2], p, to, msg); break; case SEND_YIELD_RETURN: yield_return: ERTS_BIF_YIELD_RETURN(p, msg); case SEND_AWAIT_RESULT: ASSERT(is_internal_ref(ref)); BIF_TRAP3(await_port_send_result_trap, p, ref, msg, msg); case SEND_BADARG: BIF_ERROR(p, BADARG); break; case SEND_USER_ERROR: BIF_ERROR(p, EXC_ERROR); break; case SEND_INTERNAL_ERROR: BIF_ERROR(p, EXC_INTERNAL_ERROR); break; default: ASSERT(! "Illegal send result"); break; }
我们可以看到这里面使用了BIF_TRAP很多宏,那么这个宏做了什么呢?这宏非常简单
#define BIF_TRAP2(Trap_, p, A0, A1) do { \ Eterm* reg = ERTS_PROC_GET_SCHDATA((p))->x_reg_array; \ (p)->arity = 2; \ reg[0] = (A0); \ reg[1] = (A1); \ (p)->i = (BeamInstr*) ((Trap_)->addressv[erts_active_code_ix()]); \ (p)->freason = TRAP; \ return THE_NON_VALUE; \ } while(0)
就是偷偷的改变了Erlang进程的指令i,同时,直接让函数返回THE_NON_VALUE。
这个时候有人大概会说,这不是天下大乱了,偷偷改掉了Erlang进程执行的指令,那么这段代码执行完了,怎么能回到原来模块的代码中呢。我们可以再次回到调度器的代码中,我们可以看到,调度器的全局指令I还是正在执行的模块的代码,调度器发现了TRAP的存在,先让进程的接续指令cp(相当Erlang函数的退栈返回地址)直接为I+2也就是原来模块中的下一条指令,然后再将全局指令I设置为Erlang进程指令i,接着执行下去。从Trap宏中,我们不难看出Trap函数是什么了,就是一个Export的数据结构。
最后我们分析下为什么Erlang要这样实现TRAP。主要原因是Erlang是OPCode解释型的,Erlang进程执行的流程可控。另一个原因是,直接使用C语言的编译器来完成C函数的退栈和堆栈操作时,兼容性和稳定性要好很多不需要编写平台相关的汇编代码去操作C的堆栈。