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 예외를 다시 발생시켜요.
  • inputPopen.communicate()로 전달되어 자식의 stdin에 들어가요. 쓰면 내부 Popen이 stdin을 PIPE로 설정해요.
  • check가 참이고 프로세스가 0이 아닌 종료 코드로 나오면 CalledProcessError가 발생해요.
  • encoding/errors를 지정하거나 text가 참이면 텍스트 모드로 열려요. 기본은 바이너리 모드.
  • envNone이 아니면 새 프로세스의 환경 변수를 정의하는 매핑이에요(현재 프로세스 환경 상속 대신).

예제:

>>> 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가 이 속성에 합쳐지고 stderrNone.
  • stderr — 캡처된 자식 stderr.
  • check_returncode()returncode가 0이 아니면 CalledProcessError 발생.

subprocess.DEVNULLos.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.CalledProcessErrorcheck_call(), check_output(), 또는 check=Truerun()이 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=Trueargs를 문자열로 넘기는 것이 권장돼요. POSIX에서 shell=True면 셸 기본값은 /bin/sh이고, PopenPopen(['/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=Falseargs가 지정한 프로그램을 대체해요(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_fdsTrue가 강제돼요.
  • cwd — 자식 실행 전 작업 디렉터리 변경. string/bytes/path-like.
  • restore_signals(기본 True) — Python이 SIG_IGN으로 설정한 모든 시그널을 자식에서 exec 전에 SIG_DFL로 복원(POSIX 전용).
  • start_new_sessionsetsid() 시스템 호출 수행(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이 아니면 현재 프로세스 환경 상속 대신 사용. strstr 또는 POSIX에선 bytesbytes.
  • encoding/errors/text/universal_newlines — 텍스트 모드 제어.
  • startupinfoSTARTUPINFO 객체(Windows).
  • creationflags — Windows 플래그(아래 참고).
  • pipesizePIPE 사용 시 파이프 크기 변경(현재 Linux만 지원). 3.10 추가.

Popen 객체는 컨텍스트 매니저로 지원돼요. 종료 시 표준 파일 디스크립터가 닫히고 프로세스가 기다려져요:

with Popen(["ifconfig"], stdout=PIPE) as proc:
    log.write(proc.stdout.read())

Popen과 이 모듈의 다른 함수는 감사 이벤트 subprocess.Popenexecutable, 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.argsPopen에 전달된 args. 3.3 추가.
  • Popen.stdinstdin=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/hStdErrorSTARTF_USESTDHANDLES 지정 시 표준 핸들.
  • wShowWindowSTARTF_USESHOWWINDOW 지정 시 ShowWindownCmdShow 값(SW_SHOWDEFAULT 제외). SW_HIDEshell=TruePopen 호출 시 사용.
  • lpAttributeListSTARTUPINFOEX의 추가 속성 딕셔너리. 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에서 실행 파일 찾기.