Fire Engine

化学の修士号→消防士→ITエンジニア(2016年11月〜)

Pythonで学ぶUnixプロセスの基礎

 先日、自身が主催するPyFukuokaというイベントにて「Pythonで学ぶUnixプロセスの基礎」というタイトルの発表をしました。スライドはこちらです。

speakerdeck.com

 内容としては「なるほどUnixプロセス」という本を参考にしています。本書は、Unixの基礎から、ファイルディスクリプタの話や、fork(2)を使って子プロセスを生成する方法など、非常に内容が濃く、超オススメです。本の中ではRubyを使って解説されていますが、私の発表では、この本の一部をPythonを使ってまとめました。
 以下、スライドの内容に沿って書いていきます。

動作環境

プロセスにはIDがある

プロセスはプロセスIDという値を持っています。プロセスIDはプロセスの中身とは関連づいていない単に連番になった数値のラベルです。
それではPythonインタプリタを起動して、プロセスに振られたIDを確認してみます。
(補足:Pythonでは「os」という標準ライブラリを利用することで、OS関連の機能を利用することができます。 )

$ python
>>> import os
>>> os.getpid()
1229

「1229」という数値が返ってきました。ターミナルの別ウィンドウでpsコマンドをたたくと、

$ ps -p 1229
  PID TTY           TIME CMD
 1229 ttys000    0:00.06 python

確かにPIDが1229でPythonのプロセス立ち上がっていることがわかります。(PIDはプロセスIDのこと)

プロセスには親がいる

すべてのプロセスには親となるプロセス(親プロセス)がいます。たいていの場合、親プロセスはそのプロセスを起動したプロセスとなります。
Pythonインタプリタを起動して、親プロセスのIDを確認してみます。

$ python
>>> import os
>>> os.getppid()
1165

親プロセスを確認する関数はgetppidで、pが一つ増えていることに注意してください。(parentのp)
Pythonプロセスの親はそれを起動しているbashプロセスになります。

プロセスは子プロセスを作れる

fork(2)システムコールを使うと、実行中のプロセスから新しいプロセスを生成できます。システムコールは、OSの機能を呼び出すための仕組みのことです。
以下、fork(2)システムコールの特徴を挙げます。

  • fork(2)を呼ぶ側のプロセスは「親プロセス」、生成されるプロセスは「子プロセス」となる。
  • 子プロセスは、親プロセスで使われているすべてのメモリのコピーと、親プロセスが開いているファイルディスクリプタを引き継ぐ。
  • 子プロセスがコピーしたメモリは、親プロセス側に影響を与えることなく自由に変更ができる。

それでは、fork(2)を使って、プロセスを生成してみましょう。Pythonからfork(2)を呼ぶためにもosライブラリを利用します。

import os

if os.fork():
    print("Here in the if block.")
else:
    print("Here in the else block.")

上記のプログラムに「fork.py」というファイル名をつけて実行すると、

$ python fork.py
Here in the if block.
Here in the else block.

if句の中もelse句の中もprint文が実行されており、すごく不思議な現象です。
なにが起きてるのか追ってみましょう。

forkの挙動を追ってみる

forkの挙動を追うためにif句・else句それぞれで、以下の2点を調べます。

  1. プロセスIDはどうなっているか
  2. fork関数の返値はどうなっているか
import os

print("---- before fork ----")
print("pid: {0}".format(os.getpid()))
ret = os.fork()

if ret:
    print("----- if block ------")
    print("pid: {0}".format(os.getpid()))
    print("return value: {0}".format(ret))
else:
    print("---- else block -----")
    print("pid: {0}".format(os.getpid()))
    print("return value: {0}".format(ret))

上記のプログラムに「fork_detail.py」というファイル名をつけて実行すると、下のような結果になります。

$ python fork_detail.py
---- before fork ----
pid: 1103
----- if block ------
pid: 1103
return value: 1104
---- else block -----
pid: 1104
return value: 0

まず、プロセスIDについて見てみると、forkする前(プロセスを生成する前)とif句の中が同じプロセスIDで、else句の中はif句に1足したプロセスIDを持っていることがわかります。これは、if句は親プロセス(プロセスID:1103)によって実行されており、else句はfork関数によって生成された子プロセス(プロセスID:1104)によって実行されていることを意味しています。
次に、fork関数の返値を見てみると、親プロセス側には「生成した子プロセスのID」を返し、子プロセス側には「0」を返しています。Pythonの条件式では、正の整数値はTrue、0はFalseと評価されるため、この返値によって、親プロセスはif句の中、子プロセスはfalse句の中と実行内容を分けて記述することができます。

プロセスは通信できる

複数のプロセス間でデータをやりとりするための仕組みのことをプロセス間通信(IPC: Inter Process Communication)と呼びます。 IPCには様々な手法がありますが、今回は比較的単純な「パイプ」を使った方法を紹介します。
パイプは親子関係にあるプロセス間の単方向の通信を実現します。
まず、パイプの概略図を示します。(参考リンク

f:id:hirotsuru314:20171224144448p:plain

forkによって生成された子プロセスは、親プロセスが開いているファイルディスクリプタを引き継ぎます。ファイルディスクリプタとは、OSがカーネルのレイヤーで用意している抽象化の仕組みです。Unixの世界ではファイルシステム上のファイルだけでなく、デバイス、ソケット、パイプなどもファイルとして扱われるため、ファイルディスクリプタが割り振られます。
プロセスがパイプを作ったあとでforkを呼び出した場合、生成された子プロセスは同じパイプを読み書きできるため、親子間でデータを渡すことができます。

import os
reader, writer = os.pipe()
if os.fork():
    os.close(reader)
    write_pipe = os.fdopen(writer, 'w')
    write_pipe.write('Hello child!')
    write_pipe.close()
else:
    os.close(writer)
    read_pipe = os.fdopen(reader, 'r')
    message= read_pipe.readline()
    read_pipe.close()
    print(message)

上記のプログラムに「pipe.py」というファイル名をつけて実行すると、

$ python pipe.py
Hello child!

親から子にメッセージを渡せていることがわかります。

まとめ

プロセスは、IDがあって、親がいて、子を作れて、通信ができます。そして、その様子をPythonから確認できます。今回紹介した内容は、特にPythonである必要はありません。しかし、Pythonなどの高級言語を使って、OS周りの理解を深めることは、学び方の一つのアプローチとして有効だと私は考えます。
なるほどUnixプロセス」という本には今回の内容以外にも様々なプロセスの特徴について解説しています。 下記がその例です。

  • 孤児プロセス
  • ゾンビプロセス
  • デーモンプロセス
  • ソケット通信

この辺りに興味がある方はぜひ本を読まれることをオススメします!