Pythonからの別プロセスの停止に関してのメモ【備忘録】

お疲れ様です。

最近の実装でPythonの標準モジュールsubprocessを使って立ち上げたプロセスをPython自体の機能を使って停止させる処理を書く必要があり、いろいろと苦労したのでメモを残しておこうと思います。
直近で調べた内容なので間違い等あるかもしれません。その場合ご指摘いただけますと幸いです。

実際の立ち上げはsubprocess.Popenを使って独立したプロセスを立ち上げています。

process = subprocess.Popen(["python", "example.py"], shell=True)


Pythonで別プロセス停止をする機能について

subprocessモジュール自体のメソッドを使用する

subprocessを使って立ち上げたプロセスなのでそれ自体が持つメソッドを使って停止することが可能です。 subprocess.Popenの返り値のオブジェクトに対して.terminate().kill()を使用します。

terminateは優しい終了といわれるようで、プロセス側に終了するためのシグナルを渡して終了を促すというものらしいです。(この辺は詳しくありません…。) 一方killの方はプロセスをその場で強制終了させる形になります。

process = subprocess.Popen(["python", "example.py"], shell=True)

process.terminate()  
process.wait()  

os.kill()やos.killpg()を使用する

Pythonの標準モジュールosを使って停止させることもできます。kill()またはkillpg()を使用します。
こちらの場合、停止させたいプロセスのIDが必要になります。subprocess.Popenの場合は返り値のオブジェクトから取得できます。
シグナルは実行しているOSによって違いがあるので注意(後述)。下記の例はLinux用です。

process = subprocess.Popen(["python", "example.py"], shell=True)

os.killpg(process.pid, signal.SIGTERM)

psutilを使用する

psutilを使えばさらに細かく指定することができます。こちらは個別にインストールする必要があります。
実行環境のOSに依存しないライブラリな点が他より優れていると思います。(実際のコードではこちらを採用しました。)
停止させたいプロセスのIDが必要になります。

pip install psutil

psutil.readthedocs.io

process = subprocess.Popen(["python", "example.py"], shell=True)

# psutil でプロセス取得
ps_proc = psutil.Process(process.pid)

ps_proc.terminate()

シグナルについての注意点

subprocessのメソッドやosモジュールを使用する場合、シグナルを指定することがあります。
基本的には実行環境のOSがLinux系の場合に使用するもののようです。 一部Windowsでも使用できるようですが、サポートされていなかったり、機能しなかったりすることがほとんどです。 詳しくはドキュメントを参照ください。

docs.python.org

subprocess.Popenでshell=Trueを指定したときの注意点

shell=Trueを指定することでプロセスがシェル(sh や cmd.exe)経由で起動されるという違いがあります。 今回はPythonの処理上からPythonコードを別プロセスで立ち上げているのですが、立ち上げ元のPython環境と同じ環境で別プロセスを立ち上げることができるということで指定していました。

ただ、この場合だと立ち上げたプロセスからさらにプロセス(子プロセス)が立ち上がることがあり、普通に停止させるだけだと子プロセスが残る可能性があるので、大元のプロセス(親プロセス)から子プロセスが無いか調べてそれらも止めるという形をとります。
psutilでは子プロセスを取得することができるのでこれを利用して派生したすべてのプロセスを止めます。

子プロセスからさらに別のプロセス(孫プロセスなど)が立ち上がっている可能性がある場合は、下記の処理を関数化して再起処理などを使うのが良さそう。

process = subprocess.Popen(["python", "example.py"], shell=True)

proc = psutil.Process(process.pid)

# 子プロセスを調べて停止
for child in proc.children(recursive=True):
    child.terminate()

# 親プロセスを停止
proc.terminate()