web-dev-qa-db-ja.com

Windowsマシンでpythonで信号を処理する方法

Windowsで以下に貼り付けたコードを試していますが、シグナルを処理する代わりに、プロセスを強制終了しています。ただし、同じコードがUbuntuで機能しています。

import os, sys
import time
import signal
def func(signum, frame):
    print 'You raised a SigInt! Signal handler called with signal', signum

signal.signal(signal.SIGINT, func)
while True:
    print "Running...",os.getpid()
    time.sleep(2)
    os.kill(os.getpid(),signal.SIGINT)
20
Ramu

Pythonの _os.kill_ は、Windowsで2つの無関係なAPIをラップします。 GenerateConsoleCtrlEventパラメーターが_CTRL_C_EVENT_または_CTRL_BREAK_EVENT_の場合、 sig を呼び出します。この場合、pidパラメーターはプロセスグループIDです。後者の呼び出しが失敗し、他のすべてのsig値については、 OpenProcess を呼び出してから TerminateProcess を呼び出します。この場合、pidパラメーターはプロセスIDであり、sig値は終了コードとして渡されます。 Windowsプロセスの終了は、SIGKILLをPOSIXプロセスに送信することに似ています。通常、プロセスを正常に終了できないため、これは避ける必要があります。

_os.kill_のドキュメントでは、誤って「kill()はさらにプロセスハンドルを強制終了する」と主張していることに注意してください。 OpenProcessを呼び出して、プロセスハンドルを取得します。

SIGINTおよびSIGBREAKの代わりにWinAPI _CTRL_C_EVENT_および_CTRL_BREAK_EVENT_を使用する決定は、クロスプラットフォームコードにとっては残念です。また、プロセスグループIDではないプロセスIDが渡されたときのGenerateConsoleCtrlEventの動作も定義されていません。プロセスIDを取得するAPIでこの関数を使用することは、せいぜい疑わしく、潜在的に非常に間違っています。

特定のニーズに合わせて、_os.kill_をクロスプラットフォームコードにとってもう少し使いやすくするアダプター関数を作成できます。例えば:

_import os
import sys
import time
import signal

if sys.platform != 'win32':
    kill = os.kill
    sleep = time.sleep
else: 
    # adapt the conflated API on Windows.
    import threading

    sigmap = {signal.SIGINT: signal.CTRL_C_EVENT,
              signal.SIGBREAK: signal.CTRL_BREAK_EVENT}

    def kill(pid, signum):
        if signum in sigmap and pid == os.getpid():
            # we don't know if the current process is a
            # process group leader, so just broadcast
            # to all processes attached to this console.
            pid = 0
        thread = threading.current_thread()
        handler = signal.getsignal(signum)
        # work around the synchronization problem when calling
        # kill from the main thread.
        if (signum in sigmap and
            thread.name == 'MainThread' and
            callable(handler) and
            pid == 0):
            event = threading.Event()
            def handler_set_event(signum, frame):
                event.set()
                return handler(signum, frame)
            signal.signal(signum, handler_set_event)                
            try:
                os.kill(pid, sigmap[signum])
                # busy wait because we can't block in the main
                # thread, else the signal handler can't execute.
                while not event.is_set():
                    pass
            finally:
                signal.signal(signum, handler)
        else:
            os.kill(pid, sigmap.get(signum, signum))

    if sys.version_info[0] > 2:
        sleep = time.sleep
    else:
        import errno

        # If the signal handler doesn't raise an exception,
        # time.sleep in Python 2 raises an EINTR IOError, but
        # Python 3 just resumes the sleep.

        def sleep(interval):
            '''sleep that ignores EINTR in 2.x on Windows'''
            while True:
                try:
                    t = time.time()
                    time.sleep(interval)
                except IOError as e:
                    if e.errno != errno.EINTR:
                        raise
                interval -= time.time() - t
                if interval <= 0:
                    break

def func(signum, frame):
    # note: don't print in a signal handler.
    global g_sigint
    g_sigint = True
    #raise KeyboardInterrupt

signal.signal(signal.SIGINT, func)

g_kill = False
while True:
    g_sigint = False
    g_kill = not g_kill
    print('Running [%d]' % os.getpid())
    sleep(2)
    if g_kill:
        kill(os.getpid(), signal.SIGINT)
    if g_sigint:
        print('SIGINT')
    else:
        print('No SIGINT')
_

討論

Windowsは、システムレベル[*]でシグナルを実装しません。 MicrosoftのCランタイムは、標準Cに必要な6つの信号を実装しています:SIGINTSIGABRTSIGTERMSIGSEGVSIGILLSIGFPE

SIGABRTおよびSIGTERMは、現在のプロセスに対してのみ実装されます。 C raise を介してハンドラーを呼び出すことができます。例(Python 3.5):

_>>> import signal, ctypes
>>> ucrtbase = ctypes.CDLL('ucrtbase')
>>> c_raise = ucrtbase['raise']
>>> foo = lambda *a: print('foo')
>>> signal.signal(signal.SIGTERM, foo)
<Handlers.SIG_DFL: 0>
>>> c_raise(signal.SIGTERM)
foo
0
_

SIGTERMは無意味です。

SIGABRT 関数はハンドラーが戻るとプロセスを強制終了するため、シグナルモジュールを使用してabortで多くのことを行うことはできません。メインスレッドで呼び出される登録済みPython callableのフラグ)。 Python 3の場合、代わりに faulthandler モジュールを使用できます。または、ctypesを介してCRTの signal 関数を呼び出して、ctypesコールバックをハンドラーとして設定します。

CRTは、対応するWindows例外にWindows structured exception handler を設定することにより、SIGSEGVSIGILL、およびSIGFPEを実装します。

_STATUS_ACCESS_VIOLATION          SIGSEGV
STATUS_ILLEGAL_INSTRUCTION       SIGILL
STATUS_PRIVILEGED_INSTRUCTION    SIGILL
STATUS_FLOAT_DENORMAL_OPERAND    SIGFPE
STATUS_FLOAT_DIVIDE_BY_ZERO      SIGFPE
STATUS_FLOAT_INEXACT_RESULT      SIGFPE
STATUS_FLOAT_INVALID_OPERATION   SIGFPE
STATUS_FLOAT_OVERFLOW            SIGFPE
STATUS_FLOAT_STACK_CHECK         SIGFPE
STATUS_FLOAT_UNDERFLOW           SIGFPE
STATUS_FLOAT_MULTIPLE_FAULTS     SIGFPE
STATUS_FLOAT_MULTIPLE_TRAPS      SIGFPE
_

これらの信号のCRTの実装は、Pythonの信号処理と互換性がありません。例外フィルターは、登録されたハンドラーを呼び出し、 _EXCEPTION_CONTINUE_EXECUTION_ を返します。ただし、Pythonのハンドラーは、メインスレッドでしばらくしてから、インタープリターが登録済みの呼び出し可能オブジェクトを呼び出すためのフラグをトリップするだけです。したがって、例外をトリガーした誤ったコードは、無限ループでトリガーし続けます。 Python 3では、これらの例外ベースの信号にfaulthandlerモジュールを使用できます。

これにより、SIGINTが残り、Windowsには非標準のSIGBREAKが追加されます。コンソールプロセスと非コンソールプロセスの両方がこれらのシグナルをraiseできますが、別のプロセスからそれらを受信できるのはコンソールプロセスだけです。 CRTは、 SetConsoleCtrlHandler を介してコンソールコントロールイベントハンドラーを登録することでこれを実装します。

コンソールは、kernel32.dllまたはkernelbase.dll(文書化されていない)のCtrlRoutineで実行を開始する接続プロセスで新しいスレッドを作成することにより、制御イベントを送信します。ハンドラーがメインスレッドで実行されない場合、同期の問題が発生する可能性があります(たとえば、REPLまたはinputで)。また、制御オブジェクトは、同期オブジェクトの待機中または同期I/Oの完了を待機中にブロックされた場合、メインスレッドを中断しません。 SIGINTによって割り込み可能である必要がある場合、メインスレッドでのブロックを回避するように注意する必要があります。 Python 3は、Windowsイベントオブジェクトを使用してこの問題を回避しようとします。このイベントオブジェクトは、SIGINTによって割り込み可能な待機でも使用できます。

コンソールがプロセスに_CTRL_C_EVENT_または_CTRL_BREAK_EVENT_を送信すると、CRTのハンドラーは登録されたSIGINTまたはSIGBREAKハンドラーをそれぞれ呼び出します。 SIGBREAKハンドラーは、ウィンドウが閉じられたときにコンソールが送信する_CTRL_CLOSE_EVENT_に対しても呼び出されます。 Pythonは、メインスレッドでSIGINTをrasingすることで、デフォルトでKeyboardInterruptを処理します。ただし、SIGBREAKは、最初はExitProcess(STATUS_CONTROL_C_EXIT)を呼び出すデフォルトの_CTRL_BREAK_EVENT_ハンドラーです。

GenerateConsoleCtrlEventを介して、現在のコンソールに接続されているすべてのプロセスに制御イベントを送信できます。これは、プロセスグループに属するプロセスのサブセットをターゲットにするか、ターゲットグループ0を使用して、現在のコンソールに接続されているすべてのプロセスにイベントを送信できます。

プロセスグループは、Windows APIの十分に文書化された側面ではありません。プロセスのグループを照会するパブリックAPIはありませんが、Windowsセッション内のすべてのプロセスは、たとえwininit.exeグループ(サービスセッション)またはwinlogon.exeグループ(インタラクティブセッション)であっても、プロセスグループに属します。新しいプロセスを作成するときに作成フラグ_CREATE_NEW_PROCESS_GROUP_を渡すと、新しいグループが作成されます。グループIDは、作成されたプロセスのプロセスIDです。私の知る限り、コンソールはプロセスグループを使用する唯一のシステムであり、それはGenerateConsoleCtrlEvent専用です。

ターゲットIDがプロセスグループIDでない場合のコンソールの動作は未定義であり、依存しないでください。プロセスとその親プロセスの両方がコンソールに接続されている場合、制御イベントの送信は基本的にターゲットがグループ0のように動作します。親プロセスが現在のコンソールに接続されていない場合、GenerateConsoleCtrlEventは失敗し、_os.kill_はTerminateProcessを呼び出します。奇妙なことに、「システム」プロセス(PID 4)とその子プロセスsmss.exe(セッションマネージャー)をターゲットにした場合、呼び出しは成功しますが、ターゲットが接続プロセスのリストに誤って追加される以外は何も起こりません(つまり GetConsoleProcessList )。これはおそらく、親プロセスが「アイドル」プロセスであるためです。これは、PID 0であるため、ブロードキャストPGIDとして暗黙的に受け入れられます。親プロセスルールは、非コンソールプロセスにも適用されます。非コンソールの子プロセスを対象とすることは何もしません-接続されていないプロセスを追加することによって誤ってコンソールプロセスリストを破損することを除いて。制御イベントは、グループ0または_CREATE_NEW_PROCESS_GROUP_で作成したknownプロセスグループのいずれかにのみ送信する必要があることを明確にしたいと思います。

_CTRL_C_EVENT_をグループ0以外に送信できることに依存しないでください。これは、新しいプロセスグループで最初に無効になっているためです。このイベントを新しいグループに送信することは不可能ではありませんが、ターゲットプロセスは最初にSetConsoleCtrlHandler(NULL, FALSE)を呼び出して_CTRL_C_EVENT_を有効にする必要があります。

_CTRL_BREAK_EVENT_は無効にできないため、信頼できるすべてです。このイベントの送信は、Windows _CREATE_NEW_PROCESS_GROUP_またはC SIGBREAKハンドラーがある場合、_CTRL_BREAK_EVENT_で開始された子プロセスを簡単に強制終了する簡単な方法です。そうでない場合、デフォルトのハンドラーはプロセスを終了し、終了コードを_STATUS_CONTROL_C_EXIT_に設定します。例えば:

_>>> import os, signal, subprocess
>>> p = subprocess.Popen('python.exe',
...         stdin=subprocess.PIPE,
...         creationflags=subprocess.CREATE_NEW_PROCESS_GROUP)
>>> os.kill(p.pid, signal.CTRL_BREAK_EVENT)
>>> STATUS_CONTROL_C_EXIT = 0xC000013A
>>> p.wait() == STATUS_CONTROL_C_EXIT
True
_

_CTRL_BREAK_EVENT_は現在のプロセスに送信されていないことに注意してください。この例では、子プロセスのプロセスグループ(コンソールに接続されているすべての子プロセスなど)を対象としているためです。例がグループ0を使用していた場合、SIGBREAKハンドラーを定義しなかったため、現在のプロセスも強制終了されます。試してみましょうが、ハンドラーセットを使用します。

_>>> ctrl_break = lambda *a: print('^BREAK')
>>> signal.signal(signal.SIGBREAK, ctrl_break)
<Handlers.SIG_DFL: 0>
>>> os.kill(0, signal.CTRL_BREAK_EVENT)
^BREAK
_

[*]

Windowsには、 非同期プロシージャコール (APC)があり、ターゲット関数をスレッドにキューイングします。特にカーネルモードAPCの役割を明確にするために、Windows APCの詳細な分析については、記事 Inside NT's Asynchronous Procedure Call を参照してください。 QueueUserAPC を介して、ユーザーモードAPCをスレッドのキューに入れることができます。また、 ReadFileEx および WriteFileEx によってI/O完了ルーチンのキューに入れられます。

ユーザーモードのAPCは、スレッドがアラート可能な待機に入ると実行されます(例: WaitForSingleObjectEx または SleepEx with bAlertable as TRUE)。一方、カーネルモードAPCは、すぐにディスパッチされます(IRQLが_APC_LEVEL_を下回る場合)。 I/Oマネージャーは通常、要求を発行したスレッドのコンテキストで非同期I/O要求パケットを完了するために使用します(データをIRPからユーザーモードバッファーにコピーするなど)。 APCがアラート可能およびアラート不可待機に与える影響を示す表については、 Waits and APCs を参照してください。カーネルモードAPCは待機を中断せず、待機ルーチンによって内部的に実行されることに注意してください。

WindowsはAPCを使用してPOSIXのような信号を実装できますが、実際には同じ目的で他の手段を使用します。例えば:

ウィンドウメッセージは、呼び出し元の スレッドのdesktop を共有し、同じまたはより低い整合性レベルにあるすべてのスレッドに送信およびポストできます。ウィンドウメッセージを送信すると、システムキューに入れられ、スレッドが PeekMessage または GetMessage を呼び出すときにウィンドウプロシージャを呼び出します。メッセージを投稿すると、そのメッセージがスレッドのメッセージキューに追加されます。メッセージキューのデフォルトの割り当ては10,000メッセージです。メッセージキューのあるスレッドには、GetMessageおよび DispatchMessage を介してキューを処理するメッセージループが必要です。通常、コンソールのみのプロセスのスレッドにはメッセージキューがありません。ただし、コンソールのホストプロセスであるconhost.exeは明らかにそうです。閉じるボタンがクリックされるか、コンソールのプライマリプロセスがタスクマネージャーまたは taskkill.exe を介して強制終了されると、_WM_CLOSE_メッセージがメッセージキューに投稿されますコンソールウィンドウのスレッドの。コンソールは、_CTRL_CLOSE_EVENT_を接続されているすべてのプロセスに順番に送信します。プロセスがイベントを処理する場合、強制的に終了する前に正常に終了するまで5秒が与えられます。

64
Eryk Sun