Port 모듈
Port 모듈
Port는 Erlang VM 바깥의 외부 세계와 상호작용하는 함수들을 제공해요. 포트(port)는 Erlang VM 외부의 운영체제 프로세스를 시작하고 메시지 전달로 통신하는 메커니즘입니다.
출처: Port
본문
예시(Example)
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.1444>, :closed}
:ok
위 예시에서 cat 프로그램을 실행하는 새 포트를 만들었습니다. cat은 Unix 계열 운영체제에서 사용 가능한 프로그램으로, 여러 입력에서 데이터를 받아 출력에 이어 붙입니다.
포트를 만든 뒤 send/2로 메시지 형태의 명령 두 개를 보냈어요. 첫 번째 명령은 "hello"라는 바이너리 페이로드를, 두 번째는 "world"를 담고 있습니다.
그 두 메시지를 보낸 후 IEx 헬퍼 flush()를 호출해서 포트에서 받은 모든 메시지를 출력했는데, 이 경우 "hello"와 "world"를 돌려받았어요. 메시지가 바이너리인 이유는 Port.open/2에서 포트를 열 때 :binary 옵션을 넘겼기 때문입니다. 그 옵션이 없었다면 바이트 리스트가 나왔을 거예요.
모든 작업이 끝나면 포트를 닫았습니다.
Elixir는 포트로 작업하기 위한 많은 편의 기능을 제공하면서도 몇 가지 단점도 있어요. 아래에서 그걸 살펴볼게요.
메시지 API와 함수 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이 아니면 소유자 프로세스가 종료 신호를 트랩(trap)하고 있을 때만 이 메시지를 받습니다
열기 메커니즘(Open mechanisms)
포트는 네 가지 주요 메커니즘으로 열 수 있어요.
간단히 요약하면, 아래 언급할 :spawn과 :spawn_executable 옵션을 선호하세요. 나머지 두 옵션 :spawn_driver와 :fd는 VM 내부의 고급 용도입니다. 프로그램을 실행하고 반환값만 가져오고 싶다면 System.cmd/3도 고려해 보세요.
Windows의 인자 분리와 신뢰할 수 없는 인자
Unix 시스템에서는 인자가 문자열 배열로 새 OS 프로세스에 전달되지만, Windows에서는 자식 프로세스가 파싱할 몫이에요. 일부 Windows 프로그램은 표준 C 런타임 argv 파싱과 일치하지 않는 자기들만의 규칙을 적용할 수 있습니다.
이는 .bat나 .com 파일을 호출할 때 특히 문제가 돼요. 이런 파일은 cmd.exe를 통해 암묵적으로 실행되는데, cmd.exe의 인자 파싱은 악의적인 입력에 취약하고 임의의 셸 명령을 실행하는 데 악용될 수 있기 때문이에요.
따라서 Windows에서 배치 파일이나 .com 애플리케이션을 실행한다면, 프로그램에 신뢰할 수 없는 입력을 인자로 넘기면 안 됩니다. 실행하려는 프로그램의 확장자(예: .exe)를 명시적으로 넘기고 그 프로그램이 실제로 배치 파일이나 .com 애플리케이션이 아닌지 다시 확인하면 실수로 실행하는 것을 피할 수 있어요.
이것은 spawn과 spawn_executable 모두에 영향을 미칩니다.
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_executable은 더 제한적이고 명시적인 버전의 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이 사용하는 in·out 파일 디스크립터에 접근하게 해 줍니다. 이는 :user, :shell 프로세스 같은 런타임 시스템의 핵심 부분을 재구현할 때만 쓰게 될 거예요.
고아가 된 운영체제 프로세스(Orphan OS processes)
포트는 close/1 함수 또는 {pid, :close} 메시지로 닫을 수 있어요. 하지만 VM이 죽으면 포트가 시작한 오래 실행되는 프로그램의 stdin·stdout 채널은 닫히지만 자동으로 종료되지는 않습니다.
일부 Unix 명령줄 도구는 부모 프로세스가 종료되면 종료되지만, 모든 명령줄 애플리케이션이 그런 건 아니에요. 포트를 시작한 다음 VM을 종료하고 운영체제에서 포트 프로세스가 여전히 실행 중인지 확인하면 쉽게 확인할 수 있어요.
우리가 항상 서드파티 소프트웨어가 어떻게 종료되는지 제어할 수는 없어요. 필요하다면 한 가지 해결책으로, stdin이 닫혔는지 확인하는 스크립트로 자식 애플리케이션을 감싸는 방법이 있습니다. bash 셸에서 동작이 검증된 스크립트는 다음과 같아요.
#!/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"]
)
타입(Types)
@type name() ::
{:spawn, charlist() | binary()}
| {:spawn_driver, charlist() | binary()}
| {:spawn_executable, :file.name_all()}
| {:fd, non_neg_integer(), non_neg_integer()}
함수(Functions)
close(port)
@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를 참고하세요. 컴파일러에 의해 인라인 처리됩니다.
connect(port, pid)
@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를 참고하세요. 컴파일러에 의해 인라인 처리됩니다.
info(port)
@spec info(port()) :: keyword() | nil
port에 대한 정보를 반환합니다(포트가 닫혀 있으면 nil). 자세한 내용은 :erlang.port_info/1를 참고하세요.
info(port, spec)
@spec info(port(), atom()) :: {atom(), term()} | nil
port의 특정 필드에 대한 정보를 반환합니다(포트가 닫혀 있으면 nil). 자세한 내용은 :erlang.port_info/2를 참고하세요.
list()
@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를 참고하세요. 컴파일러에 의해 인라인 처리됩니다.
open(name, options)
@spec open(name(), list()) :: port()
튜플 name과 options 리스트가 주어지면 포트를 엽니다. 위 모듈 문서에 지원되는 name 값에 대한 문서와 예시가 아래 요약과 함께 있습니다.
{:spawn, command}— 외부 프로그램 실행.command는 프로그램 이름과 선택적으로 공백으로 구분된 인자 리스트를 포함해야 합니다. 이름에 공백이 있는 프로그램이나 인자를 넘길 때는 다음 옵션을 쓰세요.{:spawn_executable, filename}— 절대 파일 이름filename이 주어진 실행 파일 실행. 인자는:args옵션으로 전달할 수 있어요.{:spawn_driver, command}— 이른바 포트 드라이버(port driver)를 생성{:fd, fd_in, fd_out}— VM이 연 파일 디스크립터fd_in,fd_out에 접근
자세한 내용과 옵션 목록은 :erlang.open_port/2를 참고하세요. 컴파일러에 의해 인라인 처리됩니다.
더 알아보기
System.cmd/3— 프로그램을 실행하고 반환값을 얻는 더 단순한 방법System.find_executable/1—$PATH에서 실행 파일 경로 찾기:erlang.open_port/2— Erlang 포트 열기 함수Node— 분산 노드 관련 모듈