今更だけど
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
を使って生成したプロセスのPID
とself()
で確認したPID
が異なっていることが確認できる。すなわち、新たなる別のプロセスの生成に成功しているということだ。では、実際のこのプロセスが何者なのかを追っていく
PIDを追う旅の始まり
僕がPID
に関して疑問を持ったのはNode.self()
の戻り値がPID
だったからだが、先ほどself()
でPID
が確認出来たので、そこから追っていこう
h spawn/1
としてhelpを確認したところ、spawn
はProcess
というモジュールに実装されているそうだ
こちらがspawn/1
に該当するコード。defdelegate
って何じゃい。しかも、引数が2つある。どういうことだ...
@spec spawn((() -> any), spawn_opts) :: pid | {pid, reference} defdelegate spawn(fun, opts), to: :erlang, as: :spawn_opt
defdelegateについて
まずはdefdelegate
から調べてみよう。ぐぐったらElixir
のKernel
にdefdelegate
を発見。見た感じ、別のモジュールに用意されている関数を自身で定義しているモジュールの関数として振る舞わせることが可能なようだ
無事にEnum
に実装されているrandom
の関数に処理が割り当てられているようだ。Erlang
の関数も無事に呼び出せるところを見ると、先ほどのspawn
に割り当てられている処理はErlang
のspawn_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
が格納されているディレクトリのパスになる
そこに記述されているのがこのコメント。どうやら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にて発見した(古い質問なので情報が古い可能性が高い)
この回答から推測するにPID layout
は28bitから32bitのエミュレーターとなっており、そのエミュレーターが<A.B.C>
という、まさにElixir
でPID
を確認した時のように(PID<0.109.0>
)構成されているということ
確かにPID layout
は最大32bitsの枠を用意している。次にA, B, Cが何を指しているのかを確認してみる。回答を翻訳すると
- A -> 0はローカルのノード。もしくは任意のリモートノード番号
- B(15bits) -> プロセス番号の最初の15bit
- C(16-18bits) -> MAXPROCSに達するたびに増加するシリアル
とのこと。うーん、よく分からない。そういう風にErlang
で枠を定義しているってことは理解した。つまりは、電話線やプロトコルのようにbinary
の枠にデータを定義してプロセス同士、もしくはErlang
自体がやり取りをしているということだろうか?
この説明を見ただけではしっくりこなかったので流浪していたところ、Erlang
の公式ドキュメントにて以下のページを見つけた
ページトップの文言を引用して、翻訳してみると
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
の深い理解が必要なようだ。ここから先はまた深い知識が必要になりそうなのでこの記事では一旦、ここまでとしておこう。現状の自分ではここでギブアップだ。来年、同じものを見た時に少しでも理解できるようになってはいたい
まとめ
今回の調査で分かった事はElixir
のPID
はErlang
で定義されている最大32bitのbinary
であり、この定義されたbinary
を使って電話線やプロトコルのようにやり取りをしているんだろうなーっていうこと
_人人人人人人人人人人人人人_
> PID
完全に理解した <
 ̄YYYYYYYYYYYYYYYYYYYY ̄