🧪

Rubyで試すUNIXプロセス

2024/06/02に公開

プロセスの基本

プロセスとは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 # パイプの中をくぐります

参考

なるほどUnixプロセス ― Rubyで学ぶUnixの基礎

『なるほどUnixプロセス』を読む前にちょっとだけナルホドとなる記事

Discussion