process on erlang

vi on emacs

前回、C語上で ブラケット {} 間を飛び回る方法として、vi を模倣したviper-mode を試してみた。こういう要求は絶対に有るはずなんで、きっとemacsだけで実現出来るのだろう。オイラーの検索力のせいで、直ぐに見つけられなかったのよ。

この際、emacs上でvi機能を実現するっていう変態指向にすこし首を突っ込んでみる。 いえね、オイラーはviもemacsも普通に使えるけど、やっぱりlispと親和性が高いemacs派でしたよ。出張先にemacsなんて入ったマシンが置いてある保障なんて無い。

だから、ポータブルemacsしてたのさ。昔の事だからフロッピー1枚に収まるようにカスタマイズしたemacsを自作し、出張の折にはそれを持参してたんだ。こう書くとソフト屋さんと思われるかも知れないけど、職業はハード屋さん、対象の機械はunixで動いていたから、ハードの診断ソフトを動かすにもunixの知識が必要。だって、オシロスコープで波形を観測しやすいように、ソフトを修正するなんで、日常茶飯事でしたからね。

所が、ある時emacsの入ったフロッピーを忘れたまま出張に出てしまった。標準のviしかない。編集もおぼつかない。右往左往しましたね。で、結論は、いきがってemacsなんて使うな。どこにもあるviを使いこなせるようになっておけ。emacs断ちを数年続けたら、身についた。

リタイアしてからは、向かう先は自分のパソコンだけ。だったらemacsの封印を解いてもいいんじゃない。

Evil: EmacsをVimのごとく使う - 導入編

こんな楽しいアーティクルを見つけた。設計思想が色々で面白い。

vimに追いつけが多分に開発動機になってるみたいだけど、オイラーは、(n)viは好きですけど、vimは大嫌い。基本ツールは、どんな状況でもちゃんと働いてくれる事を望む。だったら、edでも使っとけ。

最近edやってないから、たまには非常訓練しとくかな。災害は忘れた頃にやってくる。

ああ、そうそう、 viper-modeで動いている時、C-z で、vi/emacsを簡単に行き来出来るそう(但しcurrent bufferのみに適用)。こんな悪魔的な機能が有るなんて、ちっとも知りませんでしたよ。

try crash for erlang

前回(こればっかりだな。まるで納豆みたいに尾を引いているぞ)、ひょんな事からerlangのクラッシュを体験した。調べておけっているオラクル(神の宣託)だな。

How to Interpret the Erlang Crash Dumps

りっぱな資料が用意されてた。これに則り、以前のデータを検視してみると、

=erl_crash_dump:0.5
Fri May 21 06:00:14 2021
Slogan: erl_child_setup closed
System version: Erlang/OTP 23 [erts-11.2] [source] [smp:1:1] [ds:1:1:10] [async-
threads:1] [hipe]
Compiled: Fri May 21 05:24:59 2021
Taints:
Atoms: 6784
Calling Thread: scheduler:1
  :

スローガンって所に載ってるのが、クラッシュした原因。スローガンって標語って意味もあるけど、この場合プチ違和感が有るな。

beam.smp が立ち上がると、サブプロセスでダエモン君もどきの erl_child_setup 1024 みたいに隠れたやつが起動する。こいつを強引に kill -9 すると、上のスローガンと同じになるぞ。

それから、自前でクラッシュダンプも取れるとな。

by halt

まずは、一番簡単なやつ。

1> halt("DOWN NOW").
DOWN NOW

Crash dump is being written to: erl_crash.dump...done
vbox$

こんなに簡単に落とせてしまっていいものだろうか? 世の中は善人だけでなりたっているって方針なんだな。

=erl_crash_dump:0.5
Sat May 22 07:56:03 2021
Slogan: DOWN NOW
System version: Erlang/OTP 21 [erts-10.2] [source] [smp:1:1] [ds:1:1:10] [async-threads:1]
 :

これが原因の説明。おかしな事が起こった場合。ここに説明を書いておくって使い方を想定してるんだろう。広い意味でdebugにどうぞって事か。

by SIGUSR1

次は、ちょっと悪人ぽいやつ。erlに入れないような場合を想定かな。

vbox$ ps a
  PID TT  STAT        TIME COMMAND
97305 p1  S+       0:00.52 /usr/local/lib/erlang21/erts-10.2/bin/beam.smp -- -r
 :
vbox$ kill -SIGUSR1 97305

外側から、信号を送って、自死してもらうんだな。

1> Received SIGUSR1

Crash dump is being written to: erl_crash.dump...done

by abort with dump

次はerl起動時に出て来る非常ボタンに触れてみる。

1> ^G
User switch command
 --> ^C
BREAK: (a)bort (A)bort with dump (c)ontinue (p)roc info (i)nfo
       (l)oaded (v)ersion (k)ill (D)b-tables (d)istribution
A
Crash dump requested by user
Crash dump is being written to: erl_crash.dump...done
Abort trap (core dumped)

場合によっては、大文字のA相当の表示が出ない版もあるけど、ちゃんと隠しコマンドになってる。

Slogan: Crash dump requested by user

これはもう非常事態。OSレベルでもcoreを生成してくれてる。検視せいって声があるぞ。

vbox$ gdb -q /usr/local/lib/erlang21/erts-10.2/bin/beam.smp beam.smp.core
 :
#0  thrkill () at /tmp/-:3
3       /tmp/-: No such file or directory.
[Current thread is 1 (process 275253)]
(gdb) bt
#0  thrkill () at /tmp/-:3
#1  0x054999b0 in _libc_raise (s=6) at /usr/src/lib/libc/gen/raise.c:37
#2  0x05441d6b in _libc_abort () at /usr/src/lib/libc/stdlib/abort.c:51
#3  0x145f7518 in erts_exit_epilogue () at beam/erl_init.c:2424
#4  0x145f761a in erts_exit_vv (n=-4, flush_async=<optimized out>,
    fmt=<optimized out>, args1=<optimized out>, args2=<optimized out>)
    at beam/erl_init.c:2410
#5  0x145f40a2 in erts_exit (n=<optimized out>, fmt=<optimized out>)
    at beam/erl_init.c:2434
#6  0x146b236a in do_break () at beam/break.c:535
#7  0x14709cf8 in erts_do_break_handling () at sys/unix/sys.c:773
#8  0x145a78c2 in aux_thread (unused=0x0) at beam/erl_process.c:3126
#9  0x1476d687 in thr_wrapper (vtwd=0xcf7e1f30) at pthread/ethread.c:118
#10 0x018bc25d in _rthread_start (v=0x44e5922c)
    at /usr/src/lib/librthread/rthread.c:96
#11 0x05469dea in __tfork_thread ()
    at /usr/src/lib/libc/arch/i386/sys/tfork_thread.S:92

at boot

次は無理して起こすcore。beam.smpは手順を踏んで起動するプログラムなんだけど、その手順を吹っ飛ばして、いきなりの起動。

vbox$ $BINDIR/beam.smp
{"init terminating in do_boot",no_or_multiple_root_variables}
init terminating in do_boot (no_or_multiple_root_variables)

Crash dump is being written to: erl_crash.dump...done

OSで言うなら、カーネルが起動してないのに、initを実行しちゃうみたいなものか。

Slogan: init terminating in do_boot (no_or_multiple_root_variables)

ダンプに至る経路は一緒かな。

(gdb) bt
#0  thrkill () at /tmp/-:3
#1  0x054999b0 in _libc_raise (s=6) at /usr/src/lib/libc/gen/raise.c:37
#2  0x05441d6b in _libc_abort () at /usr/src/lib/libc/stdlib/abort.c:51
#3  0x145f7518 in erts_exit_epilogue () at beam/erl_init.c:2424
#4  0x145f761a in erts_exit_vv (n=-4, flush_async=<optimized out>,
    fmt=<optimized out>, args1=<optimized out>, args2=<optimized out>)
    at beam/erl_init.c:2410
#5  0x145f40a2 in erts_exit (n=<optimized out>, fmt=<optimized out>)
    at beam/erl_init.c:2434
#6  0x146b236a in do_break () at beam/break.c:535
#7  0x14709cf8 in erts_do_break_handling () at sys/unix/sys.c:773
#8  0x145a78c2 in aux_thread (unused=0x0) at beam/erl_process.c:3126
#9  0x1476d687 in thr_wrapper (vtwd=0xcf7e1f30) at pthread/ethread.c:118
#10 0x018bc25d in _rthread_start (v=0x44e5922c)
    at /usr/src/lib/librthread/rthread.c:96
#11 0x05469dea in __tfork_thread ()
    at /usr/src/lib/libc/arch/i386/sys/tfork_thread.S:92

uftrace for erlang (2)

これまた前回の続きで、uftraceで特定の機能を実行した時のみ記録するに挑戦。まずはその前段階として、erlのプロンプトまで届くか確認。

debian:bin$ LD_PRELOAD=`pwd`/libstart_stop.so uftrace record --disable ./erlexec
mcount: /home/sakae/src/uftrace/libmcount/misc.c:103:uftrace_send_message
 ERROR: writing shmem name to pipe: Bad file descriptor
erl_child_setup closed

Crash dump is being written to: erl_crash.dump...done

こりゃ駄目だわさ。門前払いを喰らったぞ。

escript

だったら、shellを表に出さないスクリプトはどうだ?

debian:bin$ cat z.erl
-module(z).
-compile(main/1).

main([Arg]) ->
    io:format("Hello, ~s!~n", [Arg]).
debian:bin$ ./escript z.erl Erlang
Hello, Erlang!

コンパイルしてからの実行になるんで、とろいけどね。ちょっと心配。

debian:bin$ uftrace record ./escript z.erl Erlang
mcount: /home/sakae/src/uftrace/libmcount/misc.c:103:uftrace_send_message
 ERROR: writing shmem name to pipe: Bad file descriptor
erl_child_setup closed

Crash dump is being written to: erl_crash.dump...done

見事に心配が的中しました。マーフィーの法則、健在なり。

debian:bin$ grep exename uftrace.data/task.txt | cut -d' ' -f 5
exename="/home/sakae/MINE/lib/erlang/erts-11.2/bin/escript"
exename="/bin/dash"
exename="/bin/sed"
exename="/home/sakae/MINE/lib/erlang/erts-11.2/bin/erlexec"
exename="/home/sakae/MINE/lib/erlang/erts-11.2/bin/beam.smp"
exename="/home/sakae/MINE/lib/erlang/erts-11.2/bin/erl_child_setup"

dashなんて言うshellとかsedを繰り出してソースを加工。そして普通にerlを起動してるのか。 escriptの正体を見たりはいいんだけど、ちょいとお手上げ状態だな。残念だけど、これぐらいにしておくか。

noshell and run

debian:bin$ cat z.erl
-module(z).
-compile(export_all).

hello([Arg]) ->
    io:format("Hello, ~s!~n", [Arg]),
    erlang:halt().

最後の悪あがき。上記を事前にコンパイルしておき、それをshell無しで走らせる。

debian:bin$ erlc z.erl
z.erl:2: Warning: export_all flag enabled - all functions will be exported
debian:bin$ ./erlexec -noshell -run z hello ERLANG
Hello, ERLANG!

やっぱり駄目だった。

debian:bin$ grep exename uftrace.data/task.txt | cut -d' ' -f 5
exename="/home/sakae/MINE/lib/erlang/erts-11.2/bin/erlexec"
exename="/home/sakae/MINE/lib/erlang/erts-11.2/bin/beam.smp"
exename="/home/sakae/MINE/lib/erlang/erts-11.2/bin/erl_child_setup"

erlcでコンパイルするとVM用のバイトコードが作成される。それを実行するには、重厚な環境が、どうしても必要なんだな。

まあ、erlangの舞台裏を垣間見えたんで佳しとするか。余力が有るならuftraceを追ってみろってのは無しね(だって、リナでしか通用しない、特殊な世界だから。世の中にはBSD族も居るのよね)。

アクターモデル

erlangの特徴として並行実行がある。今度はこれに手を出してみる。複数のスレッドを起動して並行実行しようとすると、競合が発生する。

一つの解決方法は、ロックを取得し、それを得たスレッドのみが実行。それ以外は待機って方法。javaで使ってるね。

競合が発生するのは、スレッドが処理の途中で割り込むからだ。割り込むのを止めさせてしまえって方針で行くのがファイバーとかコルーチンと言う方法。

ファイバーと言えば、あの言語を思い出すな。 【Ruby】Fiber(ファイバー)を理解する とか RubyのThread、Fiber、Kernel、forkで並列処理やプロセスの深淵へ だ。オイラーが活躍してたのはruby1.4の頃。実はそれ以降のファイバーとかは、全く知らない。名前だけ知ってる状態。ここで、そちら方面にスイッチすると、戻ってこれなくなるから、メインスレッドに復帰する。

そもそもメモリーを共有するから問題が発生するのよ。だから共有止めるってのが、アクターモデル。プロセスは独立法人で共有が無い。プロセス間は、パイプ通信でしょ。こればアクターモデルの代表だ。でも、プロセスを作ったり消したりはOSに負荷をかける重い処理。気軽に出来ない。軽い奴って事でpthreadとかが有るけどね。これは、あからさまにメモリー共有になってるんで、振り出しに戻るって事になっちゃう。

天下のgolang様はどうしてる? 一応つまみグイ。 Go言語と並列処理 の連載に、 自立した複数のシステムで協調動作: アクターモデル (by GOLANG) こんなのも有った。高い所から俯瞰しとくのも大事だな。

そこん所を深く考察して、OSレベルでのプロセスじゃ無く、一つのプロセス(OSからみて)内で、実現したのがerlangだ。erlang内でのプロセスの作成とかは軽く出来るようになってるので、ホイホイとプロセス(erlang内のね)を作ったり消したり出来る。

通信の方法はパイプじゃなくて、メッセージ(バイナリーデータでも文字列でも何でもよい)を、送りつけるって方法になる。

構文は非常に簡単。送り先(のプロセス)を指定してメッセージを送るだけ。

1> self().
<0.81.0>
2> self() ! hoge.
hoge
3> self() ! [123,456].
[123,456]
4> flush().
Shell got hoge
Shell got [123,456]
ok

self() は自分自身(この場合erl-shell)のプロセスidを得る関数。hogeとか配列データを自身に送信。flush()で、受け取ったメッセージを確認出来る。勿論、メッセージが送られて来るのを待ち構えていて、メッセージに反応するってのが、普通の使い方になる。

今回はshellにあからさまな受け手が居なかったので、こっそりメッセージを蓄えていた。それをflushで吐き出したって事になる。尚、メッセージを貯めておく箱を、未決済箱(部長の机にある奴です)とは言わずに、メールボックスなんて普通に呼んでいる。

メッセージは何でもいいって書いた。受け取る方は何でもに対応するため、一種のパターンマッチ構文 receive を使って場合分けする。そして、処理が終わると、そのメッセージは削除される(やっぱり、未決済箱と呼びたいぞ)。

send / receive

ob$ cat sr.erl
-module(sr).
-export([hoger/1, send_and_receive/2]).

hoger(P) ->
        receive X -> P ! {hoge,X} end,
        hoger(P).

send_and_receive( P, X ) ->
        P ! X,
        receive R -> R end.

簡単な例だけど、hogerは、何か受け取ったら、指定されたプロセスへ hogeって前置詞を付けて、受け取った何かを送り返す。 send_and_receive の方は、指定されたプロセスへメッセージを送り、また待ちに入るってやつ。

1> c(sr).
{ok,sr}
2> Q = spawn(sr, hoger, [self()]).
<0.86.0>
3> sr:send_and_receive(Q, hello).
{hoge,hello}
4> sr:send_and_receive(Q, {fumu, hage}).
{hoge,{fumu,hage}}

spawanは指定した関数を、プロセスとして起動するやつ。起動したプロセスidが返ってくるので、Qに保存してる。後は、そのプロセスQに対して、適当なデータを送信。hogerが前置詞を付けて、オウム返ししてるのを見て取れる。

3文字の省略

erlangでは、3文字の省略系がよく出てくるので(ソースを見た時にね)まとめておく。 ヘイ、シリ… とか言って聴くのが恥ずかしいので、erlang what stands for XXX ってggしたよ。

NIF   Native Implemented Functions
OTP   Open Telecom Platform
BIF   Built In Functions
MFA   Module, Function, Argument
BEAM  Bogdan/Björn's Erlang Abstract Machine
EPMD  Erlang Port Mapper Daemon

MFAはマニュアルにも出て来るな。上のswawn(M,F,A)に限らず、頻出してるぞ。

pthreadもどき

erlangでは、ホイホイとプロセスを作って、その間をメッセージで通信しあうってのが普通のスタイル。 プロセスの作成は、spawn関数で指定したMFAで起動される。この起動された関数がずっと居座れば、サーバーとして使える。関数だから状態を持たないってのが味噌になる。サーバーが応答しなくなったら、手軽に別な関数をspawnするだけだ。

そんじょそこらに有る、オブジェクト指向言語(Pyton,ruby,…)とは、違うんです。状態が無いですからねぇ。あなたとは違うんです どういう訳か、こんな発言を思い出したぞ。

この状態を持たないって性質を利用すると、気軽に稼働中のシステムにパッチを当てられる。そう、問題のあるモジュールを新しい物に入れ替えちゃうって技。絶対に止められない、電話交換システムを作るにあたっての重要事項だった。現場から生まれた言語なんだな。

で、ネットを見てたら、 Erlangによるスケーラブルなマルチコア対応分散プログラミング なんて言う研究が行われていた。こういう事も手軽に出来るんだなあ。以前にやったpthreadみたいなのをerlangでやったらどうなる? 素晴らしい例が有ったので引用させて頂く。

-module(test).
-export([calc_fibs/1, fib_send/2]).

% like main
calc_fibs( Lst ) ->
        calc_fibs_list( Lst ),
        receive_results( length(Lst) ).

% target calc
fib(0) -> 1;
fib(1) -> 1;
fib(N) -> fib(N-1) + fib(N-2).
fib_send(P,N) -> P ! {N,fib(N)}. 

% like pthread_create
calc_fibs_list([])      -> ok;
calc_fibs_list([N|Lst]) ->
        spawn( test, fib_send, [self(),N] ),
        calc_fibs_list(Lst).

% like pthread_join
receive_results( 0 ) -> complete; 
receive_results( N ) ->
        receive
                {X, Fx} ->
                        io:format("fib(~w) = ~w~n", [X, Fx] ),
                        receive_results( N-1 )
        after 1000 ->
                timeout
        end.

毎度おなじみの引数によっては膨大な計算時間がかかるfibの計算です。複数の計算を並行に行う、pthread風なやつです。以前にやったpthreadに強引にこじつけてみました。

1> c(test).
{ok,test}
2> test:fib_send( self(), 5).
{5,8}
3> test:fib_send( self(), 20).
{20,10946}
4> test:calc_fibs( [20, 15, 10]).
fib(10) = 89
fib(20) = 10946
fib(15) = 987
complete
5> test:calc_fibs( [20, 15, 40, 10]).
fib(15) = 987
fib(10) = 89
fib(20) = 10946
timeout

fib_send(P, N) で、fib(N)の結果を引数と共にeshellに送ってみた。次は、複数の引数を計算させて、ただ表示。after 1000 となってるのは、1000ms以内に計算が終了しないと、joinを諦める仕様にしたからです。

6> c:flush().
Shell got {40,165580141}
ok

でも、解き放たれた計算はそのまま進行してて、結果をeshellが受け取ってくれている。

今、ふと思ったんだけど、spawnって関数、shellから使っていると、バックグランドでのコマンド実行って見做せるね。

sakae@pen:~$ ls sim &
[1] 1744
sakae@pen:~$ i386  RASP  xv6-public

[1]+  Done                    ls --color=auto sim

コマンドlsに引数simを付けて、& で、spawnするって事。pidの1744が返ってきた。ね、そっくりでしょ。起動したコマンドが終了したからいいようなものだけど、ずっと居座って、よそのプロセスと通信を始めちゃったら、これはもう、サーバーですよ。bashとかだと、裏に回ったコマンドをfgで表に引き出せる。表に引き出せるって事は、再び標準入出力とのやり取りを可能にするって事だ。

本当のサーバーだと、裏に回すと同時に、手足をもぎ取ってしまったり、ワーキング・ディレクトリを/に変更したりするんだけどね。


This year's Index

Home