Erlang的Trap 机制

介绍Erlang的Trap机制,以及为什么需要Trap机制,让读者可以更好的理解Erlang是如何将 同步的操作,变成非阻塞的异步操作。

什么是Trap机制

在分析erlang:send的bif时候发现了一个BIF_TRAP这一系列宏。参考了Erlang自身的一些描 述,这些宏是为了实现一种叫做Trap的机制。Trap机制中将Erlang的代码直接引入了Erts中, 可以让C函数直接“使用”这些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的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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的部分代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
    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很多宏,那么这个宏做了什么呢?这宏非常简单

1
2
3
4
5
6
7
8
9
#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的堆栈。