让我们聊聊Erlang的Trap机制

在分析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机制,我个人认为有以下几个原因:

  1. 将用C函数实现比较困难的功能用Erlang来实现,直接引入到Erts中。

  2. 延迟执行,将和Driver相关的操作或者需要通过OTP库进行决策的事情,交给Erlang来实现。

  3. 主动放弃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的堆栈。





你可能感兴趣的:(Erlang的Trap机制,Trap函数,公平调度)