高手写的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这个函数。从而我得到下面的结论:

  1. Erlang的Port并不是一直在调度器的队列中的。

  2. Erlang的Port在被Erlang进程通过erlang:controll和erlang:command函数发送命令时,才会被放入RunQueue。

  3. Erlang的Port如果向erts中注册了IO任务,则在scheduler进行erl_check_io的时候才会被放入RunQueue。

  4. Erlang的Port和Erlang进程类似,可以进行link和monitor,从而达到link的进程退出Port也随之退出。

  5. Erlang的Port总有一个connected的Erlang进程,这个Erlang进程是Port默认发送消息的进程。

至此我们可以看出Erlang虚拟机的基本理念如下:

  1. Erts是以scheduler为核心的,一个全力执行任务队列中任务的机器。

  2. Erts不将复杂的IO处理当作整个虚拟机的一部分,而是将IO事件分配当作整个虚拟机的一部分。

  3. Erts将任务分为两类,一类是OPCode执行(Erlang的进程),一类是IO任务的执行(Erlang的Port)。

Erlang运行时环境选择这样做,我认为有以下几个原因:

  1. 复杂的IO处理如果作为Erts的一部分,会引入大量的代码,同时对各个平台的兼容性很难保证。所以将IO分配作为Erts的一部分且将复杂IO的操作抽象为driver(Erlang的Port),这样会大大减少代码量和提高对平台的兼容性。

  2. 这样设计符合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机制,我个人认为有以下几个原因:

  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的代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
 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的部分代码

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
    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很多宏,那么这个宏做了什么呢?这宏非常简单

?
1
2
3
4
5
6
7
8
9
#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模块

发表于1周前(2015-03-17 11:56)   阅读(32) | 评论(0) 0人收藏此文章, 我要收藏
2

如何快速提高你的薪资?-实力拍“跳槽吧兄弟”梦想活动即将开启

摘要 Erlang的global模块
Erlang全局名字 global

昨日有位同仁,问我关于Erlang中global模块的一些事情,当时给这位同仁讲述的不是特别清楚。在晚上的时候,对global模块做了简单的代码分析。

先说下global这模块是干什么的:

  1. 管理全局名字注册

  2. 管理全局锁

  3. 维护全连接网络

该模块是在Erlang节点启动的时候自动被启动的,并且会组册一个名为global_name_server的进程。Erlang本身是一个分布式的语言,这种管理全局的名字组册,全局锁这种任务难道不应该在虚拟机层面做吗?正如上篇Blog所说的,Erts本身是全力进行任务调度和任务远行的,同时Erts对分布式知道的非常有限,只知道本地进程,远程进程,本地Port和远程Port等。好我们回到正题Erlang的global模块,至于Erlang是如何构建出cluster的,我会在后面的Blog再进行分析。

global模块本身是一个gen_server进程,启动之后会再创建出两个Erlang进程,一个负责处理锁,一个负责处理名字。

我们分析下全局名字注册的代码

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
register_name(Name, Pid) when is_pid(Pid) ->
    register_name(Name, Pid, fun random_exit_name/3).
     
register_name(Name, Pid, Method0) when is_pid(Pid) ->
    Method = allow_tuple_fun(Method0),
    Fun = fun(Nodes) ->
        case (where(Name) =:= undefined) andalso check_dupname(Name, Pid) of
            true ->
                gen_server:multi_call(Nodes,
                                      global_name_server,
                                      {register, Name, Pid, Method}),
                yes;
            _ ->
                no
        end
    end,
    ?trace({register_name, self(), Name, Pid, Method}),
    gen_server:call(global_name_server, {registrar, Fun}, infinity).

从这段代码我们可以看出,注册全局名字,必须使用Erlang进程的Pid。同时该函数将注册任务交给global的gen_server去执行了。我们忽略掉多余的部分,直接看下全局名字注册是怎么做的

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
trans_all_known(Fun) ->
    Id = {?GLOBAL_RID, self()},
    Nodes = set_lock_known(Id, 0),
    try
%当锁住了所有的节点,才执行相关的操作
%全局的大锁呀,用多了性能还是比较差的
        Fun(Nodes)
    after
        delete_global_lock(Id, Nodes)
    end.
 
set_lock_known(Id, Times) -> 
    Known = get_known(),
    Nodes = [node() | Known],
%Boss是List中最后的那个元素
    Boss = the_boss(Nodes),
    %% Use the  same convention (a boss) as lock_nodes_safely. Optimization.
%先锁定住Boss
    case set_lock_on_nodes(Id, [Boss]) of
        true ->
%接这锁住剩下的节点
            case lock_on_known_nodes(Id, Known, Nodes) of
                true ->
                    Nodes;
                false -> 
                    del_lock(Id, [Boss]),
                    random_sleep(Times),
                    set_lock_known(Id, Times+1)
            end;
        false ->
            random_sleep(Times),
            set_lock_known(Id, Times+1)
    end.
 
lock_on_known_nodes(Id, Known, Nodes) ->
    case set_lock_on_nodes(Id, Nodes) of
        true ->
            (get_known() -- Known) =:= [];
        false ->
            false
    end.
 
set_lock_on_nodes(_Id, []) ->
    true;
set_lock_on_nodes(Id, Nodes) ->
    case local_lock_check(Id, Nodes) of 
        true ->
            Msg = {set_lock, Id},
            {Replies, _} = 
                gen_server:multi_call(Nodes, global_name_server, Msg),
            ?trace({set_lock,{me,self()},Id,{nodes,Nodes},{replies,Replies}}),
            check_replies(Replies, Id, Replies);
        false=Reply ->
            Reply
    end.

我们可以看出执行流程是这样的,先锁住集群中排序最大的那个节点,如上锁成功,则让所有的其余节点跟着上锁,如果上锁失败,则随机睡眠一段时间再接着尝试。如果当所有节点上都拿到锁,就执行名字注册,并且执行注册后,释放所有节点的锁。为什么要这么做呢?首先全局的锁(原子global)是所有节点共享的,如果从随机的一个节点开始上锁,很容易出现同时好几个节点都在上锁而发生锁冲突,那么大家就约定先上锁某一个节点,这样能快速的发现锁的冲突。其次,不能只锁定一个约定的节点,考虑到不稳定性,当节点出现异常无法连同的时候,那么这个锁的机制就无效了。

这篇Blog先写到这里面,后面我们会聊聊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的代码。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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这个函数做了什么。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
%连接新的节点
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函数

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
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以及和它相关的函数

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
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得到连接成功之后又进行了什么操作。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
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收到消息做了什么。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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函数

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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的时候,两个节点是如何同步他们的全局名字的。


0 0
原创粉丝点击