ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Python] Tip - 자식 프로세스를 관리하려면 subprocess를 사용
    언어/파이썬 & 장고 2016. 12. 9. 22:00

    파이썬은 실전에서 단련된 자식 프로세스 실행과 관리용 라이브러리를 갖추고 있습니다. 따라서 명령줄 유틸리티 같은 다른 도구들은 연계하는 데 아주 좋은 언어입니다. 기존 셸 스크립트가 시간이 지나면서 점점 복잡해지면, 자연히 파이썬 코드로 재작성하여 가독성과 유지보수성을 확보하려고 하기 마련입니다.

    파이썬으로 시작한 자식 프로세스는 병렬로 실행할 수 있으므로, 파이썬을 사용하면 머신의 CPU 코어를 모두 이용해 프로그램의 처리량을 극대화할 수 있습니다. 파이썬 자체는 CPU 속도에 의존할 수 있지만 파이썬을 사용하면 CPU를 많이 사용하는 작업을 관리하고 조절하기 쉽습니다.

    수년간 파이썬에는 popen, popen2, os.exec*를 비롯해 서브프로세스를 실행하는 방법이 여러 개 있습니다. 요즘 파이썬에서 자식 프로세스를 관리하는 최선이자 가장 간단한 방법은 내장모듈 subprocess를 사용하는 것입니다.

    subprocess로 자식프로세스를 실행하는 방법은 간단합니다. 다음 코드에서는 Popen생성자가 프로세스를 시작합니다. communicate 메서드는 자식 프로세스의 출력을 읽어오고 자식 프로세스가 종료할 때까지 대기합니다.

    import subprocess
    
    
    proc = subprocess.Popen(
        ['echo', 'Hello world'],
        stdout=subprocess.PIPE
    )
    out, err = proc.communicate()
    print(out)
    print(out.decode('utf-8'))
    
    # 결과
    # b'Hello world\n'
    # Hello world
    
    


    자식 프로세스는 부모 프로세스와 파이썬 인터프리터와는 독립적으로 실행됩니다. 자식 프로세스의 상태는 파이썬이 다른 작업을 하는 동안 주기적으로 폴링(polling)됩니다.

    import subprocess
    
    proc = subprocess.Popen(
        ['sleep', '0.3']
    )
    
    while proc.poll() is None:
        print('Working...')
        # 시간이 걸리는 작업을 몇 개 수행
    
    print('Exit status', proc.poll())
    
    # 결과
    
    # Working...
    # Working...
    # Working...
    # Exit status 0


    부모에서 자식 프로세스를 떼어낸다는 건 부모 프로세스가 자유롭게 여러 자식 프로세스를 병렬로 실행할 수 있음을 의미합니다. 자식 프로세스를 떼어내려면 모든 자식프로세스를 먼저 시작하면 됩니다.

    import subprocess
    from time import time
    
    
    def run_sleep(period):
        proc = subprocess.Popen(['sleep', str(period)])
        return proc
    
    
    start = time()
    
    procs = []
    for _ in range(10):
        proc = run_sleep(0.1)
        procs.append(proc)
    
    # communicate()로 자식 프로세스들이 I/O를 마치고 종료하기를 기다림
    
    for proc in procs:
        proc.communicate()
    
    end = time()
    
    print('finished in %.3f seconds' % (end - start))
    
    # 결과
    # finished in 0.127 seconds


    해당 프로세스들이 순차적으로 실행했다면 전체 지연시간은 여기서 측정한 약 0.1초가 아닌 1초였을 것입니다.

    파이썬 프로그램에서 파이프(pipe)를 이용해 데이터를 서브프로세스로 보낸 다음 서브프로세스의 결과를 받아올 수도 있습니다. 이 방법을 이용하면 다른 프로그램을 활용하여 작업을 병렬로 수행할 수 있습니다. 예를 들어 어떤 데이터를 암호화하는 데 openssl 명령줄 도구를 사용하려 한다고 가정합니다. 명령줄 인수와 I/O파이프를 사용하여 자식 프로세스를 실행하는 건 간단합니다.

    import subprocess
    import os
    
    
    def run_openssl(data):
        env = os.environ.copy()
        env['password'] = b'\xe24U\n\xd0Ql3S\x11'
        proc = subprocess.Popen(
            ['openssl', 'enc', '-des3', '-pass', 'env:password'],
            env=env,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE
        )
        proc.stdin.write(data)
        proc.stdin.flush()  # 자식 프로세스가 입력을 반드시 받게 함
    
        return proc
    
    
    # 파이프로 암호화 함수에 임의의 바이트를 전달하지만 실전에서는 사용자 입력, 파일 핸들, 네트워크 소켓 등을 전달함
    
    procs = []
    for _ in range(3):
        data = os.urandom(10)
        proc = run_openssl(data)
        procs.append(proc)
    
    # 자식 프로세스는 병렬로 실행되고 입력을 소비. 다음 코드에서는 자식 프로세스가 종료할 때까지 대기하고 최종 결과를 받음
    
    for proc in procs:
        out, err = proc.communicate()
        print(out[-10:])
    
    # 결과
    # b'?\x8b\x1aJ$%\x89O\xb5\x1d'
    # b"2\r`'\xabt\x0e\xdd\t\xb6"
    # b'T$\x04\x96\xda\x8c\xe9\x9e\x83I'


    유닉스의 파이프처럼 한 자식 프로세스의 결과를 다른 프로세스의 입력으로 연결하여 병렬 프로세스의 체인을 생성할 수도 있습니다. 다음은 자식프로세스를 시작하여 md5 명령줄 도구에서 입력 스트림을 소비하게 하는 함수입니다.

    import subprocess
    import os
    
    
    def run_openssl(data):
        env = os.environ.copy()
        env['password'] = b'\xe24U\n\xd0Ql3S\x11'
        proc = subprocess.Popen(
            ['openssl', 'enc', '-des3', '-pass', 'env:password'],
            env=env,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE
        )
        proc.stdin.write(data)
        proc.stdin.flush()  # 자식 프로세스가 입력을 반드시 받게 함
    
        return proc
    
    
    def run_md5(input_stdin):
        proc = subprocess.Popen(
            ['md5'],
            stdin=input_stdin,
            stdout=subprocess.PIPE
        )
        return proc
    
    
    """ 파이썬의 내장모듈 hashlib는 md5함수를 제공하므로 subprocess를 항상 이렇게 실행할 필요는 없음.
    여기서는 subprocess에서 입력과 추력을 파이프로 연결하는 방법을 보여주는 것이 목적"""
    
    # 데이터를 암호화하는 openssl 프로세스 집합과 암호화된 결과를 md5로 해시하는 프로세스 집합을 시작할 수 있음
    input_procs = []
    hash_procs = []
    for _ in range(3):
        data = os.urandom(10)
        proc = run_openssl(data)
        input_procs.append(proc)
        hash_proc = run_md5(proc.stdout)
        hash_procs.append(hash_proc)
    
    # 일단 자식 프로세스들이 시작하면 이들 사이의 I/O는 자동을 일어남. 할일은 모든 작업이 끝나고 최종 결과물이 출력되기를 기다리는 것 뿐!
    
    for proc in input_procs:
        proc.communicate()
    
    for proc in hash_procs:
        out, err = proc.communicate()
        print(out.strip())
    
    # 결과
    # b'25eabef4e8db927bd0637d70f5736ab5'
    # b'18e40cf3b7e74ad8e0972db699aaed8e'
    # b'b3a05ef5bf118169e9eb91d87f3133d3'


    자식 프로세스가 종료되지 않거나 입력 또는 출력 파이프에서 블록될 염려가 있다면 communicate 메서드에서 timeout 파라미터를 넘겨야 합니다. 이렇게 하면 자식프로세스가 일정한 시간 내에 응답하지 않을 때 예외가 일어나서 오동작하는 자식 프로세스를 종료할 기회를 얻습니다.

    import subprocess
    
    
    def run_sleep(period):
        proc = subprocess.Popen(['sleep', str(period)])
        return proc
    
    
    proc = run_sleep(10)
    try:
        proc.communicate(timeout=0.1)
    
    except subprocess.TimeoutExpired:
        proc.terminate()
        proc.wait()
    
    print('Exit status', proc.poll())
    
    # 결과
    # Exit status -15


    timeout 파라미터는 파이썬 3.3 이후 버전부터 사용할 수 있습니다. 이전 버전에서 I/O 타임아웃을 강제하려면 내장 모듈 select를 proc.stdin, proc.stdout, proc.stderr에 사용해야 합니다.

    요약

    자식 프로세스를 실행하고 자식 프로세스의 입출력 스트림을 관리하려면 subprocess모듈을 사용

    자식 프로세스는 파이썬 인터프리터에서 병렬로 실행되어 CPU 사용을 극대화하게 해줌

    communicate에 timeout 파라미터를 사용하여 자식 프로세스들이 교착상태(deadlock)에 빠지거나 멈추는 상황을 막아야함


    댓글