はじめに
「Linuxのしくみ 増強改訂版」の第2章の要点をメモしていきます。まだ理解が追いついていない部分もあるので、書籍を進めていく中で本記事をアップデートしていきたいと思います。
プロセス生成の目的
新しくプロセスを生成する目的は2つあります。
同じプログラムの処理を複数のプロセスに分岐して処理する
新しいプロセスを生成するために、内部的にclone()というシステムコールを発行するfork()関数を使用します。イメージとしては、親プロセスのコピーとして子プロセスを生成します。具体的な流れは以下になります。
- 親プロセスがfork()関数を呼び出す。
- 子プロセス用のメモリ領域が確保され、そこに親プロセスのメモリがコピーされる。正確には、コピーオンライトという機能を使用してメモリのコピーを行う。
- 親プロセスと子プロセスがfork()関数から復帰する。以下のfork.py内で示すように親プロセスと子プロセスでfork()関数の戻り値(ret変数)が異なるため、処理の分岐が可能となる。
書籍p.22のfork.pyで具体例を見てみます。
import os, sys
# os.fork()で子プロセスを生成する。
# 親プロセスは子プロセスのIDを、子プロセスは0を返す。
ret = os.fork()
# ret==0の場合は、生成された子プロセスの処理が実行される。
# getpid: 現在のプロセス (getpidを呼び出しているプロセス)のIDを取得。
# getppid: 現在のプロセスの親プロセスのIDを取得。
if ret == 0:
print("子プロセス: pid={}, 親プロセスのpid={}".format(os.getpid(), os.getppid()))
exit()
# ret>0の場合は、親プロセスの処理が実行される。
elif ret > 0:
print("親プロセス: pid={}, 子プロセスのpid={}".format(os.getpid(), ret))
exit()
sys.exit(1)
fork()関数の説明に関しては、Python | os.fork() methodという記事の説明が分かりやすかったです。
私の環境ではこのような結果が得られました。
./fork.py
親プロセス: pid=25881, 子プロセスのpid=25882 # 親プロセスの実行結果
子プロセス: pid=25882, 親プロセスのpid=25881 # 子プロセスの実行結果
ID:25881の親プロセスが分岐して、ID:25882の子プロセスが新たに生成されます。それらの処理はfork()関数の戻り値により条件分岐が可能となり、それぞれのプロセスの処理が実行されています。
別のプログラムを生成する
こちらの目的でプロセスを生成する場合は、fork()関数と内部的にexecve()というシステムコールを呼び出すexecve()関数を使用します。上の例のようにfork()関数で子プロセスを生成した後、子プロセス上でexecve()関数を実行することで、子プロセスを別のプログラムに置き換えることができます。具体的な流れをまとめます。
- execve()関数を呼び出す。
- execve()関数の引数で指定した実行可能ファイルを読み出す。
- 現在のプロセス(子プロセス)のメモリを新しいプログラムのデータに書き換える。
- 新プロセスのエントリポイントから実行開始する。
書籍p.24のfork-and-exec.pyの例を見てみます。
import os, sys
ret = os.fork()
if ret == 0:
print("子プロセス: pid={}, 親プロセスのpid={}".format(os.getpid(), os.getppid()))
# execve(path, args, env)
# 引数リストargsと環境変数envを渡して、pathにある実行可能ファイルを起動し、
# 現在のプロセスを書き換える。
os.execve("/bin/echo", ["echo", "pid={} からこんにちは".format(os.getpid())], {})
exit()
elif ret > 0:
print("親プロセス: pid={}, 子プロセスのpid={}".format(os.getpid(), ret))
exit()
sys.exit(1)
execve()関数については、8.1 組み込みモジュール posixを参考にしました。
私の環境では以下の結果が得られました。子プロセスの処理がexecve()関数によって書き換えられ、echo(文字列を表示するコマンド)が実行されていることが分かります。
./fork-and-exec.py
親プロセス: pid=25884, 子プロセスのpid=25885
子プロセス: pid=25885, 親プロセスのpid=25884
pid=25885 からこんにちは
全てのプロセスの先祖であるinitプロセス
コンピュータの電源を入れてから、プロセスが生成されるまでの流れは以下の記事の「OSが起動する仕組み」を参考にしてください。
親プロセスが子プロセスを生成することで、プロセスを新たに生成することができます。全てのプロセスの親プロセスをたどっていくと最終的にはinitプロセスにたどり着きます。
プロセスの状態と終了
プロセスは生成されてから、実行可能状態、実行状態、スリープ状態などさまざまな状態を行き来します。プロセスは内部的にexit_group()というシステムコールを呼ぶexit() 関数の実行によりゾンビ状態になり(カーネルがプロセスが使用していたメモリなどのリソースを解放する)、その親プロセスがwait()等のシステムコールを呼び出すことで、子プロセスの終了状態を確認し子プロセスが終了します。
親プロセスが終了した孤児プロセスやゾンビプロセスの新たな親はinitになり、wait()などのシステムコールを発行することでプロセスの終了を確認し、リソースを回収することができます。
シグナル
シグナルは、あるプロセスが他のプロセスやプロセスグループに対してイベントを通知して、外部から強制的に処理の流れを変更するための仕組みです。例えば、割り込みキーであるctrl+cでSIGINTというシグナルを送信することで、これを受け取ったプロセスはそのまま終了します。
セッションとプロセスグループ
セッションとプロセスグループにより、シェルのジョブ管理(バックグラウンドで実行するプロセスを制御する仕組み)が可能となります。あるプロセス内には、フォアグラウンドプロセスグループとバックグラウンドプロセスグループが存在します。セッションの端末にアクセスできるのはセッション内に一つだけ存在するフォアグラウンドプロセスグループのみで、バックグラウンドプロセスグループはfgコマンドでフォアグラウンドジョブにすることで、端末にアクセスできるようになる。
デーモン
デーモンとは、普通のプロセスとは異なりシステムの開始から終了まで存在し続けるプロセスです。以下がデーモンの特徴になります。
- デーモンの親は、デーモンを生成したプロセスではなく、initになる。
- ログインセッションが終了しても影響を受けないように、独自のセッションを持つ。
- 端末からの入出力が必要ないので、端末が割り当てられない。
ここでの「端末」の意味が分かりにくいので、書籍から引用させていただきます。
bashなどのシェルを介してコマンドを実行するための、文字だらけの白黒画面、あるいはウィンドウ
制御端末は、英語ではcontrolling terminalと訳され、Linuxのプロセスが開始される端末です。
【番外編】
シェルとは?
シェルとは、私たちがキーボードで入力したコマンドをOS(Linuxカーネル)に伝えるためのプログラムで、ユーザーとOSのインターフェースを提供します。シェルは、私たちが入力したコマンドをカーネルが理解できる命令に変換します。また、ユーザーがターミナルにログインまたはターミナルを開始した時に、シェルが起動します。
ターミナルとは?
端末エミュレータと呼ばれるプログラムのことで、ウィンドウを表示させることでユーザーがシェルとやり取りすることを可能にします。具体的には、ユーザーがターミナルにコマンドを入力すると、その結果をターミナルで確認することができます。
シェルとターミナルについては以下の記事を参考にしました。