關於erlang socket被動模式和delay_send合用的問題

轉載請註明,來自:http://blog.csdn.net/skyman_2001

有項目反應服務器遇到ports()裏的port,其port_info/1返回是undefined的問題,而且出現這個後,erlang:halt()不能正常關閉節點,要用erlang:halt(Status, [{flush, false}])才能關閉節點。在很多客戶端同時關閉時比較容易重現。我在網上erlang論壇上也發現有人遇到這個問題求助,所以決定研究一下。

通過對比分析ERTS的ports_0()(對應erlang:ports/0)和port_info_1()(對應erlang:port_info/1)的源代碼發現,如果一個port的狀態是ERTS_PORT_SFLG_CLOSING,則該port會包含在erlang:ports/0返回的列表中,但erlang:port_info/1返回undefined。ERTS_PORT_SFLG_CLOSING是port的中間狀態,wqtn22在http://wqtn22.iteye.com/blog/1765741一文中對此有詳細的分析。


ERTS源代碼(erts/emulator/beam/io.c)中關於它的註釋說明如下:

**ERTS_PORT_SFLG_CLOSING is a state where the port is in Limbo, waiting to

** passon. All links are removed, and the port receives in/out-put events so

** assoon as the port queue gets empty terminate_port() is called.


先講下delay_send,如果打開了delay_send,每個port會維護一個發送隊列,數據不是立即發送,而是存到發送隊列裏,等socket可寫的時候再發送,相當於是ERTS自己實現的組包機制。關於Erlang Tcp Send的深度解刨,請看霸爺的這篇文章:http://blog.yufeng.info/archives/336

 

那麼爲什麼port會停留在ERTS_PORT_SFLG_CLOSING狀態呢?通過分析和調試ERTS代碼,得出了產生這個問題的過程:

1. Socket客戶端關閉時,會調用到tcp_recv_closed()函數(見erts/emulator/drivers/common/inet_drv.c),裏面:

 

if (!desc->inet.active) {

        /* We must cancel any timer here ! */

        driver_cancel_timer(desc->inet.port);

        /* passive mode do not terminate port ! */

        tcp_clear_input(desc);

        if (desc->inet.exitf) {

            tcp_clear_output(desc);

            desc_close(INETP(desc));

        } else {

            desc_close_read(INETP(desc));

        }

        async_error_am_all(INETP(desc), am_closed);

        /* next time EXBADSEQ will be delivered */

        DEBUGF(("tcp_recv_closed(%ld): passivereply all 'closed'\r\n", port));

        printf("tcp_recv_closed(%ld): passivereply all 'closed', state=%d\r\n", (long) desc->inet.port,desc->inet.state);

    }

 

我們用的是{active,false},即被動模式。會調用tcp_clear_output()清空發送隊列裏的數據,最後會向調用進程發送'closed'消息(async_error_am_all(INETP(desc), am_closed)),但是被動模式下是不會terminate port的,所以這時該socket的狀態還是INET_STATE_CONNECTED(在宿主進程退出時terminate port,纔將該socket狀態設爲INET_STATE_CLOSED)。

 

2.  這之後如果向這個port發送數據,看相關代碼(inet_drv.c):

static void tcp_inet_commandv(ErlDrvData e,ErlIOVec* ev)

{

    tcp_descriptor* desc =(tcp_descriptor*)e;

    desc->inet.caller =driver_caller(desc->inet.port);

 

   DEBUGF(("tcp_inet_commanv(%ld) {s=%d\r\n",

           (long)desc->inet.port, desc->inet.s));

    if (!IS_CONNECTED(INETP(desc))) {

        if(desc->tcp_add_flags & TCP_ADDF_DELAYED_CLOSE_SEND) {

           desc->tcp_add_flags &= ~TCP_ADDF_DELAYED_CLOSE_SEND;

           printf("tcp_inet_commandv(%ld) closed\r\n",(long)desc->inet.port);

           inet_reply_error_am(INETP(desc), am_closed);

        }

        else

           inet_reply_error(INETP(desc), ENOTCONN);

    }

    else if (tcp_sendv(desc, ev) == 0)

       inet_reply_ok(INETP(desc));

   DEBUGF(("tcp_inet_commandv(%ld) }\r\n", (long)desc->inet.port));

}

 

上面說了,此時socket的狀態是INET_STATE_CONNECTED,所以會執行tcp_sendv(),再跟到tcp_sendv()裏面去看

static int tcp_sendv(tcp_descriptor* desc, ErlIOVec* ev)

{

   ...

 

    if ((sz = driver_sizeq(ix)) > 0) {

        driver_enqv(ix, ev, 0);

        if (sz+ev->size >= desc->high) {

            DEBUGF(("tcp_sendv(%ld):s=%d, sender forced busy\r\n",

                   (long)desc->inet.port, desc->inet.s));

 

            desc->inet.state |=INET_F_BUSY;  /* mark for low-watermark */

            desc->inet.busy_caller = desc->inet.caller;

           set_busy_port(desc->inet.port, 1);

            if (desc->send_timeout !=INET_INFINITY) {

               desc->busy_on_send = 1;

               driver_set_timer(desc->inet.port, desc->send_timeout);

            }

            return 1;

        }

    }

    else {

        int vsize = (ev->vsize > MAX_VSIZE) ?MAX_VSIZE : ev->vsize;

 

        DEBUGF(("tcp_sendv(%ld): s=%d, about tosend "LLU","LLU" bytes\r\n",

               (long)desc->inet.port, desc->inet.s, (llu_t)h_len, (llu_t)len));

        

        if (INETP(desc)->is_ignored) {

            INETP(desc)->is_ignored |=INET_IGNORE_WRITE;

            n = 0;

        } else if(desc->tcp_add_flags & TCP_ADDF_DELAY_SEND) {

            n = 0;

        } else if (IS_SOCKET_ERROR(sock_sendv(desc->inet.s,ev->iov,

                                            vsize, &n, 0))) {

            if ((sock_errno() !=ERRNO_BLOCK) && (sock_errno() != EINTR)) {

                int err =sock_errno();

               DEBUGF(("tcp_sendv(%ld): s=%d, "

                       "sock_sendv(size=2) errno = %d\r\n",

                       (long)desc->inet.port, desc->inet.s, err));

 

                return tcp_send_error(desc, err);

            }

#ifdef __WIN32__

            desc->inet.send_would_block =1;

#endif

            n = 0;

        }

        else if (n == ev->size) {

           ASSERT(NO_SUBSCRIBERS(&INETP(desc)->empty_out_q_subs));

            return 0;

        }

        else {

            DEBUGF(("tcp_sendv(%ld):s=%d, only sent "

                   LLU"/%d of "LLU"/%d bytes/items\r\n",

                   (long)desc->inet.port, desc->inet.s,

                   (llu_t)n, vsize, (llu_t)ev->size, ev->vsize));

        }

 

        DEBUGF(("tcp_sendv(%ld): s=%d, Sendfailed, queuing\r\n",

               (long)desc->inet.port, desc->inet.s));

       

        driver_enqv(ix, ev, n);

        if (!INETP(desc)->is_ignored)

           sock_select(INETP(desc),(FD_WRITE|FD_CLOSE), 1);

    }

    return 0;

}

 

因爲sz = driver_sizeq(ix) == 0,所以進入到下面的分支;因爲設了delay_send,所以跳到紅色的那行分支,也就是不會調用sock_sendv()。到最後執行 driver_enqv(ix, ev, n),就向發送隊列添加數據了。

 

3. 在該socket的宿主進程退出時,會依次調用terminate_proc()-> erts_do_exit_process() -> continue_exit_process()-> erts_sweep_links() -> doit_exit_link()-> erts_do_exit_port(),我們看下erts_do_exit_port()函數裏面相關代碼:

...

 if((reason != am_kill) && !is_port_ioq_empty(p)) {

      erts_port_status_bandor_set(p,

                                 ~ERTS_PORT_SFLG_EXITING, /* must turn it off*/

                                 ERTS_PORT_SFLG_CLOSING);

     flush_port(p);

   }

  else {

      terminate_port(p);

   }

上面因爲發送隊列裏有數據,不爲空,所以將port的狀態設爲 ERTS_PORT_SFLG_CLOSING,問題就這麼產生了!

 

總結:在被動模式下,若Socket對端關閉,本端不會terminate port,如果在這之後還向該Socket發送數據,則因爲設置的是delay_send,所以不會調用sock_sendv()真正發送數據,而是存到發送隊列裏。最後在該Socket宿主進程退出時,會terminate port,但發現發送隊列不爲空,所以就將該port設爲ERTS_PORT_SFLG_CLOSING

這個問題只有在被動模式和delay_send合用的時候纔會出現,因爲如果是非被動模式(active true或once),則對端關閉時,本端會terminate port,後面向該Socket送數據會出現closedENOTCONN錯誤;同樣如果沒設置delay_send,則向該socket發送數據時會調用sock_sendv()直接發送數據,因爲對端已經關閉,會觸發tcp_send_error(),裏面會調用erl_inet_close()關閉,這樣該Socket狀態會變爲INET_STATE_CLOSED

 

因此,要想避免這個問題,不要在對端關閉後再向該Socket發送數據;或者關掉delay_send;或者用非被動模式,比如once

 

最後的問題,節點內如果有port狀態爲ERTS_PORT_SFLG_CLOSING,爲什麼erlang:halt/0不能關閉節點呢?我們跟蹤一下halt的流程:

Erlang:halt/0對應的bif是halt_0(),代碼見erts/emulator/beam/bif.c:

/* stop the system */

/* ARGSUSED */

BIF_RETTYPE halt_0(BIF_ALIST_0)

{

   VERBOSE(DEBUG_SYSTEM,("System halted by BIF halt()\n"));

   erl_halt(0);

   ERTS_BIF_YIELD1(bif_export[BIF_halt_1], BIF_P, am_undefined);

}

裏面會調用erl_halt(),然後會關閉節點的ports,處理函數是handle_reap_ports(),裏面有個引用計數:erts_halt_progress,初始值設爲1,然後每個port在關閉前加1,關閉(erts_do_exit_port)後減1。如果每個port都這麼有加有減的話,關閉完所有porterts_halt_progress值還爲1,接着將erts_halt_progress1,若結果爲0的話,就退出節點:

if (erts_smp_atomic32_dec_read_nob(&erts_halt_progress) == 0) {

           erl_exit_flush_async(erts_halt_code, "");

        }

erl_exit_flush_async()函數就是執行節點最終退出流程的。

但是如果有port狀態是ERTS_PORT_SFLG_CLOSING,那麼就不會調用erts_do_exit_port()

          ...

          if (prt->status &(ERTS_PORT_SFLG_EXITING

                              | ERTS_PORT_SFLG_CLOSING)) {

               erts_port_release(prt);

                continue; //這裏continue

            }

   

            erts_do_exit_port(prt,prt->id, am_killed);

            erts_port_release(prt);

           ...

 

這樣引用計數erts_halt_progress就加了沒減,這樣所有port處理完後erts_halt_progress就不爲1,也就不會調用 erl_exit_flush_async()退出了。


那麼爲什麼erlang:halt(Status,[{flush, false}])可以關閉節點呢?我們看它對應的bif代碼(erts/emulator/beam/bif.c):

BIF_RETTYPEhalt_2(BIF_ALIST_2)

{

    …

 

    if (is_small(BIF_ARG_1) && (code =signed_val(BIF_ARG_1)) >= 0) {

        VERBOSE(DEBUG_SYSTEM,

                ("System halted by BIFhalt(%T, %T)\n", BIF_ARG_1, BIF_ARG_2));

        if (flush) {

            erl_halt((int)(- code));

           ERTS_BIF_YIELD1(bif_export[BIF_halt_1], BIF_P, am_undefined);

        }

        else {

            erts_smp_proc_unlock(BIF_P,ERTS_PROC_LOCK_MAIN);

            erl_exit((int)(-code), "");

        }

}

 

}

如果flush爲false,則直接退出節點。


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