Microsoft MVP성태의 닷넷 이야기
스크립트: 26. 파이썬 - PyCharm을 이용한 fork 디버그 방법 [링크 복사], [링크+제목 복사],
조회: 14232
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 

(시리즈 글이 11개 있습니다.)
개발 환경 구성: 592. JetBrains의 IDE(예를 들어, PyCharm)에서 Visual Studio 키보드 매핑 적용
; https://www.sysnet.pe.kr/2/0/12752

개발 환경 구성: 595. PyCharm - WSL과 연동해 Django App을 윈도우에서 리눅스 대상으로 개발
; https://www.sysnet.pe.kr/2/0/12773

개발 환경 구성: 597. PyCharm - 윈도우 환경에서 WSL을 이용해 파이썬 앱 개발/디버깅하는 방법
; https://www.sysnet.pe.kr/2/0/12789

개발 환경 구성: 598. PyCharm - 원격 프로세스를 디버그하는 방법
; https://www.sysnet.pe.kr/2/0/12798

개발 환경 구성: 599. PyCharm - (반대로) 원격 프로세스가 PyCharm에 디버그 연결하는 방법
; https://www.sysnet.pe.kr/2/0/12800

개발 환경 구성: 601. PyCharm - 다중 프로세스 디버깅 방법
; https://www.sysnet.pe.kr/2/0/12806

스크립트: 26. 파이썬 - PyCharm을 이용한 fork 디버그 방법
; https://www.sysnet.pe.kr/2/0/12823

개발 환경 구성: 603. GoLand - WSL 환경과 연동
; https://www.sysnet.pe.kr/2/0/12825

개발 환경 구성: 673. JetBrains IDE에서 "Squash Commits..." 메뉴가 비활성화된 경우
; https://www.sysnet.pe.kr/2/0/13318

개발 환경 구성: 679. PyCharm(을 비롯해 JetBrains에 속한 여타) IDE에서 내부 Window들의 탭이 없어진 경우
; https://www.sysnet.pe.kr/2/0/13369

개발 환경 구성: 697. GoLand에서 ldflags 지정 방법
; https://www.sysnet.pe.kr/2/0/13489




파이썬 - PyCharm을 이용한 fork 디버그 방법

지난번에 설명한,

윈도우 개발자를 위한 리눅스 fork 동작 방식 설명 (파이썬 코드)
; https://www.sysnet.pe.kr/2/0/12811

fork의 작동 방식을 알았으면 이제 디버깅까지 알아봐야겠죠. ^^ 어떤 면에서는 다중 프로젝트 디버깅의 관점으로 생각할 수 있겠지만,

PyCharm - 다중 프로세스 디버깅 방법
; https://www.sysnet.pe.kr/2/0/12806

실제로 해보면 완전히 다르게 접근해야 합니다. 어떻게 바뀌나 아래의 예제로 한번 볼까요? ^^

import os

pid = os.fork()

if pid > 0:
    print('부모 프로세스의 실행 흐름', os.getpid())
elif pid == 0:
    print('자식 프로세스의 실행 흐름', os.getpid())
else:  # pid < 0
    print('fork 오류')

print('모두 호출', os.getpid())

우선, PyCharm은 fork 시 부모/자식 프로세스에 대해 모두 디버깅 환경을 제공합니다. 하지만, 저 상태에서 실행하면 (대개의 경우) PyCharm 콘솔 화면에는 다음과 같은 식의 부모 프로세스에 대한 출력만 보입니다.

Connected to pydev debugger (build 212.4746.96)
부모 프로세스의 실행 흐름 6295
모두 호출 6295

이런 결과는 PyCharm에서 실행할 때만 발생합니다. 직접 리눅스 환경에서 실행하면 "자식 프로세스의 실행 흐름" 메시지까지 모두 볼 수 있는데요, 아마도 PyCharm은 실행 대상의 "루트 프로세스"가 종료하면 자식 프로세스들까지 모두 강제로 종료하기 때문에 그런 것으로 보입니다.

따라서 PyCharm의 실행으로 자식 프로세스의 출력까지 보고 싶다면 부모 프로세스의 종료 시간을 지연해 줄 조치를 취해야 합니다. 대충 이런 식이 될 것입니다.

if pid > 0:
    print('부모 프로세스의 실행 흐름', os.getpid())
    if os.getenv("IDE_PROJECT_ROOTS"):
        time.sleep(1)

위의 코드를 추가하고 다시 PyCharm에서 Run/Debug로 실행하면 예상했던 출력을 확인할 수 있습니다.

부모 프로세스의 실행 흐름 6641
자식 프로세스의 실행 흐름 6645
모두 호출 6645
모두 호출 6641




그런데, 디버깅 관련해서 PyCharm에는 또 다른 이상한 규칙이 있는 것 같습니다. 저렇게 fork가 실행되는 경우, PyCharm은 가능한 이후에 실행되는 프로세스로 디버깅 세션을 연결한다는 것입니다.

일례로, 다음과 같이 BP를 걸어볼까요?

fork_debug_in_pycharm_1.png

그럼, 디버그 시 PyCharm은 자식 프로세스와는 어떤 연동도 없으므로 그냥 부모 프로세스의 디버깅을 하게 돼 정상적으로 BP가 걸립니다.

반면, 다음과 같이 BP를 걸면,

fork_debug_in_pycharm_2.png

자식 프로세스는 (부모 프로세스가 종료했으므로) 어떤 기회도 얻지 못해 PyCharm의 디버그 실행으로는 BP가 걸리지 않습니다. 만약 위의 상황에서 BP를 걸고 싶다면 이전에 언급한 대로 sleep을 줄 수 있는데요,

import os
import time

pid = os.fork()

if pid > 0:
    print('부모 프로세스의 실행 흐름', os.getpid())
    if os.getenv("IDE_PROJECT_ROOTS"):
        time.sleep(1)
elif pid == 0:
    print('자식 프로세스의 실행 흐름', os.getpid())
else:  # pid < 0
    print('fork 오류')

print('모두 호출', os.getpid())

저렇게 하고 "print('자식 프로세스의 실행 흐름', os.getpid())" 라인에 BP를 걸고 다시 PyCharm에서 Debug를 시작하면 이번에는 BP가 걸리긴 하지만 PyCharm의 디버깅 환경이 이내 해제가 되어버립니다. 왜냐하면, 이번에도 역시 부모 프로세스가 1초 후에 종료했으므로 현재 BP가 걸린 자식 프로세스까지 강제 종료되어 디버깅이 해제가 된 것입니다.

따라서, 위와 같은 경우에 마음 편히 자식 프로세스의 디버깅을 하고 싶다면 sleep을 크게 주든가, 아니면 아예 input 등의 함수를 실행하든가, os.waitpid 등의 방법을 강구해야 합니다.

import os

pid = os.fork()

if pid > 0:
    print('부모 프로세스의 실행 흐름', os.getpid())
    if os.getenv("IDE_PROJECT_ROOTS"):
        os.waitpid(pid, 0)  # 자식 프로세스가 종료할 때까지 대기
elif pid == 0:
    print('자식 프로세스의 실행 흐름', os.getpid())
else:  # pid < 0
    print('fork 오류')

print('모두 호출', os.getpid())

위와 같은 경우, 부모 프로세스에서 자식 프로세스 종료까지 대기하므로 PyCharm에서 자식 프로세스에 대한 디버깅을 마음 편히 할 수 있습니다.




자, 그럼 다시 처음으로 돌아가서 "pid = os.fork()"에 BP를 걸면 어떻게 될까요? 원칙을 따지면 이해가 되는데, 처음에는 디버깅 과정이 좀 황당할 수 있습니다.

일단, BP를 걸고 PyCharm에서 디버깅을 시작하면 당연히 그 라인에서 실행이 멈춥니다. 이후 Step-Over로 다음 라인을 실행하면 그때까지만 해도 부모 프로세스를 디버깅하던 것이, 순간적으로 약간 멈칫하면서 자식 프로세스의 디버깅 세션으로 전환이 됩니다. 따라서, 이후 if 문에서는 언제나 "elif pid == 0" 내의 코드로 진입합니다.

즉, "pid = os.fork()" 라인 이전부터 디버깅을 시작하면 절대로 "if pid > 0" 내의 코드로 진입할 수 없습니다. 왜냐하면 PyCharm은 다중 디버깅은 지원하지만, 디버깅 세션 자체는 하나만 지원하기 때문입니다. 이 점이, 비주얼 스튜디오를 사용해 본 개발자들에게는 상식적이지 않아서 처음에는 PyCharm의 fork 디버깅에 대해 혼란스러울 수 있습니다.

그런데 또 하나의 문제가 있는데요, os.fork 후에 execl로 실행하는 프로세스에 대한 디버깅입니다.

import os

pid = os.fork()

if pid > 0:
    print('부모 프로세스의 실행 흐름', os.getpid())
elif pid == 0:
    print('자식 프로세스의 실행 흐름', os.getpid())
    os.execl(sys.executable, sys.executable, 'calc.py')
else:  # pid < 0
    print('fork 오류')

print('모두 호출', os.getpid())

위와 같은 경우, python 자식 프로세스로 새롭게 calc.py 스크립트를 실행시키고 있는데 해당 소스 코드에 BP를 걸어봤자 PyCharm에서는 디버깅이 안 됩니다.

이것을 디버깅하려면 아래의 글에서 쓴 방법을 활용해야 합니다. (혹시 또 다른 방법이 있다면 덧글 부탁드립니다. ^^)

PyCharm - (반대로) 원격 프로세스가 PyCharm에 디버그 연결하는 방법
; https://www.sysnet.pe.kr/2/0/12800

따라서, "Python Debug Server" 유형의 "Run/Debug Configuration"을 하나 추가하고 execl로 실행하는 (위의 경우 calc.py) 소스 코드에는 이런 식으로 pydevd.settrace 코드를 호출해 주어야 합니다.

import os
import pydevd

pydevd.settrace('...pycharm의 Python Debug Server의 주소...', port=14444, stdoutToServer=True, stderrToServer=True)

print('calc.py', os.getpid())

그런데, 다시 한번 강조하지만 자식 프로세스가 디버깅하는 동안 (fork의) 부모 프로세스가 살아 있어야 하므로, 반드시 waitpid와 같은 방법을 써야 합니다.

...[생략]...

if pid > 0:
    print('부모 프로세스의 실행 흐름', os.getpid())
    os.waitpid(pid, 0)  # 자식 프로세스가 종료할 때까지 대기

...[생략]...

또는, fork가 아닌 subprocess.Popen으로 하면,

import os
import sys
import subprocess

pid = os.fork()

if pid > 0:
    print('부모 프로세스의 실행 흐름', os.getpid())
    os.waitpid(pid, 0)  # 자식 프로세스가 종료할 때까지 대기
elif pid == 0:
    print('자식 프로세스의 실행 흐름', os.getpid())
    subprocess.Popen([sys.executable, 'calc.py'])

else:  # pid < 0
    print('fork 오류')

print('모두 호출', os.getpid())

이렇게 해도 calc.py에 있는 pydevd.settrace를 통한 디버깅 연결이 안 됩니다. 왜냐하면, 위의 경우에는 subprocess.Popen으로 곧바로 실행 반환이 되므로 해당 서브 프로세스(python calc.py)의 디버깅을 하는 동안 그것의 부모 프로세스인 fork의 자식 프로세스가 마찬가지로 종료가 돼 결국 그것의 자식 프로세스인 "python calc.py"도 종료되기 때문에 디버깅이 해제됩니다.

따라서, 위와 같이 Popen을 사용하면 (fork 된) 자식 프로세스에서도 waitpid를 해줘야 합니다.

import os
import sys
import subprocess

pid = os.fork()

if pid > 0:
    print('부모 프로세스의 실행 흐름', os.getpid())
    os.waitpid(pid, 0)  # 자식 프로세스가 종료할 때까지 대기
elif pid == 0:
    print('자식 프로세스의 실행 흐름', os.getpid())
    child_process = subprocess.Popen([sys.executable, 'calc.py'])
    os.waitpid(child_process.pid, 0)  # 자식의 자식 프로세스가 종료할 때까지 대기

else:  # pid < 0
    print('fork 오류')

print('모두 호출', os.getpid())

이 정도면 대충 느낌 아시겠죠?!!!




참고로, 다음은 uwsgi를 execl로 실행한 경우입니다.

import os
import sys

pid = os.fork()

test_args = ['uwsgi', '--http', ':18090', '--chdir', '/mnt/d/pycharm/work/testconsole', '--wsgi-file', 'calc.py']

if pid > 0:
    os.execl('/usr/local/bin/uwsgi', *test_args)
elif pid == 0:
else:  # pid < 0
    print('fork 오류')

저 경우에도 calc.py에서 pydevd.settrace를 제공하면 정상적으로 Python Debug Server에 연결해 디버깅이 진행됩니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 9/2/2021]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 91  92  93  94  95  96  97  98  99  100  101  102  103  [104]  105  ...
NoWriterDateCnt.TitleFile(s)
11325정성태10/14/201719286.NET Framework: 689. CLR 4.0 환경에서 DLL 모듈의 로드 주소(Base address) 알아내는 방법
11324정성태10/13/201720862디버깅 기술: 101. windbg - "*** WARNING: Unable to verify checksum for" 경고 없애는 방법
11322정성태10/13/201718277디버깅 기술: 100. windbg - .NET 4.0 응용 프로그램의 Main 메서드에 Breakpoint 걸기
11321정성태10/11/201719800.NET Framework: 688. NGen 모듈과 .NET Profiler
11320정성태10/11/201720593.NET Framework: 687. COR_PRF_USE_PROFILE_IMAGES 옵션과 NGen의 "profiler-enhanced images" [1]
11319정성태10/11/201728161.NET Framework: 686. C# - string 배열을 담은 구조체를 직렬화하는 방법
11318정성태10/7/201720930VS.NET IDE: 122. 비주얼 스튜디오에서 관리자 권한을 요구하는 C# 콘솔 프로그램 제작 [1]
11317정성태10/4/201726103VC++: 120. std::copy 등의 함수 사용 시 _SCL_SECURE_NO_WARNINGS 에러 발생
11316정성태9/30/201724144디버깅 기술: 99. (닷넷) 프로세스(EXE)에 디버거가 연결되어 있는지 아는 방법 [4]
11315정성태9/29/201740227기타: 68. "시작하세요! C# 6.0 프로그래밍: 기본 문법부터 실전 예제까지" 구매하신 분들을 위한 C# 7.0/7.1 추가 문법 PDF [8]
11314정성태9/28/201722005디버깅 기술: 98. windbg - 덤프 파일로부터 닷넷 버전 확인하는 방법
11313정성태9/25/201719303디버깅 기술: 97. windbg - 메모리 덤프로부터 DateTime 형식의 값을 알아내는 방법파일 다운로드1
11312정성태9/25/201722402.NET Framework: 685. C# - 구조체(값 형식)의 필드를 리플렉션을 이용해 값을 바꾸는 방법파일 다운로드1
11311정성태9/20/201716834.NET Framework: 684. System.Diagnostics.Process 객체의 명시적인 해제 권장
11310정성태9/19/201720259.NET Framework: 683. WPF의 Window 객체를 생성했는데 GC 수집 대상이 안 되는 이유 [3]
11309정성태9/13/201718390개발 환경 구성: 335. Octave의 명령 창에서 실행한 결과를 복사하는 방법
11308정성태9/13/201719442VS.NET IDE: 121. 비주얼 스튜디오에서 일부 텍스트 파일을 무조건 메모장으로만 여는 문제파일 다운로드1
11307정성태9/13/201721974오류 유형: 421. System.Runtime.InteropServices.SEHException - 0x80004005
11306정성태9/12/201720029.NET Framework: 682. 아웃룩 사용자를 위한 중국어 스팸 필터 Add-in
11305정성태9/12/201721524개발 환경 구성: 334. 기존 프로젝트를 Visual Studio를 이용해 Github의 신규 생성된 repo에 올리는 방법 [1]
11304정성태9/11/201718649개발 환경 구성: 333. 3ds Max를 Hyper-V VM에서 실행하는 방법
11303정성태9/11/201721958개발 환경 구성: 332. Inno Setup 파일의 관리자 권한을 제거하는 방법
11302정성태9/11/201718183개발 환경 구성: 331. SQL Server Express를 위한 방화벽 설정
11301정성태9/11/201717085오류 유형: 420. SQL Server Express 연결 오류 - A network-related or instance-specific error occurred while establishing a connection to SQL Server.
11300정성태9/10/201720948.NET Framework: 681. dotnet.exe - run, exec, build, restore, publish 차이점 [3]
11299정성태9/9/201719679개발 환경 구성: 330. Hyper-V VM의 Internal Network를 Private 유형으로 만드는 방법
... 91  92  93  94  95  96  97  98  99  100  101  102  103  [104]  105  ...