subprocess — 서브프로세스 관리
subprocess — 서브프로세스 관리
subprocess 모듈은 새 프로세스를 만들고, 그 입력/출력/오류 파이프에 연결하며, 반환 코드를 얻을 수 있게 해줘요. 여러 구식 모듈과 함수를 대체하기 위해 만들어졌어요.
os.system
os.spawn*
가용성: Android, iOS, WASI 아님.
출처: Python 표준 라이브러리
본문
subprocess 모듈 사용하기
서브프로세스를 호출하는 권장 접근은, 처리할 수 있는 모든 경우에 run() 함수를 쓰는 거예요. 더 고급 사용은 밑바닥의 Popen 인터페이스를 직접 쓸 수 있어요.
subprocess.run(args, *, stdin=None, input=None, stdout=None, stderr=None, capture_output=False, shell=False, cwd=None, timeout=None, check=False, encoding=None, errors=None, text=None, env=None, universal_newlines=None, **other_popen_kwargs)
args가 설명하는 명령을 실행해요. 명령이 완료될 때까지 기다린 뒤 CompletedProcess 인스턴스를 반환해요.
capture_output이 참이면 stdout과 stderr가 캡처돼요. 내부Popen객체가 stdout/stderr 둘 다PIPE로 설정된 채 자동 생성돼요. 두 스트림을 하나로 합치려면capture_output대신 stdout은PIPE, stderr은STDOUT으로 설정하세요.timeout은 초 단위로 지정하고 내부적으로Popen.communicate()에 전달돼요. 만료되면 자식 프로세스를 죽이고 기다린 뒤,TimeoutExpired예외를 다시 발생시켜요.input은Popen.communicate()로 전달되어 자식의 stdin에 들어가요. 쓰면 내부Popen이 stdin을PIPE로 설정해요.check가 참이고 프로세스가 0이 아닌 종료 코드로 나오면CalledProcessError가 발생해요.encoding/errors를 지정하거나text가 참이면 텍스트 모드로 열려요. 기본은 바이너리 모드.env가None이 아니면 새 프로세스의 환경 변수를 정의하는 매핑이에요(현재 프로세스 환경 상속 대신).
예제:
>>> subprocess.run(["ls", "-l"]) # doesn't capture output
CompletedProcess(args=['ls', '-l'], returncode=0)
>>> subprocess.run("exit 1", shell=True, check=True)
Traceback (most recent call last):
...
subprocess.CalledProcessError: Command 'exit 1' returned non-zero exit status 1
>>> subprocess.run(["ls", "-l", "/dev/null"], capture_output=True)
CompletedProcess(args=['ls', '-l', '/dev/null'], returncode=0,
stdout=b'crw-rw-rw- 1 root root 1, 3 Jan 23 16:23 /dev/null\n', stderr=b'')
3.5 추가. 3.6에서 encoding/errors, 3.7에서 text/capture_output 추가. 3.12에서 shell=True의 Windows 셸 검색 순서 변경.
class subprocess.CompletedProcess
run()의 반환값으로, 끝난 프로세스를 나타내요.
args— 프로세스를 시작하는 데 쓰인 인자. 리스트 또는 문자열.returncode— 자식 프로세스의 종료 상태. 0이면 성공. 음수-N은 시그널N으로 종료됨(POSIX).stdout— 캡처된 자식 stdout.stderr=subprocess.STDOUT으로 실행했다면 stdout과 stderr가 이 속성에 합쳐지고stderr는None.stderr— 캡처된 자식 stderr.check_returncode()—returncode가 0이 아니면CalledProcessError발생.
subprocess.DEVNULL — os.devnull 특수 파일을 쓰도록 하는 stdin/stdout/stderr 값. 3.3 추가.
subprocess.PIPE — 표준 스트림에 새 파이프를 열도록 하는 값. Popen.communicate()와 가장 유용.
subprocess.STDOUT — stderr가 stdout과 같은 핸들로 가도록 하는 값.
exception subprocess.SubprocessError — 이 모듈의 다른 모든 예외의 기본 클래스. 3.3 추가.
exception subprocess.TimeoutExpired — 자식 프로세스를 기다리는 동안 타임아웃이 만료되면 발생하는 SubprocessError 서브클래스. 속성: cmd, timeout, output, stdout(output의 별칭), stderr. 3.3 추가. 3.5에서 stdout/stderr 속성 추가.
exception subprocess.CalledProcessError — check_call(), check_output(), 또는 check=True인 run()이 0이 아닌 종료 상태를 반환할 때 발생하는 SubprocessError 서브클래스. 속성: returncode, cmd, output, stdout, stderr.
자주 쓰는 인자
Popen 생성자(및 편의 함수)는 많은 선택 인자를 받아요. 가장 자주 필요한 것:
args— 모든 호출에 필수. 문자열이거나 프로그램 인자 시퀀스. 시퀀스가 권장되는데, 필요한 이스케이프/따옴표 처리를 모듈이 처리해주기 때문이에요(예: 파일 이름의 공백). 단일 문자열이라면shell=True여야 하거나, 그냥 인자 없이 실행할 프로그램 이름이어야 해요.stdin,stdout,stderr— 각각 표준 입력/출력/오류 파일 핸들. 유효한 값은None,PIPE,DEVNULL, 기존 파일 디스크립터(양의 정수), 유효한 파일 디스크립터를 가진 기존 파일 객체. 기본None은 리다이렉션 없음. 추가로 stderr는STDOUT일 수 있어요.encoding/errors지정 또는text(일명universal_newlines) 참이면 텍스트 모드로 열려요. 텍스트 모드에서 stdin 입력의 줄 끝'\n'은os.linesep으로 변환되고, stdout/stderr의 출력 줄 끝은'\n'으로 변환돼요. 텍스트 모드가 아니면 바이너리 스트림으로 열려요.shell=True면 지정 명령이 셸을 통해 실행돼요. 셸 파이프, 파일 이름 와일드카드, 환경 변수 확장 같은 셸 기능에 편리하게 접근할 수 있으니 유용해요. 하지만 Python 자체가 많은 셸류 기능(특히glob,fnmatch,os.walk(),os.path.expandvars(),os.path.expanduser(),shutil)을 제공한다는 점을 기억하세요.shell=True를 쓰기 전에 보안 고려사항을 읽어야 해요.
Popen 생성자
class subprocess.Popen(args, bufsize=-1, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=True, shell=False, cwd=None, env=None, universal_newlines=None, startupinfo=None, creationflags=0, restore_signals=True, start_new_session=False, pass_fds=(), *, group=None, extra_groups=None, user=None, umask=-1, encoding=None, errors=None, text=None, pipesize=-1, process_group=None)
새 프로세스에서 자식 프로그램을 실행해요. POSIX에선 os.execvpe()-류 동작을, Windows에선 CreateProcess()를 사용해요.
args— 프로그램 인자 시퀀스 또는 단일 문자열/path-like 객체. 기본적으로 시퀀스의 첫 항목이 실행할 프로그램이에요.- 최대 신뢰성을 위해 실행 파일의 정규화된 경로를 쓰세요.
PATH에서 이름 없는 것을 찾으려면shutil.which()사용.sys.executable을 넘기고-m형식으로 모듈을 실행하는 게 권장돼요.
시퀀스로 전달 예:
Popen(["/usr/bin/git", "commit", "-m", "Fixes a bug."])
shlex.split()으로 셸 명령을 시퀀스로 나누는 방법을 알 수 있어요:
>>> import shlex, subprocess
>>> command_line = input()
/bin/vikings -input eggs.txt -output "spam spam.txt" -cmd "echo '$MONEY'"
>>> args = shlex.split(command_line)
>>> print(args)
['/bin/vikings', '-input', 'eggs.txt', '-output', 'spam spam.txt', '-cmd', "echo '$MONEY'"]
>>> p = subprocess.Popen(args) # Success!
shell(기본False) — 셸을 실행할 프로그램으로 쓸지.shell=True면args를 문자열로 넘기는 것이 권장돼요. POSIX에서shell=True면 셸 기본값은/bin/sh이고,Popen은Popen(['/bin/sh', '-c', args[0], args[1], ...])과 동등해요. Windows에선COMSPEC환경 변수가 기본 셸을 지정해요.bufsize— pipe 파일 객체를 만들 때open()에 공급.0은 버퍼링 없음,1은 라인 버퍼링(text=True에서만), 다른 양수는 대략 그 크기 버퍼, 음수(기본)는io.DEFAULT_BUFFER_SIZE사용. 3.3.1에서 기본 -1로 변경.executable— 실행할 대체 프로그램. 아주 드물게 필요.shell=False면args가 지정한 프로그램을 대체해요(POSIX에서ps같은 유틸리티의 표시 이름은 args 이름).stdin,stdout,stderr— 위 자주 쓰는 인자 참고.preexec_fn— 자식이 실행되기 직전에 자식 프로세스에서 호출될 콜러블(POSIX 전용). 스레드가 있는 응용에서는 안전하지 않아요 — 자식 프로세스가 exec 전에 교착할 수 있어요. 환경을 바꾸려면env를 쓰세요.os.setsid()/os.setpgid()대신start_new_session/process_group을 쓰세요. 3.8부터 서브인터프리터에서 지원되지 않음.close_fds(기본True) — 자식 실행 전에0,1,2를 제외한 모든 파일 디스크립터를 닫아요. 3.2, 3.7에서 기본 변경.pass_fds— 부모와 자식 사이에 열어둘 파일 디스크립터 시퀀스(POSIX 전용). 아무거나 제공하면close_fds가True가 강제돼요.cwd— 자식 실행 전 작업 디렉터리 변경. string/bytes/path-like.restore_signals(기본True) — Python이 SIG_IGN으로 설정한 모든 시그널을 자식에서 exec 전에 SIG_DFL로 복원(POSIX 전용).start_new_session—setsid()시스템 호출 수행(POSIX). 3.2 추가.process_group— 음이 아닌 정수면setpgid(0, value)호출(POSIX). 3.11 추가.group/extra_groups/user/umask— 각각setregid()/setgroups()/setreuid()/umask()호출(POSIX). 3.9 추가.user를 지정해도 기존 보조 그룹 멤버십은 떨어지지 않아요 — 보안 목적으론extra_groups=()도 넘겨야 해요.env— 새 프로세스의 환경 변수 매핑.None이 아니면 현재 프로세스 환경 상속 대신 사용.str→str또는 POSIX에선bytes→bytes.encoding/errors/text/universal_newlines— 텍스트 모드 제어.startupinfo—STARTUPINFO객체(Windows).creationflags— Windows 플래그(아래 참고).pipesize—PIPE사용 시 파이프 크기 변경(현재 Linux만 지원). 3.10 추가.
Popen 객체는 컨텍스트 매니저로 지원돼요. 종료 시 표준 파일 디스크립터가 닫히고 프로세스가 기다려져요:
with Popen(["ifconfig"], stdout=PIPE) as proc:
log.write(proc.stdout.read())
Popen과 이 모듈의 다른 함수는 감사 이벤트 subprocess.Popen을 executable, args, cwd, env 인자로 발생시켜요.
예외: 자식 프로세스에서 새 프로그램이 실행되기 전에 발생한 예외는 부모에서 다시 발생해요. 가장 흔한 것은 OSError. Popen에 잘못된 인자를 주면 ValueError. check_call()/check_output()은 0이 아닌 반환 코드면 CalledProcessError. 타임아웃이 만료되면 TimeoutExpired. 모두 SubprocessError를 상속해요.
보안 고려사항
다른 popen 함수와 달리 이 라이브러리는 암묵적으로 시스템 셸을 부르지 않아요. 즉 셸 메타문자를 포함한 모든 문자가 자식 프로세스에 안전하게 전달될 수 있어요. 셸을 명시적으로(shell=True) 호출하면 모든 공백과 메타문자를 적절히 따옴표 처리해서 셸 주입 취약점을 피하는 건 응용의 책임이에요. 일부 플랫폼에선 shlex.quote()로 이스케이프할 수 있어요. Windows에서 배치 파일이 시스템 셸에서 실행될 수 있으니 주의하세요.
Popen 객체
Popen.poll()— 자식 프로세스가 종료됐는지 확인. 종료됐으면returncode속성을 설정/반환, 아니면None.Popen.wait(timeout=None)— 자식이 종료될 때까지 대기.timeout후에도 종료 안 하면TimeoutExpired. 주의:stdout=PIPE/stderr=PIPE에서 자식이 OS 파이프 버퍼를 채우면 교착할 수 있어요. 파이프를 쓸 땐Popen.communicate()를 쓰세요. 3.3에서timeout추가.Popen.communicate(input=None, timeout=None)— 프로세스와 상호작용: stdin으로 데이터를 보내고 stdout/stderr에서 EOF까지 읽고, 프로세스가 종료될 때까지 대기한 뒤returncode설정.(stdout_data, stderr_data)튜플 반환. stdin에 데이터를 보내려면stdin=PIPE로 만들고, 결과 튜플에서 뭔가를 얻으려면stdout=PIPE와/또는stderr=PIPE를 줘야 해요. 타임아웃이 지나도 자식은 죽지 않으므로 정리를 위해 자식을 죽이고 통신을 마치세요:
proc = subprocess.Popen(...)
try:
outs, errs = proc.communicate(timeout=15)
except TimeoutExpired:
proc.kill()
outs, errs = proc.communicate()
데이터는 메모리에 버퍼링되므로 크거나 무한한 데이터엔 쓰지 마세요.
Popen.send_signal(signal)— 자식에 시그널 전송.Popen.terminate()— POSIX에선SIGTERM, Windows에선TerminateProcess()호출로 자식을 중단.Popen.kill()— POSIX에선SIGKILL전송, Windows에선terminate()의 별칭.
속성:
Popen.args—Popen에 전달된 args. 3.3 추가.Popen.stdin—stdin=PIPE면 쓰기 가능한 스트림(open()이 반환하는 것). 아니면None.Popen.stdout,Popen.stderr— 읽기 가능한 스트림. 아니면None.Popen.pid— 자식 프로세스의 프로세스 ID.Popen.returncode— 자식 반환 코드. 초기None,poll()/wait()/communicate()가 종료를 감지하면 설정. 음수-N은 시그널N(POSIX).shell=True면 셸 자체의 종료 상태를 반영.
Windows Popen 헬퍼
class subprocess.STARTUPINFO(*, dwFlags=0, hStdInput=None, hStdOutput=None, hStdError=None, wShowWindow=0, lpAttributeList=None)
Windows STARTUPINFO 구조의 부분 지원. 3.7에서 키워드 전용 인자 지원.
si = subprocess.STARTUPINFO()
si.dwFlags = subprocess.STARTF_USESTDHANDLES | subprocess.STARTF_USESHOWWINDOW
dwFlags— 프로세스가 창을 만들 때 특정STARTUPINFO속성을 쓸지 결정하는 비트 필드.hStdInput/hStdOutput/hStdError—STARTF_USESTDHANDLES지정 시 표준 핸들.wShowWindow—STARTF_USESHOWWINDOW지정 시ShowWindow의nCmdShow값(SW_SHOWDEFAULT제외).SW_HIDE는shell=True로Popen호출 시 사용.lpAttributeList—STARTUPINFOEX의 추가 속성 딕셔너리.handle_list(상속될 핸들 시퀀스) 지원. 3.7 추가.
Windows 상수: STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE, SW_HIDE, STARTF_USESTDHANDLES, STARTF_USESHOWWINDOW, STARTF_FORCEONFEEDBACK(3.13), STARTF_FORCEOFFFEEDBACK(3.13), 그리고 creationflags 값들(CREATE_NEW_CONSOLE, CREATE_NEW_PROCESS_GROUP, ABOVE_NORMAL_PRIORITY_CLASS, BELOW_NORMAL_PRIORITY_CLASS, HIGH_PRIORITY_CLASS, IDLE_PRIORITY_CLASS, NORMAL_PRIORITY_CLASS, REALTIME_PRIORITY_CLASS, CREATE_NO_WINDOW, DETACHED_PROCESS, CREATE_DEFAULT_ERROR_MODE, CREATE_BREAKAWAY_FROM_JOB). 대부분 3.7 추가.
구식 고수준 API
Python 3.5 이전의 고수준 API였던 함수들이에요. 이제 run()을 많이 쓸 수 있지만 기존 코드가 많이 호출해요.
subprocess.call(args, *, stdin=None, stdout=None, stderr=None, shell=False, cwd=None, timeout=None, **other_popen_kwargs)
명령 실행 후 returncode 속성을 반환. Stdout/stderr를 캡처해야 하는 코드는 run()을 쓰세요. 이 함수에 stdout=PIPE/stderr=PIPE를 쓰지 마세요 — 파이프를 읽지 않아 자식이 막힐 수 있어요.
subprocess.check_call(...) — 명령 실행, 종료 코드가 0이면 반환, 아니면 CalledProcessError 발생. stdout/stderr 캡처는 run(..., check=True) 사용.
subprocess.check_output(args, *, stdin=None, stderr=None, shell=False, cwd=None, encoding=None, errors=None, universal_newlines=None, timeout=None, text=None, **other_popen_kwargs)
명령 실행 후 출력을 반환. 0이 아닌 반환 코드면 CalledProcessError(output 속성에 출력). run(..., check=True, stdout=PIPE).stdout과 동등. input=None을 넘기면 input=b''처럼 동작해요. stderr도 캡처하려면 stderr=subprocess.STDOUT:
>>> subprocess.check_output(
... "ls non_existent_file; exit 0",
... stderr=subprocess.STDOUT,
... shell=True)
'ls: non_existent_file: No such file or directory\n'
3.1 추가. 3.4에서 input 지원.
구식 함수를 subprocess 모듈로 교체하기
이 섹션에서 "a becomes b"는 b를 a의 대체로 쓸 수 있음을 의미해요. 모든 "a" 함수는 실행 프로그램을 찾지 못하면 (대체로) 조용히 실패하지만, "b" 대체물은 OSError를 발생시켜요.
/bin/sh 셸 명령 치환:
output=$(mycmd myarg)
output = check_output(["mycmd", "myarg"])
셸 파이프라인:
output=$(dmesg | grep hda)
p1 = Popen(["dmesg"], stdout=PIPE)
p2 = Popen(["grep", "hda"], stdin=p1.stdout, stdout=PIPE)
p1.stdout.close() # Allow p1 to receive a SIGPIPE if p2 exits.
output = p2.communicate()[0]
p1.stdout.close() 호출은 p2가 p1보다 먼저 나가면 p1이 SIGPIPE를 받도록 하는 데 중요해요. 신뢰된 입력엔 셸 파이프라인을 직접 쓸 수도 있어요: output = check_output("dmesg | grep hda", shell=True).
os.system() 교체:
sts = os.system("mycmd" + " myarg")
# becomes
retcode = call("mycmd" + " myarg", shell=True)
주의: call() 반환값은 os.system()과 다르게 인코딩되고, os.system()은 실행 중 SIGINT/SIGQUIT를 무시하지만 subprocess에선 호출자가 직접 처리해야 해요.
os.spawn 계열 교체:
pid = os.spawnlp(os.P_NOWAIT, "/bin/mycmd", "mycmd", "myarg")
==>
pid = Popen(["/bin/mycmd", "myarg"]).pid
retcode = os.spawnlp(os.P_WAIT, "/bin/mycmd", "mycmd", "myarg")
==>
retcode = call(["/bin/mycmd", "myarg"])
os.spawnvp(os.P_NOWAIT, path, args)
==>
Popen([path] + args[1:])
os.spawnlpe(os.P_NOWAIT, "/bin/mycmd", "mycmd", "myarg", env)
==>
Popen(["/bin/mycmd", "myarg"], env={"PATH": "/usr/bin"})
os.popen() 교체:
pipe = os.popen(cmd, 'w')
...
rc = pipe.close()
if rc is not None and rc >> 8:
print("There were some errors")
==>
process = Popen(cmd, stdin=PIPE)
...
process.stdin.close()
if process.wait() != 0:
print("There were some errors")
레거시 셸 호출 함수
2.x commands 모듈의 레거시 함수들이에요. 암묵적으로 시스템 셸을 호출하므로 위의 보안/예외 처리 일관성 보장이 적용되지 않아요.
subprocess.getstatusoutput(cmd, *, encoding=None, errors=None) — 셸에서 cmd 실행의 (exitcode, output) 반환. 출력의 뒤따르는 개행은 제거돼요.
>>> subprocess.getstatusoutput('ls /bin/ls')
(0, '/bin/ls')
>>> subprocess.getstatusoutput('cat /bin/junk')
(1, 'cat: /bin/junk: No such file or directory')
>>> subprocess.getstatusoutput('/bin/junk')
(127, 'sh: /bin/junk: not found')
>>> subprocess.getstatusoutput('/bin/kill $$')
(-15, '')
가용성: Unix, Windows. 3.3.4에서 Windows 지원, 3.11에서 encoding/errors 추가.
subprocess.getoutput(cmd, *, encoding=None, errors=None) — 셸에서 cmd 실행의 출력(stdout과 stderr)만 반환. getstatusoutput()과 비슷하되 exit code는 무시돼요. 가용성: Unix, Windows. 3.11에서 encoding/errors 추가.
참고 사항
타임아웃 동작: 프로세스 생성 자체는 많은 플랫폼 API에서 중단할 수 없어요. 그래서 타임아웃을 지정해도 프로세스 생성이 걸리는 시간만큼은 지나야 타임아웃 예외를 볼 수 있어요. 몇 밀리초 같은 극히 작은 타임아웃 값은 프로세스 생성과 스케줄링에 시간이 필요하므로 거의 즉시 TimeoutExpired가 발생할 수 있어요.
Windows에서 인자 시퀀스 문자열 변환: 공백/탭으로 구분, 큰따옴표로 둘러싸면 공백이 있어도 단일 인자, 백슬래시가 앞선 큰따옴표는 리터럴 큰따옴표 등 MS C 런타임 규칙을 따름. shlex 모듈이 명령줄 파싱/이스케이프를 제공해요.
posix_spawn() 비활성화: Linux에서 subprocess는 안전할 때 vfork()를 내부적으로 기본 사용해 성능을 크게 높여요. 원하면 subprocess._USE_POSIX_SPAWN = False로 설정해 끌 수 있어요.
더 알아보기
- PEP 324 — subprocess 모듈을 제안한 PEP.
shlex— 명령줄을 파싱/이스케이프하는 모듈.shutil.which()—PATH에서 실행 파일 찾기.