高手写的erlang的一些内部机制分析
来源:互联网 发布:ubuntu怎么设置中文 编辑:程序博客网 时间:2024/06/04 01:19
转自 http://my.oschina.net/u/236698/blog?catalog=284836 博客地址,感谢
http://my.oschina.net/u/236698/blog/387237
让我们聊聊Erlang虚拟机的设计理念
使用Erlang很久,一直想为什么要设计Port,但是最近深入的阅读了Erts的代码后有了一些想法。
当我从erlang:open_port这个函数开始跟踪这个Port创建的时候,发现Port在创建的时候并没有被调度到scheduler上或者专用的线程上去,而是简简单单的几个数据结构。那么Port是怎么被调度到scheduler上的呢?通过代码阅读,我从erl_port_task.c文件中找到了erts_port_task_schedule函数,正是这个函数将Port调度到了scheduler上的。之后我发现在erl_check_io的时候也会调用erts_port_task_schedule这个函数。从而我得到下面的结论:
Erlang的Port并不是一直在调度器的队列中的。
Erlang的Port在被Erlang进程通过erlang:controll和erlang:command函数发送命令时,才会被放入RunQueue。
Erlang的Port如果向erts中注册了IO任务,则在scheduler进行erl_check_io的时候才会被放入RunQueue。
Erlang的Port和Erlang进程类似,可以进行link和monitor,从而达到link的进程退出Port也随之退出。
Erlang的Port总有一个connected的Erlang进程,这个Erlang进程是Port默认发送消息的进程。
至此我们可以看出Erlang虚拟机的基本理念如下:
Erts是以scheduler为核心的,一个全力执行任务队列中任务的机器。
Erts不将复杂的IO处理当作整个虚拟机的一部分,而是将IO事件分配当作整个虚拟机的一部分。
Erts将任务分为两类,一类是OPCode执行(Erlang的进程),一类是IO任务的执行(Erlang的Port)。
Erlang运行时环境选择这样做,我认为有以下几个原因:
复杂的IO处理如果作为Erts的一部分,会引入大量的代码,同时对各个平台的兼容性很难保证。所以将IO分配作为Erts的一部分且将复杂IO的操作抽象为driver(Erlang的Port),这样会大大减少代码量和提高对平台的兼容性。
这样设计符合Erlang整体以进程和消息为中心的设计理念。因为在Erlang进程和IO操作分开,让IO任务化,这样Erlang调度器复杂度就降低了很多。在这种情况下,Erlang的进程就是和Erlang的IO操作都是任务队列上的一个个任务,方便调度和任务密取的进行。
总结下,Erts核心理念就是执行任务和调度任务,让整个系统的吞吐最大化。
http://my.oschina.net/u/236698/blog/388315
让我们聊聊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机制,我个人认为有以下几个原因:
将用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的堆栈。
让我们聊聊global模块
如何快速提高你的薪资?-实力拍“跳槽吧兄弟”梦想活动即将开启
让我们聊聊Erlang的垃圾回收
原Blog地址,http://www.linkedin.com/pulse/garbage-collection-erlang-tianpo-gao?trk=prof-post。
本文将简单的描述Erlang的垃圾回收,并不是深入的探讨。
在Erlang运行时环境中,Erlang进程采用复制分代回收的方式。分代垃圾回收将内存对象划分为不同的代。在Erlang运行时环境中,有两个代,年轻代和老年代。在Erlang的运行时环境中,内存回收主要有两种,一种叫做部分垃圾回收,另一种叫做全量垃圾回收。
在Erlang运行时环境中,每当进程的堆没有足够的空间去存储新的对象的时候,将会触发对该进程的垃圾回收。由于一个Erlang进程的堆栈上的数据不和其它Erlang进程共享,当Erlang进程终止执行的时候,并不会进行垃圾回收,而是直接交还给Erlang运行时环境。当发生垃圾回收的时候,进行垃圾回收的Erlang进程会被暂停,但是支持SMP的Erlang执行环境,会继续执行其它的Erlang进程,而不是整体暂停。在进行垃圾回收的时候,部分垃圾回收会先执行。全量垃圾回收会在执行一定次数的部分垃圾回收后执行,或者当部分垃圾回收无法释放出足够的空间时,全量垃圾回收也会被执行。
在执行部分垃圾垃圾回收时,垃圾回收器只对年轻代进行垃圾回收,并且将老年代移动到老年代专用堆中。当一个Erlang的Term经历了2到3个部分垃圾回收,那么这个Term将被提升到老年代。当进行全量回收的时候,垃圾回收器会对年轻代和老年代进行垃圾回收。
那么我们是如何将内存对象划分成不同的代的?在Erlang运行时环境中,Erlang进程的控制块PCB中有一个叫做high_water的字段。当一个存储在Erlang进程堆上的Term的地址比high_water存储的值要大,那么这个Term就是年轻代,反之就是老年代。
在Erlang运行时环境中,内存回收时暂停且复制的分代回收器。每次进行垃圾回收,都会创建出一个新的堆,当垃圾回收完成之后,Erlang进程原有的堆会被释放,并且新的堆将会成为当前Erlang进程的堆,当然原有堆中存活的数据将会被移动到新的堆中。
让我们聊聊Erlang的节点互联(一)
前面我们已经聊过了Erlang的Global模块和Trap机制。这篇Blog,将会讨论下Erlang的节点是怎么互联的,主要是对net_kernel的一些代码分析。由于oschina的编辑器不支持Erlang的语法高亮,请亲们多多见谅吧。
在Erlang整个环境启动的时候,会创建一个叫做net_kernel的Erlang进程,这个进程是一个gen_server。net_kernel主要用来处理Erlang网络协议。下面我们就进入正题,net_kernel中的connect函数。
net_kenrel:connect本身就是一个gen_server:call,我们直接看net_kernel:handle_call的代码。
handle_call({connect, _, Node}, From, State) when Node =:= node() ->
async_reply({reply,
true
, State}, From);
handle_call({connect, Type, Node}, From, State) ->
verbose({connect, Type, Node}, 1, State),
case
ets:lookup(sys_dist, Node) of
[Conn] when Conn#connection.state =:= up ->
async_reply({reply,
true
, State}, From);
[Conn] when Conn#connection.state =:= pending ->
Waiting = Conn#connection.waiting,
ets:insert(sys_dist, Conn#connection{waiting = [From|Waiting]}),
{noreply, State};
[Conn] when Conn#connection.state =:= up_pending ->
Waiting = Conn#connection.waiting,
ets:insert(sys_dist, Conn#connection{waiting = [From|Waiting]}),
{noreply, State};
_ ->
case
setup(Node,Type,From,State) of
{ok, SetupPid} ->
Owners = [{SetupPid, Node} | State#state.conn_owners],
{noreply,State#state{conn_owners=Owners}};
_ ->
?connect_failure(Node, {setup_call, failed}),
async_reply({reply,
false
, State}, From)
end
end;
其中,我们可以看出,如果目标节点是自身,那么直接就忽略掉,返回成功。如果目标节点不是自身,先看一下ets中是否有向远程节点连接的进程。当这进行连接的进程状态是up,则直接返回true,否则将请求进程加入连接等待队列中。如果我们没有向远程节点进行连接的进程,则调用setup函数来建立一个。让我接着跟踪一下setup这个函数做了什么。
%连接新的节点
setup(Node,Type,From,State) ->
Allowed = State#state.allowed,
case
lists:member(Node, Allowed) of
false
when Allowed =/= [] ->
error_msg(
"** Connection attempt with "
"disallowed node ~w ** ~n"
, [Node]),
{error, bad_node};
_ ->
case
select_mod(Node, State#state.listen) of
%获得连接远程节点的Module
{ok, L} ->
Mod = L#listen.module,
LAddr = L#listen.address,
MyNode = State#state.node,
Pid = Mod:setup(Node,
Type,
MyNode,
State#state.type,
State#state.connecttime),
Addr = LAddr#net_address {
address = undefined,
host = undefined },
ets:insert(sys_dist, #connection{node = Node,
state = pending,
owner = Pid,
waiting = [From],
address = Addr,
type = normal}),
{ok, Pid};
Error ->
Error
end
end.
%%
%% Find a module that is willing to handle connection setup to Node
%%
select_mod(Node, [L|Ls]) ->
Mod = L#listen.module,
case
Mod:select(Node) of
true
-> {ok, L};
false
-> select_mod(Node, Ls)
end;
select_mod(Node, []) ->
{error, {unsupported_address_type, Node}}.
在setup函数中,我们需要先找出连接远程节点所使用的模块名称,一般情况下是inet_tcp_dist这个模块。我们下面假定是使用inet_tcp_dist这个模块,这个时候net_kernel会调用inet_tcp_dist:setup,并将成功后的Erlang进程ID放入ets中。
让我们看下inet_tcp_dist:setup函数
setup(Node, Type, MyNode, LongOrShortNames,SetupTime) ->
spawn_opt(?MODULE, do_setup,
[self(), Node, Type, MyNode, LongOrShortNames, SetupTime],
[link, {priority, max}]).
do_setup(Kernel, Node, Type, MyNode, LongOrShortNames,SetupTime) ->
?trace(
"~p~n"
,[{inet_tcp_dist,self(),setup,Node}]),
[Name, Address] = splitnode(Node, LongOrShortNames),
case
inet:getaddr(Address, inet) of
{ok, Ip} ->
Timer = dist_util:start_timer(SetupTime),
%用epmd协议获得远程节点的端口
case
erl_epmd:port_please(Name, Ip) of
{port, TcpPort, Version} ->
?trace(
"port_please(~p) -> version ~p~n"
,
[Node,Version]),
dist_util:reset_timer(Timer),
%连接远程节点
case
inet_tcp:connect(Ip, TcpPort,
[{active,
false
},
{packet,2}]) of
%拿到Socket之后,定义各种回调函数,状态以及状态机函数
{ok, Socket} ->
HSData = #hs_data{
kernel_pid = Kernel,
other_node = Node,
this_node = MyNode,
socket = Socket,
timer = Timer,
this_flags = 0,
other_version = Version,
f_send = fun inet_tcp:send/2,
f_recv = fun inet_tcp:recv/3,
f_setopts_pre_nodeup =
fun(S) ->
inet:setopts
(S,
[{active,
false
},
{packet, 4},
nodelay()])
end,
f_setopts_post_nodeup =
fun(S) ->
inet:setopts
(S,
[{active,
true
},
{deliver, port},
{packet, 4},
nodelay()])
end,
f_getll = fun inet:getll/1,
f_address =
fun(_,_) ->
#net_address{
address = {Ip,TcpPort},
host = Address,
protocol = tcp,
family = inet}
end,
mf_tick = fun ?MODULE:tick/1,
mf_getstat = fun ?MODULE:getstat/1,
request_type = Type
},
%进行握手
dist_util:handshake_we_started(HSData);
_ ->
%% Other Node may have closed since
%% port_please !
?trace(
"other node (~p) "
"closed since port_please.~n"
,
[Node]),
?shutdown(Node)
end;
_ ->
?trace(
"port_please (~p) "
"failed.~n"
, [Node]),
?shutdown(Node)
end;
_Other ->
?trace(
"inet_getaddr(~p) "
"failed (~p).~n"
, [Node,_Other]),
?shutdown(Node)
end.
顺便说一句,当独立进程epmd发现自己和某个node的连接断了,那么直接将这个node注册的名字和端口从自身缓存中删除掉。从这里面我们可以看出,Erlang依然是使用inet这模块完成tcp连接,用inet这模块完成数据收发和节点直接的心跳。
让我们看下dist_util:handshake_we_started以及和它相关的函数
handshake_we_started(#hs_data{request_type=ReqType,
other_node=Node}=PreHSData) ->
PreThisFlags = make_this_flags(ReqType, Node),
HSData = PreHSData#hs_data{this_flags=PreThisFlags},
send_name(HSData),
recv_status(HSData),
{PreOtherFlags,ChallengeA} = recv_challenge(HSData),
{ThisFlags,OtherFlags} = adjust_flags(PreThisFlags, PreOtherFlags),
NewHSData = HSData#hs_data{this_flags = ThisFlags,
other_flags = OtherFlags,
other_started =
false
},
check_dflag_xnc(NewHSData),
MyChallenge = gen_challenge(),
{MyCookie,HisCookie} = get_cookies(Node),
send_challenge_reply(NewHSData,MyChallenge,
gen_digest(ChallengeA,HisCookie)),
reset_timer(NewHSData#hs_data.timer),
recv_challenge_ack(NewHSData, MyChallenge, MyCookie),
connection(NewHSData).
%% --------------------------------------------------------------
%% The connection has been established.
%% --------------------------------------------------------------
connection(#hs_data{other_node = Node,
socket = Socket,
f_address = FAddress,
f_setopts_pre_nodeup = FPreNodeup,
f_setopts_post_nodeup = FPostNodeup}= HSData) ->
cancel_timer(HSData#hs_data.timer),
PType = publish_type(HSData#hs_data.other_flags),
case
FPreNodeup(Socket) of
ok ->
do_setnode(HSData), % Succeeds or exits the process.
Address = FAddress(Socket,Node),
mark_nodeup(HSData,Address),
case
FPostNodeup(Socket) of
ok ->
con_loop(HSData#hs_data.kernel_pid,
Node,
Socket,
Address,
HSData#hs_data.this_node,
PType,
#tick{},
HSData#hs_data.mf_tick,
HSData#hs_data.mf_getstat);
_ ->
?shutdown2(Node, connection_setup_failed)
end;
_ ->
?shutdown(Node)
end.
con_loop(Kernel, Node, Socket, TcpAddress,
MyNode, Type, Tick, MFTick, MFGetstat) ->
receive
{tcp_closed, Socket} ->
?shutdown2(Node, connection_closed);
{Kernel, disconnect} ->
?shutdown2(Node, disconnected);
{Kernel, aux_tick} ->
case
MFGetstat(Socket) of
{ok, _, _, PendWrite} ->
send_tick(Socket, PendWrite, MFTick);
_ ->
ignore_it
end,
con_loop(Kernel, Node, Socket, TcpAddress, MyNode, Type,
Tick, MFTick, MFGetstat);
{Kernel, tick} ->
case
send_tick(Socket, Tick, Type,
MFTick, MFGetstat) of
{ok, NewTick} ->
con_loop(Kernel, Node, Socket, TcpAddress,
MyNode, Type, NewTick, MFTick,
MFGetstat);
{error, not_responding} ->
error_msg(
"** Node ~p not responding **~n"
"** Removing (timedout) connection **~n"
,
[Node]),
?shutdown2(Node, net_tick_timeout);
_Other ->
?shutdown2(Node, send_net_tick_failed)
end;
{From, get_status} ->
case
MFGetstat(Socket) of
{ok, Read, Write, _} ->
From ! {self(), get_status, {ok, Read, Write}},
con_loop(Kernel, Node, Socket, TcpAddress,
MyNode,
Type, Tick,
MFTick, MFGetstat);
_ ->
?shutdown2(Node, get_status_failed)
end
end.
在这里面,handshake_we_started和远程节点进行一次验证。验证过程非常简单,远程节点生成一个随机数,然后将这个随机数发给当前节点,然后当前节点用它所知道的远程节点的cookie加上这个随机数生成一个MD5,并将这个MD5返回给远程节点,本端节点对远程节点的验证也是如此。当完成了验证,我们会进入connection这个函数,这是时候,函数首先会执行do_setnode,告诉Erts我们已经和远程的连接上了。同时通知net_kernel我们已经连上了远程,需要它改变ets连接中的状态和进行后续的操作。接着这个进程进入了和远程节点心跳监控的状态。
让我们聊聊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 *pp = NULL;
/* Prepare for success */
ERTS_BIF_PREP_RET(ret, am_true);
/*
* Check and pick out arguments
*/
if
(!is_node_name_atom(BIF_ARG_1) ||
is_not_internal_port(BIF_ARG_2) ||
(erts_this_node->sysname == am_Noname)) {
goto
badarg;
}
if
(!is_tuple(BIF_ARG_3))
goto
badarg;
tp = tuple_val(BIF_ARG_3);
if
(*tp++ != make_arityval(4))
goto
badarg;
if
(!is_small(*tp))
goto
badarg;
flags = unsigned_val(*tp++);
if
(!is_small(*tp) || (version = unsigned_val(*tp)) == 0)
goto
badarg;
ic = *(++tp);
oc = *(++tp);
if
(!is_atom(ic) || !is_atom(oc))
goto
badarg;
/* DFLAG_EXTENDED_REFERENCES is compulsory from R9 and forward */
if
(!(DFLAG_EXTENDED_REFERENCES & flags)) {
erts_dsprintf_buf_t *dsbufp = erts_create_logger_dsbuf();
erts_dsprintf(dsbufp,
"%T"
, BIF_P->common.id);
if
(BIF_P->common.u.alive.reg)
erts_dsprintf(dsbufp,
" (%T)"
, BIF_P->common.u.alive.reg->name);
erts_dsprintf(dsbufp,
" attempted to enable connection to node %T "
"which is not able to handle extended references.\n"
,
BIF_ARG_1);
erts_send_error_to_logger(BIF_P->group_leader, dsbufp);
goto
badarg;
}
/*
* Arguments seem to be in order.
*/
/* get dist_entry */
dep = erts_find_or_insert_dist_entry(BIF_ARG_1);
if
(dep == erts_this_dist_entry)
goto
badarg;
else
if
(!dep)
goto
system_limit;
/* Should never happen!!! */
//通过Port的ID获取Port的结构
pp = erts_id2port_sflgs(BIF_ARG_2,
BIF_P,
ERTS_PROC_LOCK_MAIN,
ERTS_PORT_SFLGS_INVALID_LOOKUP);
erts_smp_de_rwlock(dep);
if
(!pp || (erts_atomic32_read_nob(&pp->state)
& ERTS_PORT_SFLG_EXITING))
goto
badarg;
if
((pp->drv_ptr->flags & ERL_DRV_FLAG_SOFT_BUSY) == 0)
goto
badarg;
//如果当前cid和传入的Port的ID相同,且port的sist_entry和找到的dep相同
//那么直接进入结束阶段
if
(dep->cid == BIF_ARG_2 && pp->dist_entry == dep)
goto
done;
/* Already set */
if
(dep->status & ERTS_DE_SFLG_EXITING) {
/* Suspend on dist entry waiting for the exit to finish */
ErtsProcList *plp = erts_proclist_create(BIF_P);
plp->next = NULL;
erts_suspend(BIF_P, ERTS_PROC_LOCK_MAIN, NULL);
erts_smp_mtx_lock(&dep->qlock);
erts_proclist_store_last(&dep->suspended, plp);
erts_smp_mtx_unlock(&dep->qlock);
goto
yield;
}
ASSERT(!(dep->status & ERTS_DE_SFLG_EXITING));
if
(pp->dist_entry || is_not_nil(dep->cid))
goto
badarg;
erts_atomic32_read_bor_nob(&pp->state, ERTS_PORT_SFLG_DISTRIBUTION);
/*
* Dist-ports do not use the "busy port message queue" functionality, but
* instead use "busy dist entry" functionality.
*/
{
ErlDrvSizeT disable = ERL_DRV_BUSY_MSGQ_DISABLED;
erl_drv_busy_msgq_limits(ERTS_Port2ErlDrvPort(pp), &disable, NULL);
}
//更新Port所关联的dist
pp->dist_entry = dep;
dep->version = version;
dep->creation = 0;
ASSERT(pp->drv_ptr->outputv || pp->drv_ptr->output);
#if 1
dep->send = (pp->drv_ptr->outputv
? dist_port_commandv
: dist_port_command);
#else
dep->send = dist_port_command;
#endif
ASSERT(dep->send);
#ifdef DEBUG
erts_smp_mtx_lock(&dep->qlock);
ASSERT(dep->qsize == 0);
erts_smp_mtx_unlock(&dep->qlock);
#endif
//更新dist_entry的cid
erts_set_dist_entry_connected(dep, BIF_ARG_2, flags);
if
(flags & DFLAG_DIST_HDR_ATOM_CACHE)
create_cache(dep);
erts_smp_de_rwunlock(dep);
dep = NULL;
/* inc of refc transferred to port (dist_entry field) */
//增加远程节点的数量
inc_no_nodes();
//发送监控信息到调用的进程
send_nodes_mon_msgs(BIF_P,
am_nodeup,
BIF_ARG_1,
flags & DFLAG_PUBLISHED ? am_visible : am_hidden,
NIL);
done:
if
(dep && dep != erts_this_dist_entry) {
erts_smp_de_rwunlock(dep);
erts_deref_dist_entry(dep);
}
if
(pp)
erts_port_release(pp);
return
ret;
yield:
ERTS_BIF_PREP_YIELD3(ret, bif_export[BIF_setnode_3], BIF_P,
BIF_ARG_1, BIF_ARG_2, BIF_ARG_3);
goto
done;
badarg:
ERTS_BIF_PREP_ERROR(ret, BIF_P, BADARG);
goto
done;
system_limit:
ERTS_BIF_PREP_ERROR(ret, BIF_P, SYSTEM_LIMIT);
goto
done;
}
setnode_3首先是将,得到的远程节点的名字放入dist的hash表中,并且将这个表项和连接到远程节点的Port进行了关联。
接着将和远程节点进行连接的Port标记为ERTS_PORT_SFLG_DISTRIBUTION,这样在一个Port出现Busy的时候我们能区分出是普通的Port还是远程连接的Port,在一个Port被销毁的时候,是否要调用dist.c中的erts_do_net_exits来告诉Erts远程节点已经掉线了。当这些都顺利的完成了之后,会在这个Erts内部广播nodeup这个消息,那么nodeup的接收者又是谁呢?nodeup的接收者是那些通过process_flag函数设置了monitor_nodes标记的进程,当然monitor_nodes这选项文档中是没有的。如果我们想监听nodeup事件,只能通过net_kernel:monitors函数来完成。
我们上次说到负责连接远程节点的进程会通知net_kernel进程,让我们接着看下net_kernel收到消息做了什么。
handle_info({SetupPid, {nodeup,Node,Address,Type,Immediate}},
State) ->
case
{Immediate, ets:lookup(sys_dist, Node)} of
{
true
, [Conn]} when Conn#connection.state =:= pending,
Conn#connection.owner =:= SetupPid ->
ets:insert(sys_dist, Conn#connection{state = up,
address = Address,
waiting = [],
type = Type}),
SetupPid ! {self(), inserted},
reply_waiting(Node,Conn#connection.waiting,
true
),
{noreply, State};
_ ->
SetupPid ! {self(), bad_request},
{noreply, State}
end;
更新ets中的状态,同时发送消息给所有等待的进程,告诉他们远程连接已经成功了,你们可以继续进行后续操作了。
这个时候你会惊奇的发现,心跳在什么地方呢?不急,我们再回头看下net_kernel的init函数
init({Name, LongOrShortNames, TickT}) ->
process_flag(trap_exit,
true
),
case
init_node(Name, LongOrShortNames) of
{ok, Node, Listeners} ->
process_flag(priority, max),
Ticktime = to_integer(TickT),
Ticker = spawn_link(net_kernel, ticker, [self(), Ticktime]),
{ok, #state{name = Name,
node = Node,
type = LongOrShortNames,
tick = #tick{ticker = Ticker,
time
= Ticktime},
connecttime = connecttime(),
connections =
ets:
new
(sys_dist,[named_table,
protected
,
{keypos, 2}]),
listen = Listeners,
allowed = [],
verbose = 0
}};
Error ->
{stop, Error}
end.
net_kernel首先创建了一个ticker进程,它专门负责发心跳给net_kernel进程,然后net_kernel进程会遍历所有远程连接的进程,让他们进行一次心跳。当我们改变了一个节点的心跳时间的时候,我们会开启一个aux_ticker进程帮助我们进行过度,直到所有节点都知道了我们改变了心跳周期为止,当所有节点都知道我们改变了心跳周期,这个aux_ticker进程也就结束了它的历史性任务,安静的退出了。
那么是如何发现远程节点退出的,当然是TCP数据传输发生了故障Port被清理掉了,这个可参见dist.c中的erts_do_net_exits。
当这些都完成了,我们将继续回到global模块和global_group模块中去分析下nodeup的时候,两个节点是如何同步他们的全局名字的。
- 高手写的erlang的一些内部机制分析
- 实例分析Erlang二进制(Binary)匹配的内部机制
- nodogsplash的内部机制分析
- Erlang二进制创建的内部机制和优化(一)
- Erlang二进制创建的内部机制和优化(二)
- [Erlang 0032] Erlang Binary的内部实现
- memcached的一些内部工作机制
- Erlang数据类型的内部实现
- erlang catch的内部实现
- Erlang数据类型的内部实现
- 新手入门:一些高手写的文章可能会误导你
- Erlang如何查看gen_server内部的状态
- erlang catch的内部实现(初稿)
- [Erlang]ErlangVM的心跳机制
- 关于C++类内部的一些工作机制
- 一个view内部的事件分发机制的分析总结
- Erlang程序的一些例子
- 入门erlang的一些感想。
- 三角形2
- 2-3-2&3 三角形类2及多文件组织
- Python--filter
- 第三周项目3 程序的多文件组织
- 第三周OJ刷题渊子赛马
- 高手写的erlang的一些内部机制分析
- 第三周程序阅读(4) 利用引用访问私有数据成员
- LeetCode之Single Number
- 神经网络入门(连载之六)
- 第三周【项目2-三角形类2】
- 项目一三角形类1
- Shortcuts Plugin in Vim + Cscope
- 第三周 项目2-三角形类2
- 在VC2008下将32位C++内嵌汇编迁移到64位