Input and output in the Erlang VM are performed using I/O servers. These are simply Erlang processes that implement a low-level message interface. You never have to deal with this interface directly (which is a good thing, as it is complex). Instead, you use the various Elixir and Erlang I/O libraries and let them do the heavy lifting.
In Elixir you identify an open file or device by the PID of its I/O server. And these PIDs behave just like all other PIDs—you can, for example, send them between nodes.
If you look at the implementation of Elixir’s IO.puts function, you’ll see
def puts(device \\ group_leader(), item) do
erl_dev = map_dev(device)
:io.put_chars erl_dev, [to_iodata(item), ?\n]
(To see the source ...