Erlang-China

erlang 中文社区

a pitfall in passing socket via processes


被问到一个奇怪的问题,做了一番探索,觉得还是有必要记录下来,以供来者参考。

如下的一个简单程序,会有什么问题?

下载: r3.erl
  1. -module(r3).
  2. -compile(export_all).
  3.  
  4. start() ->
  5.     spawn(r3, start, ["www.yahoo.com", 80]).
  6.  
  7. start(RHost, RPort) ->
  8.     io:format("start:pid=~p~n", [self()]),
  9.     {ok, Sock} = gen_tcp:connect(RHost, RPort, [list, {packet, 0}, {active, false}, {reuseaddr, true}]),
  10.     io:format("start:socket=~p~n", [erlang:port_info(Sock)]),
  11.     spawn(r3, test, [Sock]).
  12.  
  13. test(Sock) ->
  14.     io:format("test:pid=~p~n", [self()]),
  15.     io:format("socket=~p~n", [erlang:port_info(Sock)]),
  16.     ok = gen_tcp:send(Sock, "GET / HTTP/1.0\r\n\r\n"),
  17.     case gen_tcp:recv(Sock, 0) of
  18.         {ok, Pack} ->
  19.             io:format("recv:ok:~p~n", [Pack]);
  20.         Any ->
  21.             io:format("recv:~p~n", [Any])
  22.     end,
  23.     ok = gen_tcp:close(Sock).

OK,我知道这个简单的例子中,两个 spawn 都是没有必要的,实际上,不加这两个 spawn 一点问题也不会有,但是,加了似乎也没有什么明显的错误,不是么。那么,运行起来是怎样的呢?

1> r3:start().
start:pid=<0.33.0>
<
0.33.0>
start:socket=[{name,"tcp_inet"},
              {
links,[<0.33.0>]},
              {
id,100},
              {
connected,<0.33.0>},
              {
input,0},
              {
output,0}]
test:pid=<0.37.0>
socket=undefined
2>
=
ERROR REPORT==== 27-Nov-2007::22:04:57 ===
Error in process <0.37.0> with exit value: {{badmatch,{error,closed}},[{r3,test,1}]}

是的,报了一个错,莫名奇妙啊,这是为什么呢?

从输出的信息中已经很明显的能看出问题的端倪。还是对照代码,这会儿加了注释:

  1. -module(r3).
  2. -compile(export_all).
  3.  
  4. start() ->
  5.     spawn(r3, start, ["www.yahoo.com", 80]).
  6.  
  7. start(RHost, RPort) ->
  8.     io:format("start:pid=~p~n", [self()]),
  9.     {ok, Sock} = gen_tcp:connect(RHost, RPort, [list, {packet, 0}, {active, false}, {reuseaddr, true}]),
  10.     %% 这里对 socket 的输出挺正常的
  11.     io:format("start:socket=~p~n", [erlang:port_info(Sock)]),
  12.     spawn(r3, test, [Sock]).
  13.  
  14. test(Sock) ->
  15.     io:format("test:pid=~p~n", [self()]),
  16.     %% 刚从 start/2 中 spawn 了 test 但,一进入 socket 的输出就变成 undefined 了,这是为什么?
  17.     io:format("socket=~p~n", [erlang:port_info(Sock)]),
  18.     ok = gen_tcp:send(Sock, "GET / HTTP/1.0\r\n\r\n"),
  19.     case gen_tcp:recv(Sock, 0) of
  20.         {ok, Pack} ->
  21.             io:format("recv:ok:~p~n", [Pack]);
  22.         Any ->
  23.             io:format("recv:~p~n", [Any])
  24.     end,
  25.     ok = gen_tcp:close(Sock).

注意:上面输出 socket 时所使用的方式。

从解决问题的角度,拿掉上面的两个 spawn 之一,都能解决问题。代码立刻运转正常。那么“追根究底”的一问——为什么加了 spawn 之后就变得不正常了?what’s the diffrent?

检视 Erlang 的代码,不难发现 gen_tcp 实际上是用 port 的方式来实现 tcp 的。出问题的例子,是在引入 spawn 之后出现异常情况的。那么,在这个 port 和 pid 之间,是否有着某种联系?

在花了大半天“追根究底”之后,发现原因是这样的:“socket 是通过 port 实现的,如果这个 port 的 owner process 退出,那么这个 port 也会关闭”。即,上述的 socket 是 pid(<0.33.0>) 创建的,它的 owner process (或曰 control process)就是这个 pid(<0.33.0>) ,在 spawn 之后这个 owner process 自己就退出了,此时,同时这个 socket 也会关闭,当新的 process pid(<0.37.0>) 拿到 socket 的时候,就已经是一个关闭了的 socket ,所以,出现问题就不出奇了。

这个 pitfall 在于:在 erlang/gen_tcp 的文档中没有提到这个“owner process/control process”的特性,只是在 port 的文档中提到了一句“The Erlang process which creates a port is said to be the port owner, or the connected process of the port. All communication to and from the port should go via the port owner. If the port owner terminates, so will the port”,但是,如果没有“gen_tcp是通过port来实现的”这个基础知识(我也是啃了半天sourcecode才整明白),又何从知道这个关窍?

经验:并不是不能在 process 之间传递 socket ,而是,在传递 socket 的时候,要考虑 socket 本身的“lifttime”是不是在 control process 的“lifetime”之内。





Comments



1
Author:  mryufeng | Date:  November 28, 2007 | Time:  10:09 am

erlang的这个特性不错的 省却了好多资源管理的麻烦,特别是在driver这方面。

2
Author:  pi1ot | Date:  November 28, 2007 | Time:  11:27 am

怎么看都是无奈之举,erlang基本上全靠port来和os打交道。

3
Author:  mryufeng | Date:  November 28, 2007 | Time:  1:24 pm

从erlang实现上看 这个特性全然不是无奈之举 个人认为是深思熟虑的结果 因为整个方法和实现是非常统一的。

4
Author:  pi1ot | Date:  November 28, 2007 | Time:  2:40 pm

干活基本靠fork,IO基本靠port。
脑子里突然冒出这么两句话,没啥其他意思,纯粹搞笑而已。

5
Author:  jackyz | Date:  November 28, 2007 | Time:  7:33 pm

对于这种“一退都退”的特性在Erlang中很常见。这属于被社区所提倡的“The Erlang Way”,即,所谓的“单一外部世界入口”处理模式。

和外界打交道的地方,也就是可能引入‘side effect’的地方,与其在系统外由外部的机制来保证处理的完整性,还不如仅仅通过一个单一的进程——“control process”来控制,其余进程通过与这个“control process”通讯,来获得对于资源的访问。

这貌似多此一举,实则不然。因为,一方面这“屏蔽”了外界系统对于并发支持的差异,将“并发/并行”的复杂性限制在 Erlang 的框架之内,通过“进程间通讯”的方式解决,从而得以最大限度的“无锁化”。令一方面,也简化了失效处理的策略,一旦失效,直接 kill 掉再重启就行,无须担心多个进程之间共享同一个资源,而导致的复杂锁定情况。

因此,这也可以说是“Erlang无锁化思想在其他问题上的延伸”。

比较 pitfall 的是,竟然没有文档提到 socket 与 creator process 的关系是: socket “属于”它的 control process 。毕竟,这与大家所熟悉的观念还是很不相同的。

6
Author:  Jerry | Date:  December 10, 2007 | Time:  7:30 pm

我觉得jackyz的这个经验很值得我们注意的。

在实际应用中,搞清楚process和其所拥有的socket之间的天生绑定关系,在写代码时就能小心避免错误。
Erlang中有不少这样的情况,比如process和其创建的ets表,也是一种从属关系,control process
销毁后,其ets表也不存在了。

另外,我认为socket还是不应该通过函数的参数来传递,即使其control process在传递完socket后依
旧存在,但是这样做还是不安全的,无法断定control process何时被kill。正确的方法应该是使用
gen_tcp中的controlling_process(Socket,Pid) -> ok|{error,eperm}函数,使得socket更换
其control process,这样就达到使其他process来代替原先的process控制特定socket的目的。从这个
函数也可以看出一点“socket属于它的control process”的端倪来。



Write a Comment

Note: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>