1つのプログラムが単独で動くのではなく, 他のプログラムを子供として起動して相互に情報のやりとりをして 動かしたりすることもある。複数のプログラムを協調して動かすことにより, 単独で動かすよりも効果的な処理が可能になる。そのためにはシステム上で プログラムが起動されている状態,すなわちプロセス についての制御が必要になる。
他のあらゆるプログラムと同様, RubyプログラムもUnixシステム上の1プロセスとして動いている。 プロセスは新たに生成したり,生成したものに信号を送ったりなどの 操作ができる。
Unixプロセスは1つのプログラムが活動している状態で, 状態を示す値がいくつかある,プログラミングをする上で 知っておくべき代表的なものを示す。
各プロセスに振られる固有の整数
そのプロセスを生成したプロセスのプロセスID
プロセスの動作権限となるユーザID
プロセスの動作権限となるグループID
プロセスのその時点の作業ディレクトリ
入出力を結び付けられた端末
走行中のRubyプログラムのシステムプロセスとしての情報は Process モジュールを介して得たり設定したりできる。以下のプログラムは 起動したプロセスのPID, UID, GIDを出力する。
#!/usr/bin/env ruby printf("pid=%d, UID=%d, GID=%d\n", Process.pid, Process.uid, Process.gid)
プロセス自身のPIDは変数 $$
でも参照できる。
プロセスのUID, GIDは,そのプログラムを起動したユーザのUID,
GIDとなる。このあと,走行中のプロセスに信号を送るシグナルについて
述べるが,シグナル送信は送信者のUIDが走行中プロセスのPIDと一致しているか,
スーパーユーザのID(UID=0)である場合のみ有効になる。
Unixシステムは起動時に /sbin/init
プロセスが起動され,
残りのプロセスはすべてその子として起動される。
プロセスの生成は以下の2つの機構を組み合わせて行なわれる。
fork
現在の自己プロセスのコピーを作り,プログラムの同じ場所から 続けて動作する
exec
現在の自己プロセスの情報をすべて引き継ぐ外部プログラムに 実行を移す
まず,それぞれの動きを理解するために以下の2つのプログラムを 動かしてみよう。
#!/usr/bin/env ruby # -*- coding: utf-8 -*- $stdout.sync = true # 標準出力を溜めずにすぐ書き出すため def message(me) 0.upto(4) do |i| printf("%sその%d\n", me, i+=1) sleep 1 end end pid = fork # forkによってプロセスの分身発生 if pid then printf("こちらは親(pid=%d)\n", pid) message("親") else # 分身側のプロセス sleep 0.5 message("\t分身") end
このプログラムを実行した結果は以下のようになる(pid値はその都度異なる)。
./fork.rb
こちらは親(pid=10783)
親その1
分身その1
親その2
分身その2
親その3
分身その3
親その4
分身その4
親その5
分身その5
実際に動かしてみると分かるが,1つのプログラムの実行主体が2つになり, それぞれが独立して message メソッドを実行している。
続いてexecの効果を調べる。
#!/usr/bin/env ruby exec("/bin/ls") # ここでexecしてプロセスが完全に置き換わる puts "Hello!!!" # ここは実行されるか?
実行すると,exec文のところで実行主体が ls コマンドになり代わり, execより後の文が実行されずにRubyが終了していることが分かる。
./exec.rb
cat-n-t.rb f+e.rb ioe-open3.rb pipe.rb th.rb
cat-n.rb fork.rb ioe-pfe.rb popen-cal.rb thread.html
exec.rb i ioe.sh process.html timeshock.rb
execproc.html index.html mpg123.rb report.html
f+e+t.rb intbye.rb mutex.rb th-op.rb
これら fork と exec を続けて行なうことで,
1つのプログラムから別のプログラム実行を起こすことが可能となる。
一般的に,1つのプログラムの制御下で別のプログラムを起動するには
forkとexecを組み合わせて行なう。forkの例示プログラムにあるように,
forkを呼ぶと呼び出し元となったプロセスには子プロセスのPIDが返されるが,
子プロセスにはnil
が返るので,forkの返却値をもとに
ifで条件分岐して親プロセスの処理と子プロセスの処理を分けて記述する。
以下の例は,1つのRubyプログラムから仮想端末コマンド(例ではkterm)を起動し, それを親となるRubyプログラムから制御するものである。
#!/usr/bin/env ruby # -*- coding: utf-8 -*- cmd = ARGV[0] || "kterm" # コマンドライン引数に指定した場合はそれを,しない場合はktermを起動 if (pid = fork()) then # 親プロセスのみ必要な処理はここ else # 子プロセスのみの処理はここ exec(cmd) end STDERR.printf("%s について: 終了を待つ=w killする=k 放置=その他: ", cmd) case STDIN.gets when /^k/i STDERR.puts "ボシュっ" Process.kill(:KILL, pid) when /^w/i STDERR.puts "じゃ,待ちます。" Process.wait else STDERR.puts "じゃ,わしゃ勝手に終わります。さいなら。" end
exec
は,プログラムの起動に失敗すると
例外を発生させる。実際のプログラムでは外部プログラムの実行が
失敗する可能性も考えなければならない。そのときの処理は
スレッドで解説する。
上記の例題プログラムにある Process.kill
は,
あるプロセスにシグナルを送るためのメソッドである。
シグナルとはUnixシステムで走行中のプロセスに対し,「何か」が起こったことを
非同期に通知するための仕組みで,
どんなプロセスも走行中,自分自身に送られるシグナルを即時に受け取り
それに応じた処理に移ることも,無視することもできる。
「それに応じた処理」は,シグナルを受け取ったときに自動的に呼ばれる
手続きを登録することで行なわせる。自動的に呼ばれる部分を
シグナルハンドラという。
シグナルはシステムによって若干異なるが数十種類のものが 存在する。日常的プログラミングで利用する主なものには以下のものがある。
シグナル名 | 意味 |
---|---|
HUP | Hangup 端末の回線が切断されたとき (ktermなど親となる仮想端末が終了したときも該当) |
INT | Interrupt 割り込みキー(C-c)を タイプしたとき |
QUIT | 終了キー(C-\ )をタイプしたとき |
KILL | 強制終了(捕捉できない) |
SEGV | Segmantation violation セグメンテーション 違反(書き込み禁止メモリへの書き込みなど) |
ALRM | Alarm 設定した制限時間が経過したときに 自動的に送られる |
TERM | Terminate killコマンドによる 強制終了(捕捉できる) |
STOP | 端末以外からの実行中断(捕捉できない) |
TSTP | 端末からの実行中断(C-z) |
CONT | 中断からの復帰 |
USR1 | ユーザ定義用(1) |
USR2 | ユーザ定義用(2) |
Rubyプログラムから別のプロセスにシグナルを送るには
Process.kill
に
シグナル名の前にコロン(:
)を付けたシンボルと,
シグナル送信先のプロセスIDを指定する。
なお,表中にある割り込みキーの割り当て状況は stty コマンドで確認できる。
stty -a speed 9600 baud; 25 rows; 80 columns; lflags: icanon isig iexten echo echoe -echok echoke -echonl echoctl -echoprt -altwerase -noflsh -tostop -flusho pendin -nokerninfo -extproc iflags: -istrip icrnl -inlcr -igncr ixon -ixoff ixany imaxbel -ignbrk brkint -inpck -ignpar -parmrk oflags: opost onlcr -ocrnl -oxtabs -onocr -onlret cflags: cread cs8 -parenb -parodd hupcl -clocal -cstopb -crtscts -mdmbuf -cdtrcts cchars: discard = ^O; dsusp = ^Y; eof = ^D; eol = <undef>; eol2 = <undef>; erase = ^?; intr = ^C; kill = ^U; lnext = ^V; min = 1; quit = ^\; reprint = ^R; start = ^Q; status = ^T; stop = ^S; susp = ^Z; time = 0; werase = ^W;
シグナル関連でないものも含まれるが,下線
で示した intr,quit,susp はそれぞれINTR,QUIT,TSTP
シグナルをその端末で走行中のプロセスに送るキーが,
C-c,C-\
,C-z
であることを示している。
プログラムにシグナルが送られた場合のデフォルトの処理は
シグナルごとに決まっている。たとえば,SIGINTが送られた場合は
プログラムが中断させられる。これを別のものに変える場合は
シグナルハンドラの登録を行なう。このためには Signal.trap()
を用いる。
Signal.trap(:シグナル, ハンドラ)
ハンドラ として nil
を指定すると
シグナルを無視する。"DEFAULT"
を指定すると
独自のハンドラ登録を解除しデフォルトの処理を行なわせる。
Rubyで書かれた任意の処理を行なわせたいときは Proc オブジェクトで,
「proc {処理}
」のように指定する。
次のプログラムは C-c を
押されて SIGINT が送られたときの処理を変えるものである。
#!/usr/bin/env ruby # -*- coding: utf-8 -*- def bye() Signal.trap(:INT, nil) # ここでのSIGINT(C-c)は無視 STDERR.puts "そんなー。" sleep 1 STDERR.puts "でもサヨナラ" exit 1 end Signal.trap(:INT, proc {bye}) # SIGINTハンドラを bye メソッド呼び出しに puts "C-cキーでは止まりません。押してみてください。" 10.downto(1) do |i| STDERR.printf("%d..", i) sleep 1 end STDERR.puts "0 おしまい!"
実際に実行し,カウントダウンが進んでいる最中に C-c を押してみる。
./intbye.rb
C-cキーでは止まりません。押してみてください。
10..9..8..7..^Cそんなー。
でもサヨナラ
さらに,「そんなー」が出力されて sleep 1
で待機している間をねらってさらに C-c
をタイプすると無視されることが分かる。
10..9..8..7..^Cそんなー。 ^C^C^Cでもサヨナラ
bye
メソッド内ではすぐにSIGINTのハンドラを
nil
,すなわち無視する設定にしているため,
C-cを連打しても止まらない。
実行中のプログラムは通常 C-c で終了する。 しかし,一時ファイルを作成しているようなプログラムは C-c で止められたときに,一時ファイルを消去するなどの 必要な後始末を行なってからexitするようにする。また, ネットワークサーバープログラムなど,端末と結び付けられずに 動くプログラムでは,設定ファイルの読み直しなどの司令を 端末に依らずに送ることができる。USR1,USR2 シグナルは プログラム作成者が自由に定義するために用意されているシグナルで, 設定ファイルの読み直しなどにしばしば利用される。
#!/usr/bin/env ruby # -*- coding: utf-8 -*- class USR1 def initialize # .newで呼ばれるメソッド @counter = 0 # 数え上げていく変数 printf("他の端末で kill -USR1 %d\n", $$) # $$は Process.pid と同じ Signal.trap(:USR1, proc {reset}) while true STDERR.printf("%d..", @counter+=1) sleep 1 end end def reset @counter = 0 # USR1シグナルが送られたらカウンタをリセット STDERR.puts "リセット!" end end USR1.new
上記プログラム実行用の端末以外に,もう1つ仮想端末を起動してから
sigusr1.rb
を実行してみる。起動直後のメッセージに出るように
他の仮想端末から kill -USR1 ???
をコマンド入力すると以下のような結果が得られる。
./sigusr1.rb
他の端末で kill -USR1 15861
1..2..3..4..5..6..リセット!
1..2..リセット!
1..2..3..4..5..6..7..8..9..10..リセット!
1..2..3..4..5..^C./sigusr1.rb:11:in `sleep': Interrupt
from ./sigusr1.rb:11:in `initialize'
from ./sigusr1.rb:20:in `new'
from ./sigusr1.rb:20
実行例は他端末で kill コマンドにてUSR1シグナルを3度送った後, 起動した端末に戻り C-c をタイプして停止したものである。