0

Erlang的Trap 机制

 2 years ago
source link: https://www.ttalk.im/2019/09/erlang-trap.html?amp%3Butm_medium=Atom
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

介绍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的代码:

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的堆栈。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK