プロセス

1つのプログラムが単独で動くのではなく, 他のプログラムを子供として起動して相互に情報のやりとりをして 動かしたりすることもある。複数のプログラムを協調して動かすことにより, 単独で動かすよりも効果的な処理が可能になる。そのためにはシステム上で プログラムが起動されている状態,すなわちプロセス についての制御が必要になる。

他のあらゆるプログラムと同様, RubyプログラムもUnixシステム上の1プロセスとして動いている。 プロセスは新たに生成したり,生成したものに信号を送ったりなどの 操作ができる。

プロセスの属性

Unixプロセスは1つのプログラムが活動している状態で, 状態を示す値がいくつかある,プログラミングをする上で 知っておくべき代表的なものを示す。

プロセスID (PID)

各プロセスに振られる固有の整数

親プロセスID (PPID)

そのプロセスを生成したプロセスのプロセスID

ユーザID (UID)

プロセスの動作権限となるユーザID

グループID (GID)

プロセスの動作権限となるグループID

カレントディレクトリ

プロセスのその時点の作業ディレクトリ

TTY

入出力を結び付けられた端末

走行中の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)である場合のみ有効になる。

fork&exec

Unixシステムは起動時に /sbin/init プロセスが起動され, 残りのプロセスはすべてその子として起動される。 プロセスの生成は以下の2つの機構を組み合わせて行なわれる。

  1. fork

    現在の自己プロセスのコピーを作り,プログラムの同じ場所から 続けて動作する

  2. exec

    現在の自己プロセスの情報をすべて引き継ぐ外部プログラムに 実行を移す

まず,それぞれの動きを理解するために以下の2つのプログラムを 動かしてみよう。

fork.rb

#!/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の効果を調べる。

exec.rb

#!/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プログラムから制御するものである。

f+e.rb

#!/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システムで走行中のプロセスに対し,「何か」が起こったことを 非同期に通知するための仕組みで, どんなプロセスも走行中,自分自身に送られるシグナルを即時に受け取り それに応じた処理に移ることも,無視することもできる。 「それに応じた処理」は,シグナルを受け取ったときに自動的に呼ばれる 手続きを登録することで行なわせる。自動的に呼ばれる部分を シグナルハンドラという。

シグナルはシステムによって若干異なるが数十種類のものが 存在する。日常的プログラミングで利用する主なものには以下のものがある。

シグナル名意味
HUPHangup 端末の回線が切断されたとき (ktermなど親となる仮想端末が終了したときも該当)
INTInterrupt 割り込みキー(C-c)を タイプしたとき
QUIT終了キー(C-\)をタイプしたとき
KILL強制終了(捕捉できない)
SEGVSegmantation violation セグメンテーション 違反(書き込み禁止メモリへの書き込みなど)
ALRMAlarm 設定した制限時間が経過したときに 自動的に送られる
TERMTerminate 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-cC-\C-z であることを示している。

シグナル捕捉

プログラムにシグナルが送られた場合のデフォルトの処理は シグナルごとに決まっている。たとえば,SIGINTが送られた場合は プログラムが中断させられる。これを別のものに変える場合は シグナルハンドラの登録を行なう。このためには Signal.trap() を用いる。

Signal.trap(:シグナル, ハンドラ)

ハンドラ として nil を指定すると シグナルを無視する。"DEFAULT" を指定すると 独自のハンドラ登録を解除しデフォルトの処理を行なわせる。 Rubyで書かれた任意の処理を行なわせたいときは Proc オブジェクトで, 「proc {処理}」のように指定する。 次のプログラムは C-c を 押されて SIGINT が送られたときの処理を変えるものである。

intbye.rb

#!/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 シグナルは プログラム作成者が自由に定義するために用意されているシグナルで, 設定ファイルの読み直しなどにしばしば利用される。

sigusr1.rb

#!/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 をタイプして停止したものである。


本日の目次

yuuji@e.koeki-u.ac.jp