让我们聊聊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的堆栈。

时间: 2024-10-04 18:25:59

让我们聊聊Erlang的Trap机制的相关文章

Erlang ERTS的Trap机制的设计及其用途

出处:http://mryufeng.iteye.com/blog/334744 erlang的trap机制在实现中用的很多,在费时的BIF操作中基本上都可以看到.它的实现需要erl vm的配合.它的作用基本上有3个: 1. 把费时操作分阶段做.由于erlang是个软实时系统,一个进程或者bif不能无限制的占用cpu时间.所以erlang的每个进程执行的时候,最多只能执行一定数量的指令.这个是设计方面的目标.实现上也要配套.所以比如md5,list_member查找这种可能耗时的操作都是用tra

Erlang垃圾回收机制的二三事

声明:本片文章是由Hackernews上的[Erlang Garbage Collection Details and Why ItMatters][1]编译而来,本着学习和研究的态度,进行的编译,转载请注明出处. Erlang需要解决的重要问题之一就是为实现极高响应能力的软实时系统创建平台.这样的系统需要一个快速的垃圾回收机制,而这个机制不会阻止系统及时的响应.另一方面,当我们把Erlang看作一种用无损更新属性的不可改变语言时,这个垃圾回收机制就显得更加重要了,因为这种语言有很高的几率产生垃

[strongswan] strongswan是如何实现与xfrm之间的trap机制的

目录 strongswan与xfrm之间的trap机制 0. 1. 前言 2. 描述 2.1 none 2.2 trap 3. 实验与过程 3.1 trap实验 3.2 none实验 4 背景知识 5. 机制分析 5.1 什么是acquire 5.2 那么,什么时候发送acquire消息呢 6. strongswan与xfrm之间的trap机制 0. 你必须同时知道,strongswan,xfrm,strongswan connect trap三个概念. 才有继续读下去的意义. 入门请转到:[T

Erlang 热更新机制

Current and Old Code The code of a module can exists in two variants in a system: current code and old code. When a module is loaded into the system for the first time, the code of the module becomes 'current' and the global export table is updated w

转---JS 一定要放在 Body 的最底部么?聊聊浏览器的渲染机制

作者:德来 segmentfault.com/a/1190000004292479 如有好文章投稿,请点击 → 这里了解详情 一.从一个面试题说起 面试前端的时候我喜欢问一些看上去是常识的问题.比如:为什么大家普遍把<script src=""></script>这样的代码放在body最底部?(为了沟通效率,我会提前和对方约定所有的讨论都以chrome为例) 应聘者一般会回答:因为浏览器生成Dom树的时候是一行一行读HTML代码的,script标签放在最后面就不

让我们聊聊Erlang的垃圾回收

原Blog地址,http://www.linkedin.com/pulse/garbage-collection-erlang-tianpo-gao?trk=prof-post. 本文将简单的描述Erlang的垃圾回收,并不是深入的探讨. 在Erlang运行时环境中,Erlang进程采用复制分代回收的方式.分代垃圾回收将内存对象划分为不同的代.在Erlang运行时环境中,有两个代,年轻代和老年代.在Erlang的运行时环境中,内存回收主要有两种,一种叫做部分垃圾回收,另一种叫做全量垃圾回收. 在

JS一定要放在Body的最底部么? 聊聊浏览器的渲染机制

请参看文章 https://segmentfault.com/a/1190000004292479 网上的回答: 1.js加载会阻塞其它内容加载,使页面加载时间更长,尤其是js文件太大,有的页面js文件数兆/客户端网速太慢/服务器网速太慢,甚至不能访问等情况. 2.dom操作,页面没提前加载,dom操作会失败,报错. 3.搜索引擎优化.

让我们聊聊Erlang的节点互联(二)

由于一篇Blog写太长无法发表,这里面我们将继续分析下dist.c中的setnode_3这个函数的作用和net_kernel得到连接成功之后又进行了什么操作. BIF_RETTYPE setnode_3(BIF_ALIST_3) {     BIF_RETTYPE ret;     Uint flags;     unsigned long version;     Eterm ic, oc;     Eterm *tp;     DistEntry *dep = NULL;     Port 

ERLANG远端节点奔溃导致发消息进程堵消息问题探源

问题描述:在生产环境中出现一例性能问题,A和B两个结点运行在两台服务器上,A与B互联,A不断向B发送消息.B结点所在机器发生宕机,导致A结点中发送消息的进程赌消息. 追踪过程:通过erlang:process_info(erlang:whereis(Pid))发现current_function一直是gen:do_call/4.messages消息堆积到数十万级别. 源码分析:在代码中向远端发送消息的调用函数为erlang:send(Pid,Msg),Pid是属于远端结点的接收进程.对该函数做一