轉載請註明,來自: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發送數據會出現closed或ENOTCONN錯誤;同樣如果沒設置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都這麼有加有減的話,關閉完所有port後erts_halt_progress值還爲1,接着將erts_halt_progress減1,若結果爲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,則直接退出節點。