Internal Implementation of erlang catch (first draft), erlangcatch
Recently, some colleagues in the project team shared the internal data (Eterm) of erlang. Eterm is short for Erlang Term, used to represent any type of data in erlang, that is, any data that erlang can use can be expressed by Eterm. For example, common atom, number, list, tuples, and even pid, port, fun, and ets tables can be expressed using Eterm.
EtermEterm is mainly divided into three categories in VM: List, boxed object, and immediate number. (This division is mainly because complex data cannot be expressed by one machine word)
The boxed object represents complex data types, such as tuples, big integers, and binary. The immediate number indicates some simple small data, such as small integers, atoms, and PIDs.
If you are interested in this section, you can first look at the internal representation and implementation of the siyao Erlang data type. Well written, I won't draw any more.
But why is there catch? I believe many people will have such questions. Therefore, this article focuses on catch.
Catch expressionForm: catch ExprIf no exception occurs during the execution of Expr, the execution result of Expr is returned. However, if an exception occurs, it will be captured.
1> catch 1+2.32> catch 1+a.{'EXIT',{badarith,[...]}}3> catch throw(hello).hello
How does the catch immediate number generate?This data type is only used within the Erlang VM. The Erlang program will not directly operate on this data type. When the Code contains catch or try-catch statements, a catch will be generated, and the data will be placed on the stack of the Erlang process.
The following is a simple description of the source code analysis.
-Module (test).-compile (export_all). t ()-> catch erlang: now (). |
Save as test. erl and use erlc-S test. erl to obtain the assembly code test. S. The content is as follows:
{Function, t, 0, 2 }. {label, 1 }. {line, [{location, "test. erl ", 4}]}. {func_info, {atom, test}, {atom, t}, 0 }. {label, 2 }. {allocate, 1, 0 }.{'Catch ', {y, 0}, {f, 3 }}.{Line, [{location, "test. erl", 5}]}. {call_ext, 0, {extfunc, erlang, now, 0 }}. {label, 3 }.{Catch_end, {y, 0 }}.{Deallocate, 1}. return. |
Here we can see that catch and catch_end appear in pairs. Compile it into opcode, which is easy to analyze in the VM code.
1> c (test). {OK, test} 2> erts_debug: df (test). OK |
Find and generate test. dis. The content is as follows:
04B55938: I _func_info_IaaI 0 test t 004B5594C: allocate_tt 1 004B55954:Catch_yfY (0) f (2017871b) 04B55960: call_bif_e erlang: now/004B55968:Catch_end_yY (0) 04B55970: deallocate_return_Q 1 |
Above, allocate_tt and deallocate_return_Q are implemented in beam_hot.h, and Other implementations in beam_emu.c can be found for relevant code.
Let's take a look at these commands:
// Beam_emu.c OpCase (catch_yf): c_p-> catches ++; // Add 1 yb (Arg (0) = Arg (1) to the number of catches ); // store the catch pointer address in the process stack, that is, f (2017871b) Next (2); // execute the Next command
// Beam_emu.c OpCase (catch_end_y): {c_p-> catches --; // The number of catches in the process minus 1 make_blank (yb (Arg (0 ))); // set the value of the catch immediate number to NIL, and the data will be discarded if (is_non_value (r (0) {// if an exception occurs if (x (1) = am_throw) {// if it is throw (Term), the returned Term r (0) = x (2);} else {if (x (1) = am_error) {// if it is an error (Term), then bring the information of the current stack SWAPOUT; x (2) = add_stacktrace (c_p, x (2), x (3 )); SWAPIN;}/* only x (2) is supported in the rootset here */if (E-HTOP <3 | c_p-> mbuf) {/* Force GC in case add_stacktrace () * created heap fragments * // check that the process heap space is insufficient. Execute gc to avoid off-heap data SWAPOUT; PROCESS_MAIN_CHK_LOCKS (c_p ); fcall- = erts_garbage_collect (c_p, 3, reg + 2, 1); Round (c_p); PROCESS_MAIN_CHK_LOCKS (c_p); SWAPIN;} r (0) = TUPLE2 (HTOP, am_EXIT, x (2); HTOP + = 3 ;}} CHECK_TERM (r (0); Next (1); // execute the Next instruction}
// Beam_hot.h OpCase (deallocate_return_Q): {DeallocateReturn (Arg (0); // release the allocated stack space and execute the catch command}
DeallocateReturn is actually a macro. The Code is as follows:
# Define DeallocateReturn (Deallocate) \ do {\ int words_to_pop = (Deallocate); \ SET_ I (BeamInstr *) cp_val (* E )); \\// parse the instruction address of the current stack E = ADD_BYTE_OFFSET (E, words_to_pop); \ CHECK_TERM (r (0); \ Goto (* I ); \ // executed command} while (0)
At this point, some people may be confused. Is the catch immediate number mentioned above true?
When talking about the catch immediate number, many people can find the following two macros:
// Erl_term.h # define make_catch (x) <_ TAG_IMMED2_SIZE) | _ TAG_IMMED2_CATCH) // convert it to catch immediate tree # define is_catch (x) & _ TAG_IMMED2_MASK) = _ TAG_IMMED2_CATCH) // whether to catch the immediate number.
These two are the generation and determination of the catch immediate number. The code below will refer to the use of these two macros.
Now let's take a look at the process of parsing and loading catch code by VM:
// Beam_load.c has a delete static void final_touch (LoaderState * stp) {int I; int on_load = stp-> on_load; unsigned catches; Uint index; beamInstr * code = stp-> code; Module * modp;/** apply for a catch index and fill in f (Limit 871b) before the catch_yf command, point to beam_catches structure data * because a catch cannot immediately put the whole beam_catches data, only the pointer */index = stp-> catches; catches = BEAM_CATCHES_NIL; while (index! = 0) {// traverse all catch_yf commands BeamInstr next = code [index]; code [index] = BeamOpCode (op_catch_yf ); // point to the opcode address of the catch_yf command // obtain the catch_end command address and construct the beam_catches structure data catches = beam_catches_cons (BeamInstr *) code [index + 2], catches ); code [index + 2] = make_catch (catches); // convert the beam_catches index position to catch immediate number index = next;} modp = erts_put_module (stp-> module ); modp-> curr. catches = catches ;/**.... */}
Let's see when the code will be executed here. Careful personnel will find that many exceptions in the VM will be called like this:
// Execute the anonymous function OpCase (I _apply_fun): {BeamInstr * next; SWAPOUT; next = apply_fun (c_p, r (0), x (1), reg); SWAPIN; if (next! = NULL) {r (0) = reg [0]; SET_CP (c_p, I + 1); SET_ I (next); Dispatchfun ();} goto find_func_info; // here is an error.} // a mathematical error occurs. Or an error is returned. lb_Cl_error: {if (Arg (0 )! = 0) {// If the label address is included, run the jump command OpCase (jump_f): {// here is the jump implementation code jump_f: SET_ I (BeamInstr *) arg (0); Goto (* I) ;}} ASSERT (c_p-> freason! = BADMATCH | is_value (c_p-> fvalue); goto find_func_info; // if an error occurs, go here.} // waiting for message timeout OpCase (I _wait_error ): {c_p-> freason = EXC_TIMEOUT_VALUE; goto find_func_info; // here is an error}
Okay, let's see what find_func_info is?
/* Fall through here */find_func_info: {reg [0] = r (0); SWAPOUT; I = handle_error (c_p, I, reg, NULL ); // get the error command address goto post_error_handling;} post_error_handling: if (I = 0) {// WAIT FOR THE NEXT scheduling erl_exit () and throw an exception to interrupt goto do_schedule ;} else {r (0) = reg [0]; ASSERT (! Is_value (r (0); if (c_p-> mbuf) {// if there is off-heap message data, execute gc erts_garbage_collect (c_p, 0, reg + 1, 3);} SWAPIN; Goto (* I); // Execute Command }}
Then, let's take a brief look at the handle_error function.
// The error handling function static BeamInstr * handle_error (Process * c_p, BeamInstr * pc, Eterm * reg, BifFunction bf) {Eterm * hp; Eterm Value = c_p-> fvalue; eterm Args = am_true; c_p-> I = pc;/* In case we call erl_exit (). */ASSERT (c_p-> freason! = TRAP);/* shoshould have been handled earlier. * // ** Check if we have an arglist for the top level call. if so, this * is encoded in Value, so we have to dig out the real Value as well * as the Arglist. */if (c_p-> freason & EXF_ARGLIST) {Eterm * tp; ASSERT (is_tuple (Value); tp = tuple_val (Value); Value = tp [1]; args = tp [2];}/** Save the stack trace info if the EXF_SAVETRACE flag is set. the * Main reason for doing this separately is to allow throws to later * become promoted to errors without losing the original stack * trace, even if they have passed through one or more catch and * rethrow. it also makes the creation of symbolic stack traces much * more modular. */if (c_p-> freason & EXF_SAVETRACE) {save_stacktrace (c_p, pc, reg, bf, Args);}/** Throws that are not caught are turne D into 'nocatch 'errors */if (c_p-> freason & EXF_THROWN) & (c_p-> catches <= 0) {hp = HAlloc (c_p, 3 ); value = TUPLE2 (hp, am_nocatch, Value); c_p-> freason = EXC_ERROR;}/* Get the fully expanded error term */Value = expand_error_value (c_p, c_p-> freason, value);/* Save final error term and stabilize the exception flags so no further expansion is done. */c_p-> fvalue = Value; c_p-> freason = P RIMARY_EXCEPTION (c_p-> freason);/* Find a handler or die */if (c_p-> catches> 0 | IS_TRACED_FL (c_p, F_EXCEPTION_TRACE ))&&! (C_p-> freason & EXF_PANIC) {BeamInstr * new_pc;/* The Beam handler code (catch_end or try_end) checks reg [0] for THE_NON_VALUE to see if the previous code finished abnormally. if so, reg [1], reg [2] and reg [3] shocould hold the exception class, term and trace, respectively. (If the handler is just a trap to native code, these registers will be ignored .) */reg [0] = THE_NON_VALUE; reg [1] = prediction_tag [GET_EXC_CLASS (c_p-> freason)]; reg [2] = Value; reg [3] = c_p-> ftrace; if (new_pc = next_catch (c_p, reg ))) {// find the nearest catch c_p-> cp = 0 from the process stack;/* To avoid keeping stale references. */return new_pc; // return the address of the catch end command} if (c_p-> catches> 0) erl_exit (1, "Catch not found");} ERTS_SMP_UNREQ_PROC_MAIN_LOCK (c_p ); terminate_proc (c_p, Value); ERTS_SMP_REQ_PROC_MAIN_LOCK (c_p); return NULL; // return 0, that is, execute erl_exit ()}
This topic explains why catch should be placed on the process stack and then implemented using the immediate number.
1. Handling of abnormal interruptionsErlang itself has a speed error principle. When an error occurs, it throws an exception and kill the process. If you want to capture exceptions and obtain the results at the interruption, you must record the address to be returned during the interruption.
2. Multi-layer nested catchBecause catch allows multiple layers of nested structures, the function code in catch can continue to catch, and it cannot be expressed as a simple type variable. This requires an array or linked list structure to represent the relationship chain at the catch level.
Question extended process heap and process stack why is the catch implementation a process stack, not a process heap, or a variable used as the VM scheduling thread?
First, the basic scheduling unit of the erlang VM is the erlang process. If you execute a code segment, you need to run the erlang process. Why can we run the code without authorization in shell? What we see is the result returned to us after the shell implements the process execution. The majority of data generated during code execution by the erlang process is stored on the process stack (except ets, binary, and atom). What is the correspondence between the process stack and the process stack?
In fact, the stack and heap of the erlang process are implemented at the underlying VM layer on the stack of the OS process/thread, because the stack space provided by the OS is actually limited. Here, the low-level Address indicates the heap bottom, and the high-level Address indicates the stack bottom. The blank area at the middle heap top and the top of the stack indicates the space not used by the process stack. When the memory is used, it is scaled in. If the memory is insufficient, gc is executed and erlang is executed, the difference between the process stack and the process stack is that the stack only contains simple data. For complex data, the stack only contains the header (that is, the list, boxed object, and immediate number ), then place the remaining data in the heap.
Here, although the VM will store the complex data to the stack of the erlang process, but the reference (or pointer) is maintained on the stack, the heap data will not be referenced (or pointer) pointing to the stack. This is done to reduce the GC cost. In this way, you only need to scan the stack with a small structure during GC, without scanning the entire stack. (Process dictionary and off-heap data are not discussed for the time being)
Here, the Data Object actually expressed by catch is a beam_catch_t structure, which can at least be expressed as an immediate number and then point to the index or pointer position. In addition, catch is related to the context code of the process running. Multi-layer Nesting is allowed to handle abnormal interruptions. If it is used as a variable of the VM scheduling thread like a register, a more complex design will be introduced.
Try-catch tail recursion the try-catch syntax structure cannot constitute tail recursion
T ()-> try do_something (), t () catch _: _-> OK end. |
Erlang will generate the try and try_end commands during compilation. Here t () actually only executes a local function and cannot constitute tail recursion. This is a good explanation. If you are interested, you can print the assembly code to find this problem. Catch also exists.
Erlang: Using erlang: hibernate will cause the catch or try-catch statement to fail. The previous content shows that try catch will push the return address when an error occurs to the stack, while erlang: hibernate will clear the stack data and cause try-catch to fail.
The solution is as follows. See proc_lib: hibernate implementation:
Hibernate (M, F, A) when is_atom (M), is_atom (F), is_list (A)-> erlang: hibernate (? MODULE, wake_up, [M, F, A]). Wake_up (M, F, A) when is_atom (M), is_atom (F), is_list (A)-> try apply (M, F, A) catch _ Class: reason-> exit (Reason) end. |
I started writing this article a long time ago. However, at this stage, I was very busy. Unfortunately, I encountered a slight chill. So I wrote the article in a rush and wrote the title with the words "first draft". I hope I can forgive me. But it shouldn't be an excuse to write the wrong content. If you see the error, please correct it and I will correct it. Thank you for your support.
Reference: http://blog.csdn.net/mycwq/article/details/44661219