それマグで!

知識はカップより、マグでゆっくり頂きます。 takuya_1stのブログ

習慣に早くから配慮した者は、 おそらく人生の実りも大きい。

python でコマンド実行。サブプロセスの終了待ち・強制終了・親プロセスと一緒に殺す。

python でコマンドを実行するには

subprocess モジュールを使う

以前にも書いたんだけど、気になったので、再度調べ直した。

suprocessでコマンドを実行する

単純にコマンドを実行するには、subprocess.call を使うのが楽ですね バッククォート ` やos.system のかわりに subprocess .call() を使うようです。

import subprocess
cmd = "sleep 30"
proc = subprocess.call( cmd , shell=True)

実行した結果はこちら。(シェル経由)

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
takuya   16804  0.0  0.0  20960  4076 pts/4    Ss   03:49   0:00 /bin/bash
takuya   16864  1.0  0.0  23572  4864 pts/4    S+   03:49   0:00  \_ python proc.py
takuya   16865  0.0  0.0   4180   580 pts/4    S+   03:49   0:00      \_ /bin/sh -c sleep 30
takuya   16866  0.0  0.0   5152   584 pts/4    S+   03:49   0:00          \_ sleep 30

shell=True を渡したので、シェルsh -c 'sleep 30' が実行されている。

この場合は、終了待ちをしています。call の部分でブロックされます。

PythonをCtrl+Cで止めたら、sh も一緒に止まりました。

シェル経由をしない場合

今度は、シェル経由をせずに、直接プロセスを起動する場合。

import subprocess
cmd = "sleep 30"
proc = subprocess.call( cmd .strip().split(" ") )

実行中のプロセスのツリーはこちら。

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
takuya   16804  0.0  0.0  21112  4164 pts/4    Ss   03:49   0:00 /bin/bash
takuya   16963  0.0  0.0  23572  4864 pts/4    S+   03:53   0:00  \_ python proc.py
takuya   16964  0.0  0.0   5152   584 pts/4    S+   03:53   0:00      \_ sleep 30

シェル経由ではないので、コマンドとコマンド引数を配列で渡しています。

終了待ちをしています。callでブロックされます。Ctrl+Cで終了したら、sleep も殺してくれます。

Popen を用いる場合。

os.spawn の代わりに最近では Popenを用いるべきらしいです。

popen には使い方がいくつかあります。

単純に呼び出した場合。

import subprocess
from subprocess import Popen
from time import  sleep
cmd = "sleep 30"
proc = Popen( cmd .strip().split(" ") )

単純に呼び出した場合のプロセスツリーです。

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
takuya   16804  0.0  0.0  21112  4172 pts/4    Ss   03:49   0:00 /bin/bash
takuya   17038  0.0  0.0   5152   584 pts/4    S    03:58   0:00 sleep 30

単純なPopenは終了待ちをせずにサブプロセスを起動するだけです。

親プロセスのPythonはsleep の終了待ちをせずに、sleepより先に終了してしまい、孤児プロセスとなったsleep はroot プロセス(init)に引き取られています。

import subprocess
from subprocess import Popen
from time import  sleep
cmd = "sleep 30"
proc = Popen( cmd .strip().split(" ") )
sleep(5)

time.sleep を使って、起動直後の状態を確認してみます。

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
takuya   16804  0.0  0.0  21112  4172 pts/4    Ss   03:49   0:00 /bin/bash
takuya   17044  0.1  0.0  23572  4864 pts/4    S+   03:58   0:00  \_ python proc.py
takuya   17045  0.0  0.0   5152   584 pts/4    S+   03:58   0:00      \_ sleep 30

起動直後では、親プロセスがPythonで子プロセスにsleep がちゃんと入っています。

起動後、親プロセスのPythonが先に終了するので 、子プロセスのsleep が孤児になるわけですね。

Popen+シェル経由で、サブプロセスを起動する。

今度は、シェル経由でサブプロセスを起動して、Pythonが終了待ちをしないPopenを見てみます。

import subprocess
from subprocess import Popen
from time import  sleep

cmd = "sleep 30"
proc = Popen( cmd,shell=True )

Pythonが先に終了するので、sh -c が孤児プロセスとして引き取られていました。

takuya@atom:~$ ps uxf
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
takuya   16718  0.0  0.0  94388  1876 ?        S    03:49   0:00 sshd: takuya@pts/0
takuya   16719  0.0  0.0  22816  6012 pts/0    Ss   03:49   0:00  \_ -bash
takuya   16802  0.0  0.0  22656  1176 pts/0    S+   03:49   0:00      \_ screen
takuya   17138  0.0  0.0   4180   580 pts/4    S    04:04   0:00 /bin/sh -c sleep 30
takuya   17139  0.0  0.0   5152   584 pts/4    S    04:04   0:00  \_ sleep 30

シェル経由なので sh -c が出てきてます。

こちらも、起動後、親プロセスのPythonが先に終了するので 、子プロセスのsleep が孤児になるわけですね。

そのままでは終了を待ち合わせしません。

終了待ちをする Popen#wait を使う。

popen = Popen( cmd,shell=True )
popen.wait()

これで、Waitできる。でもPopenってなに?

Popen オブジェクトについて

Popenによってコマンドを実行し、サブプロセスを起動した場合はPopenオブジェクトが返却される。

Popenオブジェクトのインスタンスは、process ID や STDIN/STDOUT などを変数に持つプロセスを抽象化したオブジェクトのようです。

import subprocess
from subprocess import Popen
from time import  sleep

cmd = "sleep 30"
proc = Popen( cmd,shell=True )
print( "process id = %s" % proc.pid )

実行結果は次のようになっていて

takuya@atom:~$ python proc.py
process id = 17216

実行後のプロセスは次のようになっていました。

takuya@atom:~$ ps uxf
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
takuya   16718  0.0  0.0  94388  1876 ?        S    03:49   0:00 sshd: takuya@pts/0
takuya   16719  0.0  0.0  22816  6012 pts/0    Ss   03:49   0:00  \_ -bash
takuya   16802  0.0  0.0  22656  1176 pts/0    S+   03:49   0:00      \_ screen
takuya   17216  0.0  0.0   4180   580 pts/4    S    04:09   0:00 /bin/sh -c sleep 30
takuya   17217  0.0  0.0   5152   580 pts/4    S    04:09   0:00  \_ sleep 30

これから、間違いなくサブプロセスのプロセスIDを取得できていることが分かりました。

Popenオブジェクト便利ですね。

Popen#waitをしながら、Ctrl+Cを押した。

subprocess.call と同じように終了待ちをPopenで実現するには、 Popen#waitを使う。

Ctrl+Cでpythonを止めると、起動した子プロセスも一緒に死んでくれて便利。

孤児プロセスが出ないのは嬉しい。

import subprocess
from subprocess import Popen
from time import  sleep

cmd = "sleep 30"
proc = Popen( cmd,shell=True )
print( "process id = %s" % proc.pid )
proc.wait()

ただし、Pythonが面倒見てくれるのはCtrl+Cを押した時だけ。

別のターミナルから、SIGINTを送信したら。

takuya@atom:~$ kill -INT 17343
takuya@atom:~$ ps uxf
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
takuya   17354  0.0  0.0  16388  1252 pts/5    R+   04:17   0:00  \_ ps uxf
takuya   16804  0.0  0.0  21268  4252 pts/4    Ss+  03:49   0:00 /bin/bash
takuya   16718  0.0  0.0  94388  1876 ?        S    03:49   0:00 sshd: takuya@pts/0
takuya   16719  0.0  0.0  22816  6012 pts/0    Ss   03:49   0:00  \_ -bash
takuya   16802  0.0  0.0  22656  1176 pts/0    S+   03:49   0:00      \_ screen
takuya   17344  0.0  0.0   4180   580 pts/4    S    04:16   0:00 /bin/sh -c sleep 30 ## initに引き取られた孤児プロセス
takuya   17345  0.0  0.0   5152   584 pts/4    S    04:16   0:00  \_ sleep 30

TERM も INT も HUP も孤児が出来た。キーボードを押したら孫まで消してくれるのに。。。Ctrl+C押した場合とシグナル送信では若干動きが違う?

この辺は理由がわからないけど、ruby もそうだったので、Linuxのシグナルとプロセス管理をやっぱり勉強しなおしだね。

サププロセスを終了する Popen#terminate

teminate() を使うとSIGTERMシグナルが送信進されて、プロセスが終了される。

これもきちんと終了してくれる。

一定時間経過したらプロセスを終了するには

sleep と組み合わせて戦う。

import subprocess
from subprocess import Popen
from time import  sleep

cmd = "sleep 30"
proc = Popen( cmd,shell=True )
print( "process id = %s" % proc.pid )
sleep(5)
proc.terminate()

stack overflowには、Decoratorをと例外を使ってタイムアウトを実現する方法が紹介されていた。

Popenと wait と terminate を使う時の注意点

プロセスを扱うときに、PIPE.stdin/stdoutがデッドロックになる可能性があるので、注意しろとドキュメントに書いてあった。

terminate は SIGTERMを送信するが、WindowsはWin32APIの ProcessTerminate()を送信する。実行環境で違いがある。

実装系に依って動作が異なるようだ。

また、OSXで動かした時は shell=Trueをつけても、シェル経由にならなかった。

OSX で shell=True 付きの動作

takuya@~/Desktop$ pstree -s python
-+= 00001 root /sbin/launchd
 \-+= 02974 takuya /Applications/iTerm.app/Contents/MacOS/iTerm2
   \-+= 42127 takuya /Applications/iTerm.app/Contents/MacOS/iTerm2 --server login -fp takuya
     \-+- 42128 root login -fp takuya
       \-+= 42129 takuya -bash
         \-+= 42234 takuya /usr/local/Cellar/python/2.7.11/Frameworks/Python.framework/Versions/2.7/Resources/Python.ap
           \--- 42235 takuya sleep 30

理由は良く分からないけど、shell=Trueをつけて試したがシェル経由にはらなかった。疑問が残った。

Pythonのsubprocess は実行環境次第なところがあるらしく、OSを変えたらチェックが必要かもしれない。

複数プロセスをパイプするとき

subprocess.PIPを使って、プロセスをパイプする。

cmd1 = "ls -lt "
cmd2 = "head -n 5 "
p1 = subprocess.Popen(cmd1.strip().split(" "), stdout=subprocess.PIPE)
p2 = subprocess.Popen(cmd2.strip().split(" "), stdin=p1.stdout)
p1.stdout.close()
output = p2.communicate()[0]

comunicate を呼び出すと、waitになるし、stdout.close()して次に渡されるようですね。

p2が起動後に、p1のSTDOUTをclose()したらp1 がp2からのSIGPIPEを受け取れるのでパイプを使えるとのこと。

check_output でも出来るようです

output=check_output("ls -alt / | head -n 2", shell=True)

戻り値が byte 何だけど、str でほしいよ。

byte で出てくるので、decode が必要になったりして、めんどくさい。

ret = subprocess.check_output("ls -l ", shell=True,universal_newlines=True)

universal_newlines=True をつけると unicode な str でもらえるよ。

プログラムにSTDINを渡したい。

rubyphpbashなどSTDINでソース・コードを渡せるスクリプトpythonから実行するには

p1 = Popen(['php'],shell=False,stdin=subprocess.PIPE,stdout=subprocess.PIPE)
p1.stdin.write(b'<?php echo "hello";')
p1.stdin.close()
p1.wait()
ret = p1.stdout.read()
print(str(ret, 'UTF-8'))

このように、手順を踏めば、pythonから外部スクリプトを実行して、その結果を変数に受け取ることができる。jsonなどで外部コードとやり取りすれば、プロセス間通信のようなことも可能になる。

まとめ

コマンドの実行

  • 単純な呼び出し(終了待ちする系)
    • call ( "ls -l " , shell=True)
    • check_call ( "ls -l " , shell=True) 終了STATUSが0以外なら例外
    • check_output ( "ls -l ", shell=True ) stdoutを取得
  • Popenを使ったプロセス起動 (終了待ちなし)
    • popen = Popen( "ls -l" ,shell=True)
    • popen . wait ()
    • popen.terminate()
    • popen.communicate()[0] stdout取得
    • popen.reterncode exit status 終了STATSUを取得
    • popen .pid プロセスIDを取得
    • popen.poll() 終了しているか取得
  • シェル経由かそうでないか
    • シェル経由 shell=True をつけて文字列で渡す
    • 非シェル経由 コマンドを配列で渡す。
関数 popenを使って同等のこと
call Popen( cmd_list ).wait()
check_output Popen( cmd_list ).communicate[0]

追記

Popenの読み方がよくわからん。 Popen = p-open なのか、 Pop-en なのか、どっちなんだろう。

2016-04-25追記

split だと正規表現が使えないので

cmd .strip().split(" ")

正規表現を使う場合は

import re
cmd = re.split('\s+', cmd.strip() )

のほうが良さそうです。

2019-09-03

str / byte の取り方について補足

参考資料

https://docs.python.org/2/library/subprocess.html#replacing-functions-from-the-popen2-module

http://stackoverflow.com/questions/2281850/timeout-function-if-it-takes-too-long-to-finish