7

(交叉发布到 lwt github 问题)

我已经将我的用法归结为这个代码示例,它会泄漏文件描述符。

说你有:

#require "lwt.unix"

open Lwt.Infix

let echo ic oc = Lwt_io.(write_chars oc (read_chars ic))

let program =
  let server_address = Unix.(ADDR_INET (inet_addr_loopback, 2000)) in

  let other_addr = Unix.(ADDR_INET (inet_addr_loopback, 2001)) in

  let server = Lwt_io.establish_server server_address begin fun (tcp_ic, tcp_oc) ->
      Lwt_io.with_connection other_addr begin fun (nc_ic, nc_oc) ->

        Lwt_io.printl "Created connection" >>= fun () ->
        echo tcp_ic nc_oc <&> echo nc_ic tcp_oc >>= fun () ->
        Lwt_io.printl "finished"

      end
      |> Lwt.ignore_result

    end
  in
  fst (Lwt.wait ())

let () =
  Lwt_main.run program

然后你创建一个简单的服务器:

nc -l 2001

然后让我们启动 OCaml 代码 utop example.ml

然后打开一个客户端

nc localhost 2000
blah blah
^c

然后使用 lsof 查看端口 2000 的连接,我们看到

ocamlrun 71109 Edgar    6u  IPv4 0x7ff3e309cb80aead      0t0  TCP 127.0.0.1:callbook (LISTEN)
ocamlrun 71109 Edgar    7u  IPv4 0x7ff3e309c9dc8ead      0t0  TCP 127.0.0.1:callbook->127.0.0.1:54872 (CLOSE_WAIT)

事实上,对于 的每次使用nc localhost 2000,我们都会CLOSE_WAIT从 lsof 使用中得到一个剩余的记录。

最终这将导致系统用尽文件描述符,这不会让程序崩溃,但会导致 Lwt 挂起。

我不知道我做错了什么,或者这是一个真正的错误,无论如何这对我来说是一个严重的错误,我在 10 小时内用完了文件描述符......

编辑:在我看来,问题在于连接的一侧已关闭,但另一侧未关闭,我原以为with_connection只要任一侧关闭,也就是无论何时关闭,都应该清理/nc_ic关闭nc_oc

编辑二:我已经尝试了所有手动关闭描述符的方式Lwt_io.close,但我仍然有 CLOSE_WAIT 消息。

编辑三:甚至Lwt_unix.close在给 with_connection 的可选fd参数的原始 fd 上使用类似的坏结果。

编辑四:最阴险的是,如果我使用Lwt_daemon.daemonize,那么这个问题似乎就消失了

4

2 回答 2

5

首先,不清楚为什么使用 join<&>而不是 select <?>。我想如果双方之一想要关闭连接,应该关闭它。

关于:它是从服务器到客户端CLOSE_WAIT的半封闭连接。utopnc

一个 TCP 连接由两个半连接组成,它们是独立关闭的。从nc客户端到utop服务器的连接nc由于Ctrl-C. 但是您必须通过关闭输出流来显式关闭服务器端的相反连接。我不确定为什么Lwt.establish_server不自动关闭它。可能,这是一个设计问题。

这在 CentOS 7 上对我有用:

Lwt_io.printl "Created connection" >>= fun () ->
echo tcp_ic nc_oc <?> echo nc_ic tcp_oc >>= fun () ->
Lwt_io.close tcp_oc >>= fun () ->
Lwt_io.printl "finished"

此外,还有一个简化的代码片段可以重现该问题:

#require "lwt.unix"

let program =
  let server_address = Unix.(ADDR_INET (inet_addr_loopback, 2000)) in

  let _server = Lwt_io.establish_server server_address begin fun (ic, oc) ->
    (* Lwt_io.close oc |> Lwt.ignore_result; *) ()
  end
  in
  fst (Lwt.wait ())

let () =
  Lwt_main.run program

运行nc localhost 2000几次以使连接处于CLOSE_WAIT状态。取消注释代码以解决问题。

于 2016-01-13T07:29:04.393 回答
2

在提出这个问题时,根本问题是根本没有做任何努力来关闭与和Lwt_io.establish_server关联的文件描述符。虽然这可以(并且应该)由用户手动关闭它们来解决,但这是一种奇怪且出乎意料的行为。tcp_ictcp_oc

自 Lwt 3.0.0 起可用的Lwt_io.establish_servertcp_ic的确实会尝试tcp_oc自动关闭。为了允许这一点,它对回调有一个稍微不同的类型签名:回调必须返回一个承诺,当tcp_ic/tcp_oc不再需要时你应该解决它。(编辑)在实践中,这意味着您只需以自然 Lwt 样式编写回调,完成最后一个 Lwt 操作将关闭通道。

新 API 还在内部调用Lwt.async以运行您的回调,因此您不必调用它或Lwt.ignore_result.

您仍然可以在回调中手动关闭tcp_ictcp_oc,以编写您自己的错误处理程序,可以根据您的喜好进行详细说明。新内部的第二个自动内部关闭Lwt_io.establish_server不会产生任何有害影响。

新的 API 是Lwt 问题 #208中对这个问题的并行讨论的最终结果。

如果有人想要旧的、痛苦的行为,也许是为了重现问题中的问题,旧的 API 在名称下可以使用一段时间Lwt_io.Versioned.establish_server_1

于 2017-04-20T00:39:45.710 回答