標籤:
在分析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的堆棧。
讓我們聊聊Erlang的Trap機制