はじめに
なるほどUnixプロセス ― Rubyで学ぶUnixの基礎 という本を読みました。プロセスという概念を Ruby のコードを用いて説明してくれます。非常に分かりやすい本で感動しました
復習を兼ねて書籍中の一部の Ruby コードを Python に置き換えたいです。せっかくなので、一番実用的だと思ったプロセスのデーモン化を実装しました。
コード
from datetime import datetime
import os
import sys
import time
def daemonize():
"""
プロセスをデーモン化する。
"""
def fork():
if os.fork():
sys.exit()
def throw_away_io():
stdin = open(os.devnull, 'rb')
stdout = open(os.devnull, 'ab+')
stderr = open(os.devnull, 'ab+', 0)
for (null_io, std_io) in zip((stdin, stdout, stderr),
(sys.stdin, sys.stdout, sys.stderr)):
os.dup2(null_io.fileno(), std_io.fileno())
fork()
os.setsid()
fork()
throw_away_io()
def create_empty_file():
"""
現在時刻をファイル名としたテキストファイルを出力する。
ファイルにはこのファイルを出力したプロセス pid が入力される。
"""
now = datetime.now().strftime('%Y%m%d%H%M%S')
filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)),
f'tmp/{now}.txt')
with open(filepath, 'w') as f:
f.write(str(os.getpid()))
def create_empty_files(interval=60):
"""
interval 秒ごとに create_empty_file() を呼び出す。
"""
while True:
create_empty_file()
time.sleep(interval)
if __name__ == '__main__':
daemonize()
create_empty_files()
daemonize() の説明
最初の os.fork() について
上記のコードでは fork() を 2 度呼んでいます。
def fork()
if os.fork():
sys.exit()
os.fork() では親プロセスでは子プロセスの pid が返り、子プロセスでは 0 が返ります。つまり、親プロセスは終了し、子プロセスは存続します。
os.setsid() について
os.setsid() はその名の通り setsid() システムコールを呼び出します。そして setsid() は次の 3 つの処理を行います1。
- 新しいセッションを作成する。
- 呼び出したプロセスを新しいセッションのリーダーにする。
- プロセスを制御端末から切り離す。
これにより os.setsid() を呼び出した子プロセスは、新しいセッション (とそのセッションの新しいプロセスグループ) のリーダーとなり、なおかつ制御端末を持たなくなります。ただし、技術的には制御端末をセッションに再設定することが可能だそうです2。
2 度目の os.fork() について
2 度目の os.fork() で、さきほどセッションとプロセスグループのリーダーとなったプロセスが終了し、新しい子プロセスが生成されます。終了したセッションリーダーは制御端末を持たず、なおかつ新しい子プロセスはセッションリーダではないので、これで絶対に制御端末を持たないことを保証できます。デーモンプロセスの完成です。
throw_away_io() について
def throw_away_io():
stdin = open(os.devnull, 'rb')
stdout = open(os.devnull, 'ab+')
stderr = open(os.devnull, 'ab+', 0)
for (null_io, std_io) in zip((stdin, stdout, stderr),
(sys.stdin, sys.stdout, sys.stderr)):
os.dup2(null_io.fileno(), std_io.fileno())
標準ストリームをすべて /dev/null
に送ることで捨ててしまいます。これデーモンプロセスは制御端末を持たないので、これらのリソースはもはや必要ないからです。ここで標準ストリームを close するのではなく /dev/null
に送るるようにしているのは、標準ストリームの利用を想定した外部プログラムを考慮して、外部からは依然として標準ストリームが利用可能であるように見せるためだそうです。
実行結果
$ python main.py
$ ps aux | grep -v grep | grep python
quanon 4621 0.0 0.0 2418360 2996 ?? S 11:46PM 0:00.00 python main.py
$ cat tmp/20170520234627.txt
4621⏎
手作りのデーモンプロセスがバックグラウンドで動き続けております
さいごに
気が向いたら python-daemon のコードを読みたい
参考
書籍
Web
-
呼び出したプロセスがプロセスグループリーダーでないことを前提としています。 ↩
-
書中では詳細は言及されていません。Unixデーモンの仕組み という記事では「昔の System V 系ではセッションリーダーのプロセスに後から制御端末を結びつける事ができたため、歴史的な理由でやっています。」と説明されています。 ↩