やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

Elixrのプロセスとは一体何なのか。探索の旅に出てた

今更だけど

Node間通信のチャットサーバーを作っている時にふとなぜか、「Elixirでspawnとかした後に返ってくるPIDって一体何なんだ?」と思い立った
なぜsend(target_pid, message)とするだけで対象のプロセスにdataを送ることが可能なのだろか。Node間通信をしている最終に気になってしまったので調べた結果を個人的メモとしてまとめておく

実際にPIDを見てみよう

PIDとは「Process ID」の略語でプロセスを認識するための固有IDなのかなと間違えてしまいそうだが、実際には「Process Identifier」の略語であり、プロセスの識別子、つまりはプロセスを識別するために割り振りされた値という捉え方をすることが出来る

以下のコードをiexを立ち上げて実行することでPIDの存在を確認することが出来る

iex(1)> self()
#PID<0.105.0>

この値がPID。ここに表示されたPIDは現在、立ち上がっているiexに割り振られたPIDになる。すなわち、iexもプロセスとして動いているということになる。次は新規に生成したプロセスのPIDを確認してみる

iex(3)> spawn(fn -> IO.puts("nice") end)
nice
#PID<0.109.0>
iex(4)> self()
#PID<0.105.0>

spawnを使って生成したプロセスのPIDself()で確認したPIDが異なっていることが確認できる。すなわち、新たなる別のプロセスの生成に成功しているということだ。では、実際のこのプロセスが何者なのかを追っていく

PIDを追う旅の始まり

僕がPIDに関して疑問を持ったのはNode.self()の戻り値がPIDだったからだが、先ほどself()PIDが確認出来たので、そこから追っていこう

h spawn/1としてhelpを確認したところ、spawnProcessというモジュールに実装されているそうだ
こちらがspawn/1に該当するコード。defdelegateって何じゃい。しかも、引数が2つある。どういうことだ...

@spec spawn((() -> any), spawn_opts) :: pid | {pid, reference}
defdelegate spawn(fun, opts), to: :erlang, as: :spawn_opt

defdelegateについて

まずはdefdelegateから調べてみよう。ぐぐったらElixirKerneldefdelegateを発見。見た感じ、別のモジュールに用意されている関数を自身で定義しているモジュールの関数として振る舞わせることが可能なようだ

無事にEnumに実装されているrandomの関数に処理が割り当てられているようだ。Erlangの関数も無事に呼び出せるところを見ると、先ほどのspawnに割り当てられている処理はErlangspawn_optをcallしているはずだ

defmodule Sample do
  defdelegate random(list), to: Enum
  defdelegate my_random(list), to: Enum, as: :random
  defdelegate my_sleep(time), to: :timer, as: :sleep
end

Sample.random([1,2,3]) |> IO.puts()
Sample.my_random([1,2,3]) |> IO.puts()
Sample.my_sleep(1000) |> IO.puts()

# 2
# 1
# ok

spawn_opt

次にErlangドキュメントのspawn_optを見に行こう。Erlangのドキュメントの目次のproc_libの項目からspawn_optを発見出来た

  • spawn_opt/2 -> spawn_opt(Fun, SpawnOpts) -> pid()
  • spawn_opt/3 -> spawn_opt(Node, Function, SpawnOpts) -> pid()
  • spawn_opt/4 -> spawn_opt(Module, Function, Args, SpawnOpts) -> pid()
  • spawn_opt/5 -> spawn_opt(Node, Module, Function, Args, SpawnOpts) -> pid()

ドキュメントの文言通り、optionを用いて新たなプロセスを生成するで〜とのこと。ふむふむ、この関数で間違いないだろう

Spawns a new process and initializes it as described in the beginning of this manual page. The process is spawned using the spawn_opt BIFs.

この関数はどれもpid()という値を返している。次はこのpid()を探しに行こう

Erlangのドキュメントを見たところData Typesの項目にPidの項目があるのだが、実際に内部のデータがどうなっているかという記述はなく、詳しいことはProcessを見て下さいとある。しかし、ProcessのページでもPIDの内部に触れている部分はなかった

pid()はいずこに

探しに探し、このpdfファイルの引用からヒントを得て、対象のファイルを見つけることが出来た
Erlang and Elixir for Imperative Programmers

https://github.com/erlang/otp/blob/maint/erts/emulator/beam/erl_term.h#571

現在はErlangディレクトリ構造やファイル内容が更新されていて、パスは変わっている。こちらが現在、erl_term.hが格納されているディレクトリのパスになる

github.com

そこに記述されているのがこのコメント。どうやらbinaryでデータを持たせているようだ

/*
 *  PID layout (internal pids):
 *
 *   |3 3 2 2 2 2 2 2|2 2 2 2 1 1 1 1|1 1 1 1 1 1    |               |
 *   |1 0 9 8 7 6 5 4|3 2 1 0 9 8 7 6|5 4 3 2 1 0 9 8|7 6 5 4 3 2 1 0|
 *   |               |               |               |               |
 *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *   |n n n n n n n n n n n n n n n n n n n n n n n n n n n n|0 0|1 1|
 *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *
 *  n : number
 *
 *  Very old pid layout:
 *
 *   |3 3 2 2 2 2 2 2|2 2 2 2 1 1 1 1|1 1 1 1 1 1    |               |
 *   |1 0 9 8 7 6 5 4|3 2 1 0 9 8 7 6|5 4 3 2 1 0 9 8|7 6 5 4 3 2 1 0|
 *   |               |               |               |               |
 *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *   |s s s|n n n n n n n n n n n n n n n|N N N N N N N N|c c|0 0|1 1|
 *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *
 *  s : serial
 *  n : number
 *  c : creation
 *  N : node number
 *
 */

何だこれ。全くわからん。websocketの実装をRFCで覗いたことがあるが、その時にもpayloadの内訳がこんな感じで定義されていた。電話線でのやり取りで扱われるデータも同様だ。つまりは、最大32bitの固定長のbinaryの枠に何かしらの情報を格納したものをPIDとして扱っているということだろう。この内訳が何なのかを調べていたところ、以下の質問をstackoverflowにて発見した(古い質問なので情報が古い可能性が高い)

stackoverflow.com

この回答から推測するにPID layoutは28bitから32bitのエミュレーターとなっており、そのエミュレーター<A.B.C>という、まさにElixirPIDを確認した時のように(PID<0.109.0>)構成されているということ
確かにPID layoutは最大32bitsの枠を用意している。次にA, B, Cが何を指しているのかを確認してみる。回答を翻訳すると

  • A -> 0はローカルのノード。もしくは任意のリモートノード番号
  • B(15bits) -> プロセス番号の最初の15bit
  • C(16-18bits) -> MAXPROCSに達するたびに増加するシリアル

とのこと。うーん、よく分からない。そういう風にErlangで枠を定義しているってことは理解した。つまりは、電話線やプロトコルのようにbinaryの枠にデータを定義してプロセス同士、もしくはErlang自体がやり取りをしているということだろうか?
この説明を見ただけではしっくりこなかったので流浪していたところ、Erlangの公式ドキュメントにて以下のページを見つけた

erlang.org

ページトップの文言を引用して、翻訳してみると

The external term format is mainly used in the distribution mechanism of Erlang
external term formatは主にErlangの配布機構(おそらくmessage passingのことを指す)にて使用されています

このページでビンゴだろう。それにPIDに該当するであろうNEW_PID_EXTというformatを発見することが出来た

1 N 4 4 4
88 Node ID Serial Creation

それぞれの項目に対する説明を軽く翻訳してみた

  • Node -> ATOM_UTF8_EXT, SMALL_ATOM_UTF8_EXT or ATOM_CACHE_REFを使ってエンコードされた発信元の名前
  • ID -> A 32-bit big endian unsigned integerを用いて表現。15bitsが重要で残りは0になる
  • Serial -> A 32-bit big endian unsigned integerを用いて表現。13bitsが重要で残りは0になる
  • Creation -> A 32-bit endian unsigned integerを用いて表現。同じNodeから送信される識別子は同じ作成を持つ必要がある。この値を用いてcrashしたNodeと新しいNodeを区別することが可能。値0はデバッグに用いるため、使用しないで下さい

だそうです。何となく意味は分かるが、より深い理解をするためにはErlangの深い理解が必要なようだ。ここから先はまた深い知識が必要になりそうなのでこの記事では一旦、ここまでとしておこう。現状の自分ではここでギブアップだ。来年、同じものを見た時に少しでも理解できるようになってはいたい

まとめ

今回の調査で分かった事はElixirPIDErlangで定義されている最大32bitのbinaryであり、この定義されたbinaryを使って電話線やプロトコルのようにやり取りをしているんだろうなーっていうこと

_人人人人人人人人人人人人人_
> PID完全に理解した <
 ̄YYYYYYYYYYYYYYYYYYYY ̄

参考文献