Yuki's Tech Blog

仕事で得た知見や勉強した技術を書きます。

プロセス周辺の知識についてざっくり深ぼる

概要

プロセス周辺の知識が必要だなと感じる場面が何度かあったので、ざっくり深掘ります。

そもそもプログラムはどうやって実行されているのか

プロセスを学ぶ前に、そもそもプログラムはどうやって実行されるのかが見えてくると、プロセスが理解しやすくなります。 以下に、静的型付け言語で書いたプログラムがどのように実行されるかの手順をまとめます。

  1. まず、プログラムのファイルはハードディスクに存在しています。
  2. ビルドを実行して、機械語で書かれた実行ファイルを作成します。この実行ファイルもハードディスクに保存されます。(ビルドはコンパイル処理 + リンク処理)
  3. 実行ファイルのプログラムを実行した場合、OSは実行ファイルをメモリ上に展開します。その「メモリ上でいつでも実行できる状態のプログラム」を、OSは「プロセス」として管理します。
  4. プロセスは1つ以上のスレッド(プログラムの処理の流れ)を持っていて、CPUコアでそのスレッドを処理します。スレッドはCPUから見た際のプログラムの処理単位です。CPUコアでスレッドが実行されたら、プログラムの処理がちゃんと実行されています。

動的型付け言語の場合はちょっと特殊で、ソースプログラムをインタプリタが解析しながら実行するようです。インタプリタはまだまだ理解できていない部分があるので、いつか作ってみようと思います。

www.hpcs.cs.tsukuba.ac.jp

next.rikunabi.com

プロセスとは

プロセスとは、OSからプログラムを見た際のプログラムの実行単位です。 OSから見てプログラムはプロセスとして管理されています。 OSは限られたCPUで複数のプロセスを効率よく実行するために、OSは適切にプロセスを切り替えています。

プロセスの特徴を以下にまとめます。

プロセスはプロセスidを用いてOSによって管理されている

プロセスはプロセス自体を識別するためにプロセスidを用いて、OSによって管理されています。

プロセスは1つ以上のスレッド(プログラムの処理の流れ)を持つ

プロセスは1つ以上のスレッド(プログラムの処理の流れ)を持ちます。

プロセス自体はメモリ上に存在している

プロセス自体はメモリ上に存在しています。プロセスは自分が独占したメモリの中に存在していて、その中で何をしても他のプロセスには影響を与えません。メモリ上に存在しているプロセスの構成をすごくざっくり言うと、「プログラム」と「プログラムの実行に必要なデータ(変数や環境変数等々)」の2つで構成されています。

CPUコアでプロセスが持つスレッドを実行する過程で、外部データがプロセスにインプットされる場合、プロセスが確保しているメモリ領域を動的に増やして、データを受け入れられるようにしたりします。ただ、データがあまりにも大きいと、プロセスが確保しないといけないメモリ領域も多くなってしまい、サーバー自体に元々用意されているメモリ領域を大きく圧迫してしまい、結果的にサーバーの処理能力を低下させてしまいます。 Linuxカーネルの持つ機能でOOMKillerというのがあります。この機能はシステムがメモリ不足になったときに、メモリを多く消費しているプロセスを強制的に殺すという機能です。例えばアプリケーションサーバーのプロセスがあまりにもメモリを使いすぎていて、システムがメモリ不足になると、そのプロセスが強制的に殺されてしまます。その結果、ユーザーがサイトにアクセスした際に画面が表示されず、ビジネス的な損失を被る可能性があります。

プロセスのライフサイクル

通常、プロセスは、以下のようなライフサイクルです。

  1. プロセスが何らかの方法で生成される
  2. 処理中(実行中、待ち状態、ブロック中の3パターンがある)
  3. 終了

処理中の間、プロセスは3つの状態を持ちます。

実行中は、CPUで実行されている状態です。 待ち状態は、CPUでいつでも処理できるよという状態です。 ブロック中は、プロセスでIO待ちが発生しているから、今はCPUで処理できないよという状態です。 基本的にはプログラムの実行が終了することで、プロセスが終了します。 プロセスの生成に関しては、forkというシステムコールを用いることで生成できます。

システムコールとは

システムコールとは、アプリケーションプログラムがカーネルの機能を利用するためのインターフェースです。システムコールはアプリケーションプログラムとカーネルの間に存在します。通常システムコールは関数で表現されます。

例えば、ネットワークを利用した通信、ファイルへの入出力、新しいプロセスの生成、プロセス間通信、コンテナの生成などは、システムコールを使用することで実現されています。

qiita.com

forkとexec

fork

forkとは、OSに親プロセスを複製して子プロセスを生成するように命令するためのシステムコールです。 通常、プロセスは、親プロセスがforkシステムコールをOSに送ることで、生成されます。 forkを実行すると、OSは親プロセスを複製して子プロセスを生成します。つまり、この時、メモリ上のプロセスのデータが複製されていることを意味します(複製とは言いつつも、別のプロセスとしてメモリ上に存在するので、プロセスidは異なります)。 forkによってプロセスは生成されるため、基本的に全てのプロセスには「自分を生んだ親プロセス」が存在します。

注) forkした子プロセスはforkを実行した位置以降の処理を実行するので、そこは注意しましょう。

親のプロセスの親のプロセスの親のプロセスのって辿っていけるのかと疑問に思うと思います。実は全ての祖先となるinitプロセスとなるものがLinuxには存在していて、このプロセスは、コンピュータが起動したときに生成されて、そのあと全てのプロセスがここを祖先としてforkされていきます。このプロセスは一番最初のプロセスなので、プロセスidが1です。

プロセスは木構造の親子関係を持っています。この親子関係を「プロセスツリー」と呼びます。macだとlaunchdというプロセスがinitプロセスと同等のプロセスです。 pstreeというコマンドを用いることで、プロセスツリーを確認できます。

pstree
-+= 00001 root /sbin/launchd
 |--= 00092 root /usr/libexec/logd
 |--= 00093 root /usr/libexec/smd
 |--= 00094 root /usr/libexec/UserEventAgent (System)
 |--= 00097 root /System/Library/PrivateFrameworks/Uninstall.framework/Resources/uninstalld
 |--= 00098 root /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/FSEvents.framework/Versions/A/Support/fseventsd
 |--= 00099 root /System/Library/PrivateFrameworks/MediaRemote.framework/Support/mediaremoted
# 省略

exec

execとは、execを実行したプロセスの内容を、execに指定した内容で書き換えて実行するためのシステムコールです。 あるプロセスがexecというシステムコールを実行すると、execの内容でそのプロセス自体の内容を書き換えて実行することができます。

親プロセスから複製した子プロセスを異なる処理内容のプロセスとして実行するには、以下の手順で実現できます。

  1. forkで親プロセスを複製したプロセスを作成
  2. そのプロセスでexecを実行して、別の内容のプロセスとして書き換えて実行する

forkとexecを試しに実行してみます。

puts "forking..."

# forkメソッドを呼び出す
# 親プロセスでは子プロセスのpidが取得できる。
# 複製された子プロセスでは、pidはnilである
pid = Process.fork
p pid

# ここに来てるということは、正常にプロセスが複製された。
# この時点で親プロセスと子プロセスが *別々の環境で*
# 同時にこのプログラムを実行していることになる。
puts "forked!"

if pid.nil?
  # 子プロセスはこっちを実行する

  # execメソッドで、Rubyのプロセスを無限ループでsleepするプロセスに置き換える
  # ここで子プロセスをexecしている
  exec "ruby -e 'loop { sleep }'"
else
  # 親プロセスはこっちを実行する

  # 子プロセスが終了するのを待つ
  # 親プロセスだからpidはnilではない
  Process.waitpid(pid, 0)
end

このコードを実行すると、以下のような結果が表示されます。

ruby /Users/yuuki_haga/repos/learning/rails/rails-n-plus-1/src/sample.rb
forking...
81598
forked!
nil
forked!

psを実行した結果、確かに子プロセスが複製時とは異なる状態で実行されていることが確認できました。

ps
  PID TTY           TIME CMD
64897 ttys003    0:01.52 -zsh -g --no_rcs
81585 ttys003    0:00.12 ruby /Users/yuuki_haga/repos/learning/rails/rails-n-plus-1/src/sample.rb
81598 ttys003    0:00.10 ruby -e loop { sleep }
65703 ttys004    0:01.19 -zsh -g --no_rcs

pstreeコマンドでフォークされたプロセスも確認できるので、ちゃんとフォークされているんだなと分かります。

pstree 84073
-+= 84073 yuuki_haga ruby /Users/yuuki_haga/repos/learning/rails/rails-n-plus-1/src/sample.rb
 \--- 84086 yuuki_haga ruby -e loop { sleep }

&をつけると、バックグラウンドプロセスとして実行される

コマンドを実行する際に&をつけることで、バックグラウンドプロセスとして実行することができます。

ruby -e "sleep" &
[1] 76342

バックグラウンドプロセスとして実行することで、ターミナルを通して入力を受け付けることができなくなります。 fgを実行すればフォアグラウンドプロセスに戻せます。

www.code-magagine.com

ジョブとシェルの関係性

ジョブはシェルが管理するプログラムのグループのことです。 基本は1プログラム1ジョブです。

シェルからコマンドを叩いてプロセスを生成する場合、親プロセスはシェルのプロセスである

シェルからコマンドを叩いてプロセスを生成する場合、親プロセスはシェルのプロセスです。pstreeコマンドで実際に確認できます。

ps
  PID TTY           TIME CMD
64897 ttys003    0:01.74 -zsh -g --no_rcs
86122 ttys003    0:00.13 ruby /Users/yuuki_haga/repos/learning/rails/rails-n-plus-1/src/sample.rb
86135 ttys003    0:00.10 ruby -e loop { sleep }
65703 ttys004    0:01.67 -zsh -g --no_rcs
pstree 64897
-+= 64897 yuuki_haga -zsh -g --no_rcs
 \-+= 86122 yuuki_haga ruby /Users/yuuki_haga/repos/learning/rails/rails-n-plus-1/src/sample.rb
   \--- 86135 yuuki_haga ruby -e loop { sleep }

単体のpsコマンドとps -efコマンドの違い

単体のpsコマンドは実行中のシェルとシェルの子プロセスを表示します。複数のターミナル上でシェルを実行している場合、単体のpsコマンドを実行すると複数のシェルのプロセスと、それらの子プロセスが表示されます。

ps -efは、システム全体の全てのプロセスを表示することができます。

www.geeksforgeeks.org

プロセスとファイル入出力の関係性

プロセスに対してデータを入力したり、プロセスが処理したデータをファイルに出力したりできます。

以下の例では、ディスクにあったファイルのデータを変数に代入することで、プロセス内部のメモリ上に展開して、メモリ上に展開されたデータを別のファイルとしてディスクに出力しています。

file = File.open("nyan.txt", "r")
# ファイルのデータはもともとディスクに存在している。プロセスがもともとメモリー内に持っているものではない
# このディスクに存在しているファイルのデータを、readlinesで変数に代入することで、
# プログラム上で、ファイルをIOする際に一瞬プロセスは「ブロック中」になっている。
# プロセスの外部に存在しているディスクのデータを、プロセスの内部のメモリーに読み込んでいる
lines = file.readlines # ファイルの中身を全部読み込む
file.close

copy_file = File.open("nyan_copy.txt", "w")
copy_file.write(lines.join)
file.close

Linuxでは、全てがファイルです。もっと詳細に言うと、Linuxでは、プロセスに関する全ての入出力をファイルと同じインターフェースで扱うことができます。プロセスがターミナルからの入力を受け取りたかったり、ネットワーク越しに入力をもらって、ネットワーク越しに出力したりなどをファイルと同じインターフェースで扱えます。

ファイルディスクリプタとは

ファイルディスクリプタとは、OSが開いているファイルの識別子です。

プロセスは低レイヤーの処理(ファイルを開いたり、ディスクにデータを書き込んだり、ディスクからデータを読み取ったり等)を自分自身でするわけではなくて、プロセスがシステムコールを実行することで、OSに低レイヤーの処理を実行させています。 OSはプロセスから「ファイルを開け」というシステムコールを受け取ると、実際にファイルを開いて、そのファイルを表す識別子(ファイルディスクリプタ)を作成してプロセスに返します。プロセスはファイルディスクリプを使って、「このファイルディスクリプタで表されるファイルにこれを書き込め」というシステムコールを送ります。そうするとOSはファイルディスクリプタで表された、既に開いているファイルに対して書き込みを行います。 書き込みが終了したら、プロセスは不要になったファイルディスクリプタをcloseというシステムコールでOSに返却します。OSはファイルディスクリプタが返却されたので、「もうこのファイルは使わないのか」と判断して、ファイルを閉じます。

ここからはコード例を元に解説します。

file = File.open("kuga.txt", "w")
puts file.fileno # => 9
file.close

1行目では、openシステムコールをOSに対して送っています。正常にopenされるとファイルディスクリプタを内部に持ったfileオブジェクトが生成されます 2行目では、fileオブジェクトが保持しているファイルディスクリプタを取得してターミナルに出力します 3行目では、fileを閉じていますが、これはRubyが内部でfileオブジェクトが保持しているファイルディスクリプタを使って、OSにcloseシステムコールを送っています。

filenoを実行することでファイルディスクリプタを出力できます。 プロセスから標準入出力が指し示すファイルのファイルディスクリプタを見ることもできます。

puts $stdin.fileno
puts $stdout.fileno
puts $stderr.fileno

# => ruby std_fds.rb
# 0
# 1
# 2

標準入力、標準出力、標準エラー出力

標準入力、標準出力、標準エラー出力とは、生成されたプロセスがファイルから入力を得たり、ファイルにデータを出力をしたりする際に使う最初から用意されているファイルのことです。 標準入力、標準出力、標準エラー出力はデフォルトでターミナルが指定されています。 標準入出力も標準エラー出力も、ファイルと同じインターフェースでプロセスから操作できます。 標準入出力,標準エラー出力はデフォルトでターミナルが指定されているだけで、設定すると変更できます。

プロセスはファイルディスクリプタを指定してシステムコールを実行することで、ファイルからデータを読み取ったり、ファイルにデータを出力したりできます。プロセスは生成された時点で最初から3つのファイルディスクリプタを扱うことができて、その3つが、0(標準入力), 1(標準出力), 2(標準エラー出力)です。

例えば、リダイレクトを使うことで、標準入出力に別のファイルを指定できます。

↓ 標準出力のリダイレクト

# 標準出力にファイルを指定
# ファイルを生成しつつ。プロセスからの出力をファイルにアウトプットする
# 1を省略することもできる
ruby /Users/yuuki_haga/repos/learning/rails/rails-n-plus-1/src/print_mew.rb 1>hina.txt

↓ 標準入力のリダイレクト

file = $stdin
# IOを待っているので、プロセスが「ブロック中」になっている。
lines = file.readlines
file.close

# rubyの組み込みグローバル変数 $stdout には、「標準出力」と言われるものが、
# すでにFile.openされた状態で入っています。この「標準出力」の出力先は、デフォルトではターミナルをさします
file = $stdout
file.write(lines.join)
file.close
# 0を省略することもできる
ruby /Users/yuuki_haga/repos/learning/rails/rails-n-plus-1/src/stdout.rb 0<hina_hina.txt
mew

↓ 標準入力、標準出力どちらもリダイレクトする

stdin_file = $stdin
# IOを待っているので、プロセスが「ブロック中」になっている。
lines = stdin_file.readlines
stdin_file.close

# rubyの組み込みグローバル変数 $stdout には、「標準出力」と言われるものが、
# すでにFile.openされた状態で入っています。この「標準出力」の出力先は、デフォルトではターミナルをさします
stdout_file = $stdout
stdout_file.write(lines.join)
stdout_file.close
# プロセスがファイルを標準入力としていて、プロセスの標準出力を設定することで、プロセスのアウトプット先がファイルになる
ruby std_in_out.rb 0<hina.txt 1>hoge.txt

標準エラー出力

# プロセスからアウトプットされたエラーは、標準エラー出力に設定されたファイルに出力される。
puts "this is stdout"
warn "this is stderr" # warnは標準エラー出力に引数を出力する
ruby stdout_stderr.rb 1>out.txt 2>err.txt
# こう書くこともできる
# &をつけることで、この1は1っていう名前のファイルじゃなくてファイルディスクリプタと表すことができる
ruby stdout_stderr.rb 1>out.txt 2>&1

プロセス間通信とは

ファイルのデータをプロセスに入力したり、プロセスの処理結果をファイルに出力したり、プロセスへの入出力は基本的にはファイルを用いると思うかも知れません。しかし、それ以外にも、プロセスは、プロセス同士で通信することができます。

プロセス間通信とは、異なるプロセス間でデータを送信したり受信したりすることです。MySQLもサーバーで実行されている時は一つのプロセスであって、Railsアプリケーションもサーバーで実行されている時は一つのプロセスです。

RailsアプリケーションからMySQLのDBに対してデータをインサートしたい場合、Railsアプリケーションのプロセスと、MySQLのプロセスの間で通信(プロセス間通信)をする必要があります。プロセス間通信をすることで、データを別のプロセスに送信できたり、別のプロセスからデータを受信できたりします。プロセス間通信は大きく分けて2種類に分類できます。

  • 同一ホスト間に存在するプロセス同士でプロセス間通信する
  • 異なるホスト間に存在するプロセス同士でプロセス間通信する

2種類のプロセス間通信をどのように実装するのか、見てみましょう。

同一ホスト間に存在するプロセス同士でプロセス間通信する

メジャーな実装方法は以下の2種類です。

各実装方法について以下にまとめます。

パイプ

パイプに関しては、以下のような使い方を想定しています。

command_a | command_b

command_aのプロセスの出力結果が、commnad_bのプロセスの入力になります。 この際に、comnnad_aのプロセスの処理が完全に終わらなくても、出力があるなら、comnnad_bのプロセスに入力されます。commnad_bのプロセスは、commnad_aからの入力が来るまで、プロセスの状態が「ブロック中」になります。

以下に実行した結果を提示します。

# hoge.txt
mew
hinana
# command_a.rb
puts "start command_a"

stdin_file = File.open("hoge.txt", "r")
lines = stdin_file.readlines
stdin_file.close

stdout_file = $stdout
stdout_file.write(lines.join)
stdout_file.close
# command_b.rb
puts "start command_b"

stdin_file = $stdin
lines = stdin_file.readlines
stdin_file.close

stdout_file = $stdout
stdout_file.write([*lines, "command_b"].join)
stdout_file.close
# 実行結果
ruby command_a.rb | ruby command_b.rb
# => 
start command_b
start command_a
mew
hinana
command_b%    

UNIXドメインソケット

UNIXドメインソケットとは、ホストのファイルシステム上に存在する特殊な名前付きのソケットファイルのことです。同一ホストに存在するプロセス同士がUNIXドメインソケットを用いることで、プロセス間通信することができます。 UNIXドメインソケットを用いたプロセス間通信は、TCP/IPを使わないので非常に高速に通信できますが、プロセスが同一ホスト上に存在する必要があります。

UNIXドメインソケットでの実装を以下にまとめます。

# server.rb
require 'socket'

# hena_hoge.sockがunixドメインソケットの名前になる
socket_path = '/tmp/hena_hoge.sock'

# UNIXServer.newを実行することで、hena_hoge.sockというUNIXドメインソケット(ファイル)が生成される
server = UNIXServer.new(socket_path)
puts "サーバーが起動しました"

loop do
  # プロセスはこのUNIXドメインソケットを利用して、外部からの入力を受け付けられるようにしている
  # acceptはクライントにデータを流すためのソケットを戻り値として返す
  client = server.accept

  puts "クライアントが接続しました"

  # クライアントからのデータを読み取り、加工して返す例
  data = client.gets.chomp
  processed_data = data.upcase

  client.puts "サーバーからの応答: #{processed_data}"

  client.close
end
# client.rb
require 'socket'

socket_path = '/tmp/hena_hoge.sock' # サーバーと同じunixドメインソケットのパスを使用

client = UNIXSocket.new(socket_path)

client.puts "Hello, server!"

response = client.gets.chomp
puts response

client.close
# サーバー側のシェル
# 起動した後に、クライアントのプログラムを呼び出してサーバーのプロセスに接続している
ruby server.rb
サーバーが起動しました
クライアントが接続しました
ruby client.rb
サーバーからの応答: HELLO, SERVER!

クライアントのプロセスとサーバー側のプロセスがunixドメインソケットを用いて通信できることは分かりました。

次にUNIXドメインソケットのファイルは、ちゃんと存在しているのかを確認してみます。/tmp/hena_hoge.sockというUNIXドメインソケットのファイルは、UNIXServer.new(socket_path)を実行したタイミングで生成されます。

ll /tmp/hena_hoge.sock
srwxr-xr-x  1 yuuki_haga  wheel  0 12  3 00:35 /tmp/hena_hoge.sock

このファイルがどんな形式のファイルかをfileコマンドで確認してみます。

file /tmp/hena_hoge.sock
/tmp/hena_hoge.sock: socket

つまり、このファイルはただのファイルではなくて、UNIXドメインソケットを表すファイルであることが分かります。

このUNIXドメインソケットをどのプロセスが利用しているのかを確認してみます。指定したファイルを開いているプロセスを確認するにはlsofコマンド(list open files)を実行します。

lsof /tmp/hena_hoge.sock
COMMAND   PID       USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
ruby    85483 yuuki_haga    9u  unix 0xdb10307bb54e6e21      0t0      /tmp/hena_hoge.sock

/tmp/hena_hoge.sockはプロセスidが85483のプロセスによって開かれていることが分かります。 このプロセスidが85483のプロセスが、実は先ほど実行したサーバー側のプロセスです。

ps
  PID TTY           TIME CMD
71698 ttys000    0:01.31 -zsh -g --no_rcs
81408 ttys002    0:01.03 -zsh -g --no_rcs
64897 ttys003    0:05.33 -zsh -g --no_rcs
85483 ttys003    0:00.13 ruby server.rb

サーバー側のプロセスがどんなファイルディスクリプを持っているかはlsofコマンドにpオプションを指定することで確認できます。

lsof -p 85483
COMMAND   PID       USER   FD   TYPE             DEVICE SIZE/OFF      NODE NAME
ruby    85483 yuuki_haga    0u   CHR               16,3 0t444978     22899 /dev/ttys003
ruby    85483 yuuki_haga    1u   CHR               16,3 0t444978     22899 /dev/ttys003
ruby    85483 yuuki_haga    2u   CHR               16,3 0t444978     22899 /dev/ttys003
ruby    85483 yuuki_haga    3   PIPE 0xc0913dfbb36ea347    16384           ->0xd549b208306283b8
ruby    85483 yuuki_haga    4   PIPE 0xd549b208306283b8    16384           ->0xc0913dfbb36ea347
ruby    85483 yuuki_haga    5   PIPE 0x69ccf80e48859be3    16384           ->0x36bc210bd8e109a9
ruby    85483 yuuki_haga    6   PIPE 0x36bc210bd8e109a9    16384           ->0x69ccf80e48859be3
ruby    85483 yuuki_haga    7   PIPE 0xe5b65924fbab5eff    16384           ->0x20dd7b52143b272c
ruby    85483 yuuki_haga    8   PIPE 0x20dd7b52143b272c    16384           ->0xe5b65924fbab5eff
ruby    85483 yuuki_haga    9u  unix 0xdb10307bb54e6e21      0t0           /tmp/hena_hoge.sock

よって、サーバー側のプロセスはファイルディスクリプタ9を指定することで、UNIXドメインソケットのファイルにアクセスできて、それを用いて、クライアントのプロセスと通信しているんだなとざっくり理解することができました。

異なるホスト間に存在するプロセス同士でプロセス間通信する

メジャーな実装方法は以下の1種類です。

  • ソケット

各実装方法について以下にまとめます。

ソケット

ソケットを用いると、同一ホストや異なるホスト間でプロセス間通信することができます。しかし、同一ホストならUNIXドメインソケットで通信した方がパフォーマンスが良いので、ソケットは、異なるホストでのプロセス間通信をする際に使うのが良さそうです。

ソケットはネットワーク経由でプロセス間通信するなら必須の知識なので、詳しくは以下の記事を見ると良いでしょう。

shinpeim.github.io

ソケットもファイルと同じインターフェースで操作することができる。

Linuxでは全てがファイルなので、ソケットもファイルと同じインターフェースで操作することができます。プロセスからソケットに対して書き込んだり、ソケットからプロセスに入力したい場合、ソケットを表すfdと、read, writeのシステムコールをプロセスから実行すれば良いです。

require "socket"

# 12345 portで待ち受けるソケットを開く
listening_socket = TCPServer.open(12345)

# ソケットもファイルなので、fdがある
puts listening_socket.fileno # => 10

# とりあえず閉じる
listening_socket.close

プロセスが死ぬ(プロセスが終了する)とは

プロセスが死ぬ(プロセスが終了する)とは、そのプロセスの実行が終了することを指します。プロセスは、プログラムが実行された際に作成され、そのプログラムの実行が完了したり、エラーが発生して強制的に終了したりすると、そのプロセスは終了します(プロセスが死ぬ)

もし、プログラムの最中に親プロセスが死んだら、残された子プロセスはinitプロセス(macだとlaunchdプロセス)を親として、紐付けられます。

# kill_parent.rb
pid = Process.fork

if pid.nil?
  # exec 'ruby -e p "hello"'
  # 子プロセス
  # 親プロセスのidを取得する
  puts "親プロセスid: #{Process.ppid}"

  # 親が死ぬまで2秒まつ
  sleep 2

  # 親プロセスが死んだ後のppid
  puts "親プロセスid: #{Process.ppid}"
  sleep
else
  # 親プロセス
  sleep 1

  # rubyプログラムを終了させる
  # つまり、実行中のプロセスがなくなるので、プロセスが死ぬ
  exit
end
pstree 1
-+= 00001 root /sbin/launchd
 \--- 11837 yuuki_haga ruby kill_parent.rb
ps
  PID TTY           TIME CMD
11837 ttys003    0:00.00 ruby kill_parent.rb
64897 ttys003    0:04.37 -zsh -g --no_rcs

親プロセスにプロンプトが対応しているので、親プロセスが終了したら、子プロセスの終了に関わらずプロンプトが入力可能状態になります。

ゾンビプロセス

子プロセスが実行終了しているにもかかわらず、親プロセスに wait されないとプロセスが回収されず、ゾンビプロセスとして残ってしまいます。 以下のコード例を見てみましょう

pid = Process.fork

if pid.nil?
  # 子プロセス
  # 子プロセスは即死する
  exit
else
  # 親プロセス
  # 子プロセスのpidを出力
  puts pid
  loop do
    sleep
  end
end

子プロセスが即死しているのに、親プロセスがそれに気づかないでずっと動いていると、子プロセスが成仏できずにゾンビ化します。STATがZだと、ゾンビプロセスを意味します。

ps 13232
  PID   TT  STAT      TIME COMMAND
13232 s003  Z+     0:00.00 <defunct>

ゾンビプロセスの問題点は、ゾンビプロセス自体の処理は完了しているのに、ゾンビプロセスがずっと存在し続けるせいでメモリなどのコンピュータリソースを無駄に消費してしまうことです。

親プロセスと子プロセスの処理が終了したパターンについてまとめると、

  • 親プロセスの処理が完了、子プロセスの処理が完了すると、どちらのプロセスも終了する。
  • 親プロセスの処理が続いている、子プロセスの処理が完了している、しかし親プロセスで子プロセスの終了を検知しないと、子プロセスがゾンビプロセスになる。
  • 親プロセスの処理が終了、子プロセスの処理が続いている場合、子プロセスはinitプロセス(launchdプロセス)の養子になる。

pstree -p pidで、pidの親プロセスを特定できます。

pstree -p 27938
-+= 00001 root /sbin/launchd
 \--- 27938 yuuki_haga ruby parent.rb

シグナルとは

シグナルとは、プロセスに送る信号のようなものです。 シグナルもプロセス間通信の一つのようです。

シグナル (Unix) - Wikipedia

プロセスはプロセス間通信やファイルディスクリプタを通じて外界と入出力のやり取りをする以外に、シグナルを利用して外界とやりとりします。

プロセスにシグナルを送るためには、killコマンドを使います。killはプロセスに対してシグナルを送るためのコマンドです。

ruby -e "loop { sleep }" &
kill -INT 15663
Traceback (most recent call last):
    3: from -e:1:in `<main>'
    2: from -e:1:in `loop'
    1: from -e:1:in `block in <main>'
-e:1:in `sleep': Interrupt
[1]  + 15663 interrupt  ruby -e "loop { sleep }"

killコマンドでSIGINTというシグナルをrubyプロセスに送ったことで、rubyのプロセスが死んだことが確認できます(killでSIGINTシグナルを指定するときは、SIGを書かなくて良い)。 SIGINTシグナルをプロセスが受け取ると、デフォルト値では、そのプロセスは実行を停止します。

SIGINTシグナルをプロセスが受け取った時に、挙動を変更したいなら、Signalモジュールのtrapメソッドを使います。

trap("INT") do
  warn "ぬわーーーーっっ!!";
end

loop do
  sleep;
end

上のコードを実行した場合、psで以下のように表示されます。

ps
  PID TTY           TIME CMD
30463 ttys000    0:00.11 ruby papas.rb
71698 ttys000    0:02.61 -zsh -g --no_rcs
81408 ttys002    0:01.41 -zsh -g --no_rcs
64897 ttys003    0:05.34 -zsh -g --no_rcs

killコマンドで、プロセスid 30463のプロセスにSIGINTシグナルを送ってみます。

kill -INT 30463

そうするとプロセスを実行してたシェルで以下のように表示されます。

ruby papas.rb
ぬわーーーーっっ!!

SIGINTシグナルではこのプロセスは終了させられないので、SIGTERMシグナルを送ってみます。そうすると、プロセスを終了させることができました。

kill -TERM 30463

最後に、主要なシグナルについてまとめておきます。

  • SIGINTシグナルは、プロセスの実行に対して割り込みをかけるシグナル。
  • SIGTERMシグナルは、プロセスの実行を終了するシグナル
  • SIGKILLシグナルは、プロセスを強制終了するシグナル

プロセスグループ

プロセスは、必ず一つのプロセスグループというものに所属しています。

ps -o pid,pgid,command
  PID  PGID COMMAND
13247 13247 -zsh -g --no_rcs
19375 19375 ruby -e sleep
64897 64897 -zsh -g --no_rcs

子プロセスは、親プロセスと同じプロセスグループに所属します。

# fork.rb
Process.fork

sleep
ps -o pid,pgid,command -f
  PID  PGID COMMAND            UID  PPID   C STIME   TTY           TIME
13247 13247 -zsh -g --no_rcs   501   572   0 11:01PM ttys000    0:02.24
20540 20540 ruby fork.rb       501 64897   0 11:42PM ttys003    0:00.14
20553 20540 ruby fork.rb       501 20540   0 11:42PM ttys003    0:00.00
64897 64897 -zsh -g --no_rcs   501   572   0  3:11PM ttys003    0:04.86

プロセスグループには、リーダーが存在していて、そのリーダーは、PIDとPGIDが同じプロセスです。

プロセスグループのメリットは、killコマンドでプロセスグループを指定することで、プロセスグループに所属しているプロセスを一気に全て消すことができることです。kill で pid を指定する部分に、"-" を付けてあげると、pid ではなくて pgid を指定したことになります。

ps -o pid,pgid,command -f
  PID  PGID COMMAND            UID  PPID   C STIME   TTY           TIME
13247 13247 -zsh -g --no_rcs   501   572   0 11:01PM ttys000    0:02.31
20540 20540 ruby fork.rb       501 64897   0 11:42PM ttys003    0:00.14
20553 20540 ruby fork.rb       501 20540   0 11:42PM ttys003    0:00.00
64897 64897 -zsh -g --no_rcs   501   572   0  3:11PM ttys003    0:04.86
kill -INT -20540
ps -o pid,pgid,command -f
  PID  PGID COMMAND            UID  PPID   C STIME   TTY           TIME
13247 13247 -zsh -g --no_rcs   501   572   0 11:01PM ttys000    0:02.39
64897 64897 -zsh -g --no_rcs   501   572   0  3:11PM ttys003    0:04.87

並列処理について

並列処理を行うには以下の2つの方法があります。

  • 子プロセスを複製する
  • プロセス内のスレッドを複製する

ここは後々深ぼろうと思います。

moro-archive.hatenablog.com

参考記事

プログラムの基本をざっくりまとめてみる

なるほどUNIXプロセスを読んでいく #Ruby - Qiita

【Linux カーネル: OS 基礎入門2】CPU、プロセス管理 | ほげほげテクノロジー

「UNIX ドメインソケット」と「ソケット」について比較する #UNIX - Qiita

UNIXドメインソケット - Wikipedia

Goメモ-103 (Go で Unix domain socket (AF_UNIX) のメモ) - いろいろ備忘録日記

UnicornとNginxの接続方法は、UNIXドメインソケットとリバースプロキシの2つの方法がある - kasei_sanのブログ

UNIXドメインソケットとは - 意味をわかりやすく - IT用語辞典 e-Words

ソケット通信の仕組みをスライド図解と Go 実装でまとめてみる

unixドメインソケット - すな.dev

イケてるエンジニアになろうシリーズ 〜メモリとプロセスとスレッド編〜 - もろず blog

https://www.info.kindai.ac.jp/OS/lecture/OS03note.pdf

【メモ】AWS EC2上に立てたRailsアプリでサイズの大きいファイルをアップロードすると 502 Bad Gateway となった話|石灰

Linux メモリ不足で発生するOOM Killerによるプロセスの突然死の確認&回避方法|ITの魔力

process-book

Rubyでプロセスとスレッドを学ぶ

親プロセスのPIDを調べる - hosiimo_kt’s 備忘録

ゾンビプロセスとは - 意味をわかりやすく - IT用語辞典 e-Words