其中,boxed对象表示了复杂数据类型,如元组,大整数,二进制等。而立即数表示的就是一些简单的小数据,如小整数,原子,pid 等。
但这里为什么会有catch?相信不少人都会有这样的疑问。所以,本文就围绕着 catch 做说明。
catch表达式
形式:catch Expr
如果 Expr执行过程没有异常发生,就返回Expr的执行结果。但如果异常发生了,就会被捕获。
1> catch 1+2.
3
2> catch 1+a.
{'EXIT',{badarith,[...]}}
3> catch throw(hello).
hello
catch立即数怎么产生的?
这个数据类型只在 Erlang VM内部使用,Erlang 程序不会直接操作这个数据类型。当代码中含有 catch 或者 try-catch语句时就会产生catch,而且,这个数据将会被放置到Erlang进程的栈上。
源码分析
下面以一个简单做说明。
-module(test).
-compile(export_all).
t() ->
catch erlang:now().
|
保存为test.erl,通过erlc -S test.erl 得到汇编代码 test.S,内容如下:
{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.
|
这里可以看到 catch 和 catch_end 是成对出现的。下面再编译成opcode吧,容易到VM代码中分析。
1> c(test).
{ok,test}
2> erts_debug:df(test).
ok
|
找到生成 test.dis,内容如下:
04B55938: i_func_info_IaaI 0 test t 0
04B5594C: allocate_tt 1 0
04B55954:catch_yfy(0) f(0000871B)
04B55960: call_bif_e erlang:now/0
04B55968:catch_end_yy(0)
04B55970: deallocate_return_Q 1
|
以上, allocate_tt 和 deallocate_return_Q 在 beam_hot.h实现,其他在 beam_emu.c 实现,都可以找相关代码。
先看下这几个指令:
// beam_emu.c
OpCase(catch_yf):
c_p->catches++; // catches数量加1
yb(Arg(0)) = Arg(1); // 把catch指针地址存入进程栈,即f(0000871B)
Next(2); // 执行下一条指令
// beam_emu.c
OpCase(catch_end_y): {
c_p->catches--; // 进程 catches数减1
make_blank(yb(Arg(0))); // 将catch立即数的值置NIL,数据将会丢掉
if (is_non_value(r(0))) { // 如果异常出现
if (x(1) == am_throw) { // 如果是 throw(Term),返回 Term
r(0) = x(2);
} else {
if (x(1) == am_error) { // 如果是 error(Term), 再带上当前堆栈的信息
SWAPOUT;
x(2) = add_stacktrace(c_p, x(2), x(3));
SWAPIN;
}
/* only x(2) is included in the rootset here */
if (E - HTOP < 3 || c_p->mbuf) { /* Force GC in case add_stacktrace()
* created heap fragments */
// 检查进程堆空间不足,执行gc避免出现堆外数据
SWAPOUT;
PROCESS_MAIN_CHK_LOCKS(c_p);
FCALLS -= erts_garbage_collect(c_p, 3, reg+2, 1);
ERTS_VERIFY_UNUSED_TEMP_ALLOC(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); // 执行下一条指令
}
//beam_hot.h
OpCase(deallocate_return_Q):
{
DeallocateReturn(Arg(0));//释放分配的栈空间,返回上一个CP指令地址(注:CP是返回地址指针)
}
DeallocateReturn实际是个宏,代码如下:#define DeallocateReturn(Deallocate) \
do { \
int words_to_pop = (Deallocate); \
SET_I((BeamInstr *) cp_val(*E)); \ // 解析当前栈的指令地址,即获取上一个CP指令地址
E = ADD_BYTE_OFFSET(E, words_to_pop); \
CHECK_TERM(r(0)); \
Goto(*I); \//执行的指令
} while (0)
到这里,应该有同学开始疑惑了。这里说的catch ,真是前面提到的 catch立即数吗?
谈到 catch 立即数,很多同学可以找到以下这两个宏:
// erl_term.h
#define make_catch(x) (((x) << _TAG_IMMED2_SIZE) | _TAG_IMMED2_CATCH) // 转成catch立即树
#define is_catch(x) (((x) & _TAG_IMMED2_MASK) == _TAG_IMMED2_CATCH) // 是否catch立即数
这两个是 catch立即数的生成和判定,后面的代码会提到这两个宏的使用。
现在,我们来看下VM解析和加载 catch 代码的过程:
// beam_load.c 加载beam过程,有删节
static void final_touch(LoaderState* stp)
{
int i;
int on_load = stp->on_load;
unsigned catches;
Uint index;
BeamInstr* code = stp->code;
Module* modp;
/*
* 申请catch索引,填补catch_yf指令
* 前面的f(0000871B)就在这里产生的,指向了beam_catches结构数据
* 因为一个catch立即数放不了整个beam_catches数据,就只放了指针
*/
index = stp->catches;
catches = BEAM_CATCHES_NIL;
while (index != 0) { //遍历所有的catch_yf指令
BeamInstr next = code[index];
code[index] = BeamOpCode(op_catch_yf); // 指向catch_yf指令的opcode地址
// 获取 catch_end 指令地址,构造beam_catches结构数据
catches = beam_catches_cons((BeamInstr *)code[index+2], catches);
code[index+2] = make_catch(catches); // 将beam_catches索引位置转成 catch立即数
index = next;
}
modp = erts_put_module(stp->module);
modp->curr.catches = catches;
/*
* ....
*/
}
再来看下什么时候会执行到 这里的代码。
细心的同学就会发现,VM中很多异常都会这样调用:// 执行匿名函数
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; // 遇到错误走这里
}
// 数学运算错误,或检查错误就会走这里
lb_Cl_error: {
if (Arg(0) != 0) { // 如果带了 label地址,就执行 jump指令
OpCase(jump_f): { // 这里就是 jump实现代码
jump_f:
SET_I((BeamInstr *) Arg(0));
Goto(*I);
}
}
ASSERT(c_p->freason != BADMATCH || is_value(c_p->fvalue));
goto find_func_info; // 遇到错误走这里
}
// 等待消息超时
OpCase(i_wait_error): {
c_p->freason = EXC_TIMEOUT_VALUE;
goto find_func_info; // 遇到错误走这里
}
好了,再看下find_func_info 究竟是什么神通?
/* Fall through here */
find_func_info: {
reg[0] = r(0);
SWAPOUT;
I = handle_error(c_p, I, reg, NULL); // 获取异常错误指令地址
goto post_error_handling;
}
post_error_handling:
if (I == 0) { // 等待下次调度 erl_exit(),抛出异常中断
goto do_schedule;
} else {
r(0) = reg[0];
ASSERT(!is_value(r(0)));
if (c_p->mbuf) { // 存在堆外消息数据,执行gc
erts_garbage_collect(c_p, 0, reg+1, 3);
}
SWAPIN;
Goto(*I); // 执行指令
}
}
然后,简单看下handle_error函数。// erl_emu.c VM处理异常函数
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); /* Should 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 turned 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 = PRIMARY_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] should 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] = exception_tag[GET_EXC_CLASS(c_p->freason)];
reg[2] = Value;
reg[3] = c_p->ftrace;
if ((new_pc = next_catch(c_p, reg))) { // 从进程栈上找到最近的 catch
c_p->cp = 0; /* To avoid keeping stale references. */
return new_pc; // 返回 catch end 指令地址
}
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; // 返回0,就是执行 erl_exit()
}
问题讨论
为什么catch要放在进程栈,然后利用立即数实现。
1、异常中断处理
erlang本身就有速错原则,发生错误就会抛出异常,并kill掉进程。如果需要捕获异常,并获取中断处的结果,就要记录中断时要返回的地址。
2、catch多层嵌套
因为catch允许多层嵌套结构,catch里面的函数代码还可以继续再catch,就无法用一个简单类型的变量表示。这需要一种数组或链表结构来表示catch层级的关系链。
问题延伸
进程堆与进程栈
至于catch实现为什么是进程栈,而不是进程堆,或者作为VM调度线程的变量?
首先,erlang VM的基本调度单位是erlang进程。如果执行某段代码,就要有运行erlang进程来执行。为什么我们可以在shell下肆无忌惮地运行代码,实际上我们看到的是由shell实现进程执行后返回给我们的结果。
而erlang进程执行代码过程中产生的大多数数据会放到进程的堆栈上(ets,binary,atom除外),而进程栈和进程堆是什么样的对应关系?
实际上,erlang进程的栈和堆在VM底层实现上都是在OS进程/线程的堆上,因为OS提供的栈空间实在有限。
这里,低位地址表示了堆底,高位地址表示了栈底。中间堆顶和栈顶的空白区域,表示了进程堆栈还未使用到的空间,使用内存时就向里收缩,不够时就执行gc
而erlang中,进程栈和进程堆的区别是栈只放入了简单的数据,如果是复杂数据,就只放头部(即前面最开始谈到的列表,boxed对象),然后把实际的数据放到堆中。
这里,尽管VM会把复杂数据存入erlang进程的堆上,但在栈上都保持了引用(或指针),但是,堆数据不会有引用(或指针)指向了栈。这么做,是为了减少GC的代价。这样,GC时只要扫描结构较小的栈就可以,不用扫描整个堆栈。而进程字典写操作,就保留引用指向进程堆(暂时不讨论堆外数据的情况)
而这里,catch实际表达的数据对象是一个beam_catch_t 结构,最少最多也只能一个立即数表示,然后指向索引或指针位置。而且,catch与进程运行上下文代码有关,允许多层嵌套,处理异常中断,如果像寄存器一样,作为VM调度线程的变量,将会引入更加复杂的设计
try-catch尾递归
try-catch语法结构内无法构成尾递归
t() ->
try
do_something(),
t()
catch
_:_ -> ok
end.
|
erlang编译时会生成 try 和 try_end 指令,而这里 t() 实际就只是执行一次本地函数,不能构成尾递归。这点很好解释,感兴趣的同学可以打印汇编码探寻这个问题。同样的问题,catch也存在。
erlang:hibernate
调用这个函数会使当前进程进入wait状态,同时减少其内存开销。适用场合是进程短时间不会收到任何消息,如果频繁收到消息就不适合了,否则频繁内存清理操作,也是不少开销。
但是,使用 erlang:hibernate 将会导致 catch或者try-catch 语句失效。
通过前面的内容可以知道,try catch 会往栈里面都压入了出错时候的返回地址,而 erlang:hibernate则会清空栈数据,将会导致try-catch失效。
解决办法如下,参考proc_lib:hibernate的实现:
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.
|
最后语
很早之前就开始写这篇文章,但现阶段比较忙,不巧又遇上一点小风寒,所以文章写得仓促,标题打了“初稿”字样,希望能够见谅。但也不该成为写错内容的借口,如果你有看到错误,请提出批评指正,我看到就会改正,谢谢支持。
2015/4/3 修改源码分析中deallocate_return_Q的说明
参考:http://blog.csdn.net/mycwq/article/details/44661219
相关推荐
Erlang零成本实现云计算,为初学者提供参考和学习,并为企业建设云提供帮助
在给定的文件内容中,通过Erlang语言实现了KMP算法,以下是从文件内容中提取的关键知识点: 1. kmp_search/2函数:这是KMP算法的主函数,用于检查子串(SubString)是否存在于主串(String)中。该函数调用了kmp_...
此外,提供的"erlang的timer和实现机制.pdf"和"更多erlang资料下载.txt"也是进一步学习和研究的好资源。 总之,Erlang的timer模块是其强大并发能力的一个体现,通过熟练掌握这一部分,开发者可以构建出更加高效和...
Erlang emulator 实现分析Erlang emulator 实现分析
Erlang零成本实现云计算.pptx
- **分布式**:Erlang节点可以在多台机器上运行,并可以相互通信,实现分布式计算。 - **热升级**:Erlang支持在线代码升级,无需停机即可更新应用,保证服务的连续性。 - **错误处理**:Erlang采用异常处理机制,...
Erlang零成本实现云计算讲义.pptx
标题"erlang ranch实现的游戏tcp服务器"表明我们将探讨如何利用Ranch库来创建一个针对游戏场景的TCP服务器。在这个项目中,我们可能涉及到以下几个核心知识点: 1. **Erlang语言基础**:Erlang是一种函数式编程语言...
Erlang B和Erlang C是电信领域中两种重要的流量模型,用于预测和分析通信系统中的呼叫处理能力和拥塞情况。这两个模型由丹麦工程师Agner Krarup Erlang在20世纪初提出,至今仍广泛应用于现代通信网络的设计与优化。 ...
本案例聚焦于使用Erlang编程语言实现百万级用户的排行榜桶排序方法。Erlang以其并发处理和分布式计算能力而闻名,非常适合处理大数据场景。 桶排序(Bucket Sort)是一种非比较型整数排序算法,它的基本思想是将待...
erlang文献及资料汇总 入门资料: erlang中文手册(R11B 文档译文,最适合入门) ...erlang VM内部数据共享机制 erlang 消息传递机制 文章地址:http://blog.csdn.net/mycwq/article/details/43115733
- **Error Handling**:Erlang的错误处理通常通过`try...catch`结构实现,确保程序在遇到异常时能够优雅地恢复或终止。 4. **并发与通信**: - **Erlang进程间的通信**:如果游戏涉及到多个玩家,每个玩家可能会...
Erlang的并发模型和 otp 设计模式使得系统能够轻松实现负载均衡和故障切换。当一个节点或进程失败时,系统可以重新分配工作,确保服务的连续性。 ### 9. BEAM虚拟机 BEAM是Erlang运行时系统的名称,全称为伯尔尼...
RabbitMQ,一个基于Erlang开发的消息队列系统,是实现异步通信的关键组件。 标题中提到的“Erlang官网下载过慢”可能是因为网络问题或者官方服务器的繁忙导致的,这对于急需安装或更新Erlang的开发者来说是一个常见...
作为“源码软件”,Erlang 25.0同样提供了源代码,开发者可以深入研究其内部工作原理,进行定制化开发,或者为Erlang社区贡献代码。对于开发者而言,理解Erlang的源码可以帮助他们更好地利用这个平台,实现更高效、...
1. **并发模型**:Erlang的并发基于轻量级进程(Lightweight Processes, LSPs),这些进程间的通信通过消息传递实现,这与传统的线程模型不同,具有更好的隔离性和容错性。 2. ** OTP(Open Telecom Platform)**:...
Erlang是一种面向并发的、函数式编程语言,被广泛应用于分布式系统和高可用性服务。在Erlang中,ETS(Erlang Term Storage)是内置的一种高效、内存中的数据库,用于存储和检索Erlang术语。然而,ETS的一个限制是它...
2. **掌握并发编程**:习题可能包含创建和管理Erlang进程、实现进程间的消息传递,帮助学习者理解并发编程的核心概念。 3. **函数式编程思维**:习题可能涉及无副作用函数的编写,以及如何利用函数式编程特性如递归...