高手寫的erlang的一些內部機制分析

轉自 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模塊的一些事情,當時給這位同仁講述的不是特別清楚。在晚上的時候,對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的時候,兩個節點是如何同步他們的全局名字的。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章