프로그래밍/Python

[Python] 윈도우(windows)에서 시그널(signal) 다루기

채윤아빠 2022. 4. 20. 22:12
728x90
반응형

윈도우(windows) 시그널(signal)

파이썬으로 개발할 때, 멀티 프로세서 서로 다른 프로세스로 작업을 나누어 처리할 경우가 종종 있는데 소켓 통신 등을 사용하지 않고 간단하게 프로세스간 시그널(signal)을 주고 받는 방법을 사용할 수 있는데, 윈도우(windows) 개발 환경에서는 몇 가지 제약이 있습니다.

리눅스 등에서는 특정 프로세스에 시그널(signal)을 보낼 때, os.kill(pid, sig) 함수를 이용하면 됩니다. 하지만 윈도우 환경에서 os.kill() 함수를 호출하면 해당 프로세스가 바로 종료되면서 두 번째 인자로 전달된 시그널(signal) 값이 반환됩니다.

윈도우 환경에서 시그널을 보내기 위해서는 psutil 패키지의 Process.send_signal() 함수를 이용해야 하는데, 그나마도 SIGTERM, CTRL_C_EVENT, CTRL_BREAK_EVENT 등 3개의 시그널(signal)만 보낼 수 있고, 그 외의 시그널을 보내려고 하면 다음과 같이 오류가 발생합니다.

Traceback (most recent call last):
  File "d:\Dev\Python\python-test\multi-processing\multi-proc-signal2.py", line 41, in 
    proc.send_signal(signal.SIGINT)
  File "d:\Dev\Python\venv\cv2\lib\site-packages\psutil\__init__.py", line 278, in wrapper
    return fun(self, *args, **kwargs)
  File "d:\Dev\Python\venv\cv2\lib\site-packages\psutil\__init__.py", line 1205, in send_signal
    self._proc.send_signal(sig)
  File "d:\Dev\Python\venv\cv2\lib\site-packages\psutil\_pswindows.py", line 688, in wrapper
    return fun(self, *args, **kwargs)
  File "d:\Dev\Python\venv\cv2\lib\site-packages\psutil\_pswindows.py", line 883, in send_signal
    raise ValueError(ValueError: only SIGTERM, CTRL_C_EVENT and CTRL_BREAK_EVENT signals are supported on Windows

윈도우에서 의미가 있는 시그널(signal) 핸들러는 SIGINT, SIGBREAK 등 두 가지로 각각은 CTRL_C_EVENT, CTRL_BREAK_EVENT 시그널(signal)에 대응됩니다.


SIGBREAK 시그널 핸들러

아래 예제는 SIGBREAK 시그널 핸들러를 지정한 예제입니다.

from multiprocessing import Process

import psutil
import signal
import time

_terminated = False

def new_handler(signum, frame) -> None:
    global _terminated
    print(f'Signal handler called with signal : {signum}')
    _terminated = True


def multiproc():
    signal.signal(signal.SIGBREAK, new_handler)
    index = 0
    while (not _terminated):
        time.sleep(0.5)
        index += 1
        print(f'index = {index}')
    print(f'multiproc() end...')


if __name__ == '__main__':
    print('sub-process started...')
    p = Process(target=multiproc)
    p.start()
    print('wait 3 second...')
    time.sleep(3)
    proc = psutil.Process(p.pid)
    proc.send_signal(signal.CTRL_BREAK_EVENT)

    p.join()

위 예제를 실행한 결과는 다음과 같습니다.

sub-process started...
wait 3 second...
index = 1
index = 2
index = 3
index = 4
index = 5
index = 6
Signal handler called with signal : 21
multiproc() end...

SIGINT 시그널 핸들러

아래 예제는 SIGINT 시그널 핸들러를 지정한 예제입니다.

from multiprocessing import Process

import psutil
import signal
import time

_terminated = False

def new_handler(signum, frame) -> None:
    global _terminated
    print(f'Signal handler called with signal : {signum}')
    _terminated = True


def multiproc():
    signal.signal(signal.SIGINT, new_handler)
    index = 0
    while (not _terminated):
        time.sleep(0.5)
        index += 1
        print(f'index = {index}')
    print(f'multiproc() end...')


if __name__ == '__main__':
    print('sub-process started...')
    p = Process(target=multiproc)
    p.start()
    print('wait 3 second...')
    time.sleep(3)
    proc = psutil.Process(p.pid)
    proc.send_signal(signal.CTRL_C_EVENT)

    p.join()

위 예제를 실행한 결과는 다음과 같습니다.

sub-process started...
wait 3 second...
index = 1
index = 2
index = 3
index = 4
index = 5
index = 6
Signal handler called with signal : 2
multiproc() end...
Traceback (most recent call last):
  File "d:\Dev\Python\python-test\multi-processing\win-signal-SIGINT.py", line 43, in 
    p.join()
  File "C:\Dev\Python\Python39\lib\multiprocessing\process.py", line 149, in join 
    res = self._popen.wait(timeout)
  File "C:\Dev\Python\Python39\lib\multiprocessing\popen_spawn_win32.py", line 108, in wait
    res = _winapi.WaitForSingleObject(int(self._handle), msecs)
KeyboardInterrupt

SIGBREAK 시그널 핸들러와는 실행 결과가 다른데, 자식 프로세스에 발생시킨 SIGINT 시그널이 부모 프로세스에도 전달되어 KeyboardInterrupt Exception이 발생하였습니다.


SIGTERM 시그널 핸들러

아래 예제는 SIGTERM 시그널을 보내는 예제입니다.

from multiprocessing import Process

import psutil
import signal
import time


_terminated = False


def new_handler(signum, frame) -> None:
    global _terminated
    print(f'Signal handler called with signal : {signum}')
    _terminated = True


def multiproc():
    signal.signal(signal.SIGTERM, new_handler)
    index = 0
    while (not _terminated):
        time.sleep(0.5)
        index += 1
        print(f'index = {index}')
    print(f'multiproc() end...')


if __name__ == '__main__':
    print('sub-process started...')
    p = Process(target=multiproc)
    p.start()
    print('wait 3 second...')
    time.sleep(3)
    proc = psutil.Process(p.pid)
    proc.send_signal(signal.SIGTERM)

    p.join()

위 예제의 실행 결과는 다음과 같습니다.

sub-process started...
wait 3 second...
index = 1
index = 2
index = 3
index = 4
index = 5

SIGTERM 시그널을 보내면, 자식 프로세스를 os.kill() 한 것과 동일하게 바로 해당 프로세스가 terminate 처리되어 설정한 핸들러가 의미가 없습니다.


맺음말

윈도우 환경에서 사용할만한 시그널은 SIGBREAK 하나로 프로세스간 주고 받을 시그널이 2개 이상일 경우에는 소켓 통신 등 다른 방법을 고려해야 합니다.


참고자료