查看源代码 Port (Elixir v1.16.2)

用于通过端口与外部世界交互的函数。

端口提供了一种机制来启动 Erlang VM 外部的操作系统进程,并通过消息传递与它们进行通信。

示例

iex> port = Port.open({:spawn, "cat"}, [:binary])
iex> send(port, {self(), {:command, "hello"}})
iex> send(port, {self(), {:command, "world"}})
iex> flush()
{#Port<0.1444>, {:data, "hello"}}
{#Port<0.1444>, {:data, "world"}}
iex> send(port, {self(), :close})
:ok
iex> flush()
{#Port<0.1464>, :closed}
:ok

在上面的示例中,我们创建了一个新的端口,该端口执行程序 catcat 是类 Unix 操作系统上可用的一个程序,它从多个输入接收数据并将它们连接到输出。

端口创建后,我们使用 send/2 以消息形式向它发送了两个命令。第一个命令的二进制有效负载为 "hello",第二个命令的二进制有效负载为 "world"。

发送这两个消息后,我们调用了 IEx 帮助程序 flush(),它打印了从端口接收到的所有消息,在本例中,我们得到了 "hello" 和 "world"。请注意,消息是二进制的,因为我们在 Port.open/2 中打开端口时传递了 :binary 选项。如果没有此选项,它将生成一个字节列表。

完成所有操作后,我们关闭了端口。

Elixir 提供了许多用于处理端口的便利功能,但也有一些缺点。我们将在下面探讨这些内容。

消息和函数 API

有两种 API 用于处理端口。它可以是异步的,通过消息传递,就像上面的示例一样,也可以通过调用此模块上的函数来实现。

端口支持的消息及其对应的函数 API 列在下面

  • {pid, {:command, binary}} - 将给定数据发送到端口。请参阅 command/3.

  • {pid, :close} - 关闭端口。除非端口已关闭,否则端口将在刷新其缓冲区并有效关闭后回复 {port, :closed} 消息。请参阅 close/1.

  • {pid, {:connect, new_pid}} - 将 new_pid 设置为端口的新所有者。打开端口后,端口将与调用进程链接并连接,与端口的通信仅通过连接的进程进行。此消息使 new_pid 成为新的连接进程。除非端口已失效,否则端口将回复旧所有者 {port, :connected}。请参阅 connect/2.

反过来,端口将向连接的进程发送以下消息

  • {port, {:data, data}} - 端口发送的数据
  • {port, :closed} - 对 {pid, :close} 消息的回复
  • {port, :connected} - 对 {pid, {:connect, new_pid}} 消息的回复
  • {:EXIT, port, reason} - 端口崩溃时的退出信号。如果 reason 不是 :normal,则只有在所有者进程正在捕获退出时才会收到此消息

打开机制

端口可以通过四种主要机制打开。

简而言之,优先使用下面提到的 :spawn:spawn_executable 选项。另外两种选项 :spawn_driver:fd 适用于 VM 内部的更高级用法。如果只是想执行一个程序并检索其返回值,还可以考虑使用 System.cmd/3.

spawn

:spawn 元组接收一个将作为完整调用执行的二进制文件。例如,我们可以使用它直接调用 "echo hello"

iex> port = Port.open({:spawn, "echo hello"}, [:binary])
iex> flush()
{#Port<0.1444>, {:data, "hello\n"}}

:spawn 将从参数中检索程序名称,并遍历您的操作系统 $PATH 环境变量以查找匹配的程序。

虽然以上方法很方便,但它意味着无法调用其名称或任何参数中包含空格的可执行文件。出于这些原因,大多数情况下最好执行 :spawn_executable

spawn_executable

Spawn 可执行文件是 spawn 的一个更受限且更明确的版本。它期望您要执行的可执行文件的完整文件路径。如果它们在您的 $PATH 中,可以通过调用 System.find_executable/1 来检索它们。

iex> path = System.find_executable("echo")
iex> port = Port.open({:spawn_executable, path}, [:binary, args: ["hello world"]])
iex> flush()
{#Port<0.1380>, {:data, "hello world\n"}}

使用 :spawn_executable 时,可以通过 :args 选项传递参数列表,如上所述。有关选项的完整列表,请参阅 Erlang 函数 :erlang.open_port/2 的文档。

fd

:fd 名称选项允许开发人员访问 Erlang VM 使用的 inout 文件描述符。只有在重新实现运行时系统核心部分(如 :user:shell 进程)时才会使用这些描述符。

僵尸操作系统进程

端口可以通过 close/1 函数或发送 {pid, :close} 消息来关闭。但是,如果 VM 崩溃,端口启动的长时间运行的程序将关闭其 stdin 和 stdout 通道,但**不会自动终止**。

虽然大多数 Unix 命令行工具在关闭其通信通道后会退出,但并非所有命令行应用程序都会这样做。您可以通过启动端口,然后关闭 VM 并检查您的操作系统来轻松验证这一点,以查看端口进程是否仍在运行。

虽然我们鼓励通过检测 stdin/stdout 是否已关闭来优雅地终止,但我们并不总是能够控制第三方软件如何终止。在这些情况下,您可以将应用程序包装在一个检查 stdin 的脚本中。以下是一个脚本,已验证在 bash shell 上有效

#!/usr/bin/env bash

# Start the program in the background
exec "$@" &
pid1=$!

# Silence warnings from here on
exec >/dev/null 2>&1

# Read from stdin in the background and
# kill running program when stdin closes
exec 0<&0 $(
  while read; do :; done
  kill -KILL $pid1
) &
pid2=$!

# Clean up
wait $pid1
ret=$?
kill -KILL $pid2
exit $ret

请注意,上面的程序劫持了 stdin,因此您将无法通过 stdin 与底层软件进行通信(从好的方面来说,从 stdin 读取的软件通常在 stdin 关闭时终止)。

现在,不是

Port.open(
  {:spawn_executable, "/path/to/program"},
  args: ["a", "b", "c"]
)

您可以调用

Port.open(
  {:spawn_executable, "/path/to/wrapper"},
  args: ["/path/to/program", "a", "b", "c"]
)

摘要

函数

关闭 port

data 发送到端口驱动程序 port

port 标识符与 pid 关联。

取消监视由给定 reference 标识的监视器。

返回有关 port 的信息(如果端口已关闭,则返回 nil)。

返回有关 port 中特定字段的信息(如果端口已关闭,则返回 nil)。

返回当前节点中所有端口的列表。

从调用进程开始监视给定的 port

使用元组 nameoptions 列表打开端口。

类型

@type name() ::
  {:spawn, charlist() | binary()}
  | {:spawn_driver, charlist() | binary()}
  | {:spawn_executable, :file.name_all()}
  | {:fd, non_neg_integer(), non_neg_integer()}

函数

@spec close(port()) :: true

关闭 port

有关更多信息,请参阅 :erlang.port_close/1.

由编译器内联。

链接到此函数

command(port, data, options \\ [])

查看源代码
@spec command(port(), iodata(), [:force | :nosuspend]) :: boolean()

data 发送到端口驱动程序 port

有关更多信息,请参阅 :erlang.port_command/3.

由编译器内联。

@spec connect(port(), pid()) :: true

port 标识符与 pid 关联。

有关更多信息,请参阅 :erlang.port_connect/2.

由编译器内联。

链接到此函数

demonitor(monitor_ref, options \\ [])

查看源代码 (自 1.6.0 起)
@spec demonitor(reference(), options :: [:flush | :info]) :: boolean()

取消监视由给定 reference 标识的监视器。

如果 monitor_ref 是调用进程通过调用 monitor/1 获得的引用,则该监视将被关闭。如果监视器已关闭,则不会发生任何事情。

有关更多信息,请参阅 :erlang.demonitor/2.

由编译器内联。

@spec info(port()) :: keyword() | nil

返回有关 port 的信息(如果端口已关闭,则返回 nil)。

有关更多信息,请参阅 :erlang.port_info/1.

@spec info(port(), atom()) :: {atom(), term()} | nil

返回有关 port 中特定字段的信息(如果端口已关闭,则返回 nil)。

有关更多信息,请参阅 :erlang.port_info/2.

@spec list() :: [port()]

返回当前节点中所有端口的列表。

由编译器内联。

链接到此函数

monitor(port)

查看源代码 (自 1.6.0 起)
@spec monitor(port() | {name, node()} | name) :: reference() when name: atom()

从调用进程开始监视给定的 port

监视的端口进程一旦死亡,一条消息将以以下形式传递给监视进程

{:DOWN, ref, :port, object, reason}

其中

  • ref 是此函数返回的监视器引用;
  • object 是正在监视的 port(按端口 ID 监视时)或 {name, node}(按端口名称监视时);
  • reason 是退出原因。

有关更多信息,请参阅 :erlang.monitor/2.

由编译器内联。

@spec open(name(), list()) :: port()

使用元组 nameoptions 列表打开端口。

上面的模块文档包含对支持的 name 值的文档和示例,总结如下

  • {:spawn, command} - 运行外部程序。 command 必须包含程序名称,并可以选择包含以空格分隔的参数列表。如果传递的程序或参数名称中包含空格,请使用下一个选项。
  • {:spawn_executable, filename} - 运行由绝对文件名 filename 指定的可执行文件。可以通过 :args 选项传递参数。
  • {:spawn_driver, command} - 生成所谓的端口驱动程序。
  • {:fd, fd_in, fd_out} - 访问 VM 打开的文件描述符 fd_infd_out

有关更多信息和选项列表,请参阅 :erlang.open_port/2.

由编译器内联。