Rubyで試すUNIXプロセス
プロセスの基本
プロセスとはOS上で実行中のプログラムのインスタンスを指し、独自のアドレス空間、コードデータ及びその他のシステムリソースを持つ。
プロセスはpsコマンドで確認ができる。
$ ps
PID TTY TIME CMD
1228 pts/0 00:00:00 bash
1253 pts/0 00:00:00 ps
psコマンドもプログラムなので、上記の実行結果にpsのプロセスが表示されていることが確認できる。
システムに存在する全プロセスを列挙したいのであればauxをつける。
$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
...
root 762 0.0 0.1 55860 2392 ? Ss 14:08 0:00 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
www-data 763 0.0 0.3 56600 6336 ? S 14:08 0:00 nginx: worker process
www-data 764 0.0 0.3 56600 6336 ? S 14:08 0:00 nginx: worker process
www-data 765 0.0 0.3 56600 6336 ? S 14:08 0:00 nginx: worker process
www-data 766 0.0 0.3 56600 6336 ? S 14:08 0:00 nginx: worker process
root 789 2.9 3.7 1462872 75668 ? Ssl 14:08 0:01 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
root 915 0.0 0.0 0 0 ? I 14:08 0:00 [kworker/3:3-cgroup_destroy]
root 1114 0.0 0.5 17180 11008 ? Ss 14:08 0:00 sshd: hoge [priv]
hoge 1117 0.1 0.4 17068 9844 ? Ss 14:08 0:00 /lib/systemd/systemd --user
hoge 1118 0.0 0.1 169464 3916 ? S 14:08 0:00 (sd-pam)
hoge 1227 0.1 0.4 17312 8140 ? R 14:08 0:00 sshd: hoge@pts/0
hoge 1228 0.0 0.2 8736 5568 pts/0 Ss 14:08 0:00 -bash
hoge 1252 0.0 0.0 10072 1608 pts/0 R+ 14:09 0:00 ps aux
- a
- すべてのユーザーのプロセスを表示
- u
- プロセスの実行ユーザーも表示
- x
- 端末をもたないプロセス(デーモン)も表示
- TTYが?のやつ
システムで動作するすべてのプロセスには識別子(PID)が存在する。
rubyではProcess
モジュールを通じてプロセスのPIDや親プロセスの情報を取得できる。
puts Process.pid
# 出力例: 20091
親プロセス
すべてのプロセスには親が存在する。 というのもプロセスが生成されるときにすでにあるプロセスが分裂することで新しくプロセスを生成する。
よって必ず分裂元である親がいるのである。
大抵の場合、親プロセスはそのプロセスを起動したプロセスである。
psコマンドでプロセスの親子関係を確認するにはf
オプションを使う(ちなみにfはforestのf)。
$ ps f
PID TTY STAT TIME COMMAND
1228 pts/0 Ss 0:00 -bash
1409 pts/0 R+ 0:00 \_ ps f
上記の例ではbashのプロセスからpsコマンドのプロセスが生えており、bashが親プロセスでpsが子プロセスであることがわかる。
子プロセスを生成するにはfork(2)システムコールを使う。
以下はrubyでプロセスを複製した例。
puts "親プロセスのPIDは#{Process.pid}です"
if fork
puts "#{Process.pid}よりifブロックに入りました"
else
puts "#{Process.pid}よりelseブロックに入りました"
end
出力
親プロセスのPIDは19248です
19248よりifブロックに入りました
19263よりelseブロックに入りました
親プロセスによってifブロックが実行され、子プロセスによってelseブロックが実行されているのが確認できる。
forkメソッドは親プロセスでは生成した子プロセスのPIDが返り、子プロセスではnilが返る。
標準ストリーム
すべてのUnixプロセスには以下の3つの開かれた標準ストリームと呼ばれるものがついてくる。
- 標準入力
- プログラムの標準的な入力(キーボードからの入力など)
- 標準出力
- プログラムの標準的な出力(モニタの画面など)
- 標準エラー出力
- プログラムのエラーメッセージの標準的な出力(モニタの画面など)
puts STDIN.fileno
puts STDOUT.fileno
puts STDERR.fileno
出力結果
0
1
2
0, 1, 2の整数は後で説明するファイルディスクリプタと呼ばれるものである。
プログラムは「何かしらの入力を受けて、処理をし、何かしらの結果を返す」ものと捉えることができる。そう捉えたときに、 その標準の入出力先としてプロセスとつながっているのが、「標準ストリーム」というものになる。
puts Process.pid
# 出力例: 20091
こちらのプログラムで使用しているputsは標準出力に対して文字を出力していると言える。
また、標準入力から文字を受け取るにはgetsメソッドを利用する。
in = gets
puts "#{in}が入力されました"
この標準入力や標準出力は、キーボードやモニター以外のもの付け替えることもできる。
プロセスとファイルディスクリプタについて
OSがプログラムにリソース(ファイル、ソケットなど)へのアクセスを許可するとき、そのリソースに対する参照としてファイルディスクリプタを提供する。
プログラムはこのディスクリプタを使用して、リソースを操作しようとする。
例えば、プログラムがファイルを開くときに、「ファイルを開く」要求を行い、成功すればOSはファイルディスクリプタを返す。
リソースを開いたプロセスが終了するとファイルディスクリプタ番号も閉じられる。
rubyではfileno
でファイルディスクリプタ番号を確認できる。
passwd = File.open('/etc/passwd')
puts passwd.fileno
hosts = File.open('/etc/hosts')
puts hosts.fileno
出力結果
3
4
プロセスがファイルに対して入出力を行うときには、このファイルディスクリプタ番号を通して入出力をしている。
出力したファイルディスクリプタ番号が3で始まっている理由としては、0, 1, 2はプロセスが生まれると同時に使われるからである。
標準入出力先を入れ替えてみる
rubyではreopenというメソッドを使うことで、標準入出力先を付け替える事が可能。
puts "標準入力のファイルディスクリプタ: #{STDIN.fileno}"
puts "標準出力のファイルディスクリプタ: #{STDOUT.fileno}"
file = File.open('a.txt', 'w')
puts "a.txt ファイルへの入出力のファイルディスクリプタ: #{file.fileno}"
STDOUT.reopen(file) # 標準出力先を変更
puts '標準入力先が変わっているはず'
実行結果
標準入力のファイルディスクリプタ: 0
標準出力のファイルディスクリプタ: 1
a.txt ファイルへの入出力のファイルディスクリプタ: 5
最後に「標準入力先が変わっているはず」という出力が出ないのは、a.txtの中に出力されているからである。
a.txtの中身を見てみると確かに出力先が変わっている。
$ cat a.txt
標準入力先が変わっているはず
プロセスの終了
プロセスを終了するときはkillコマンドを使う。
以下はバックグラウンドで実行しているsleepコマンドのプロセスを終了する例。
$ sleep 60 &
[1] 24039
$ kill 20439
[1] + 24039 terminated sleep 60
シグナル
killコマンドはシグナルを送信するコマンド。
シグナルとはプロセスに送信される信号のこと。
シグナルの種類を一覧表示するにはl
オプションを使う。
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
...
シグナルにはSIGから始まる名前と番号を持っていて、SIGは省略可能。
よく使われるシグナルとしては以下のものがある。
番号 | シグナル名 | 機能 |
---|---|---|
2 | INT | キーボードからの割り込み(Ctrl + C) |
9 | KILL | 強制終了 |
15 | TERM | 終了シグナル(killコマンドのデフォルト) |
20 | TSTP | 停止(Ctrl + Z) |
以下はrubyのプロセスを2つ起動して、片方からのシグナルを使ってもう片方のrubyプロセスをkillする例。
puts Process.pid
sleep 60
# 前のプログラムの実行した結果、26766というPIDを持っていた場合
Process.kill(:INT, 26766)
シグナルの再定義
シグナルを再定義するとシグナルを受け取ったときの振る舞いを変更できる。
puts Process.pid
trap(:INT) { print "終了できないお!" }
sleep 60
このプロセスをCtrl + Cで終了しようとするとできない。
これはINTのシグナルを受け取ったときの振る舞いがprint文で上書きしているため、終了ができないのである。
しかし、シグナルには再定義ができないものもあり、その中の1つがKILLであり、以下のコマンドでなら終了ができる。
$ kill -9 <rubyのプロセスの番号>
プロセス間通信
パイプについて
パイプは単方向のデータの流れで、「パイプを開く」とはプロセスの片方の端を別のプロセスの端につなぐことをいう。
パイプの基本的な動作は「書き込み」と「読み出し」の2つの操作で構成されている。
一方のプロセスがパイプにデータを書き込むと、そのデータはパイプを通して別のプロセスに渡され、読み出し操作で取り出される。
ls | grep "txt"
このコマンドラインは、現在のディレクトリ内のファイルリストを取得し(ls)、その結果から "txt" を含む行だけを抽出(grep "txt")する。
rubyでパイプを作る方法は以下の通りで、IOオブジェクトの配列を返す。
reader, writer = IO.pipe #=> [#<IO:fd 5>, #<IO:fd 6>]
このIO.pipeから返ってくるIOオブジェクトは名前のないファイルのようなものだと考えてよくて、Fileと同じように扱うことができる。
reader, writer = IO.pipe
writer.write("パイプの中をくぐります")
writer.close
puts reader.read # パイプの中をくぐります
Discussion