signal – 非同期のシステムイベント通知を受け取る

目的:非同期のシステムイベント通知を受け取る
利用できるバージョン:1.4 以上

Note

Unix シグナルハンドラでプログラミングすることは普通以上の努力が必要です。本稿はシグナルの入門です。全てのプラットホーム上で正しくシグナルを利用することの詳細は説明していません。Unix のバージョンを横断して標準仕様があり、また変形した仕様もあります。そのため、実行して問題があれば使用中の OS のドキュメントを調べてください。

シグナルはあるイベントをプログラムへ通知するオペレーティングシステムの機能で、非同期にその通知を受け取ります。シグナルはそのシステムが生成するか、1つのプロセスから他のプロセスへ送信されます。シグナルはプログラムの通常処理の流れを中断させるので、シグナルを受信したら何らかの操作(特に I/O)でエラーを発生させることができます。

シグナルは整数値で識別され、オペレーティングシステムの C 言語のヘッダに定義されています。Python は signal モジュールのシンボルとしてプラットホームに適したシグナルを定義します。 SIGINTSIGUSR1 を使用する例を次に示します。両方とも全ての Unix や Unix に似たシステムで定義されています。

シグナルを受け取る

イベントベースプログラミングの別の形態として、あるシグナルが発生したときに実行される シグナルハンドラ というコールバック関数を設定することでそのシグナルを受け取れます。シグナルハンドラへの引数はシグナル番号とシグナルによって中断されたプログラム位置のスタックフレームです。

import signal
import os
import time

def receive_signal(signum, stack):
    print 'Received:', signum

signal.signal(signal.SIGUSR1, receive_signal)
signal.signal(signal.SIGUSR2, receive_signal)

print 'My PID is:', os.getpid()

while True:
    print 'Waiting...'
    time.sleep(3)

この例は数秒毎に停止しながら無限ループする比較的シンプルなものです。シグナルが発生したとき sleep 呼び出しは中断されて、シグナルハンドラ receive_signal() がシグナル番号を表示します。シグナルハンドラが返される場合、ループは継続します。

実行中のプログラムへシグナルを送信するために kill コマンドを使用します。次の出力を生成するために1つのウィンドウで signal_signal.py を実行しました。その後、別のウィンドウで kill -USR1 $pid , kill -USR2 $pid , kill -INT $pid を実行しました。

$ python signal_signal.py
My PID is: 71387
Waiting...
Waiting...
Waiting...
Received: 30
Waiting...
Waiting...
Received: 31
Waiting...
Waiting...
Traceback (most recent call last):
  File "signal_signal.py", line 25, in <module>
    time.sleep(3)
KeyboardInterrupt

getsignal()

あるシグナルに登録されているシグナルハンドラを確認するには getsignal() を使用してください。そして、引数にシグナル番号を渡してください。返り値は登録されたハンドラか、 signal.SIG_IGN (シグナルが無視された場合), signal.SIG_DFL (デフォルト動作が用いられた)の特殊な値のどれかか、又は None (存在するシグナルハンドラが Python ではなく C で登録されていた場合)です。

import signal

def alarm_received(n, stack):
    return

signal.signal(signal.SIGALRM, alarm_received)

signals_to_names = {}
for n in dir(signal):
    if n.startswith('SIG') and not n.startswith('SIG_'):
        signals_to_names[getattr(signal, n)] = n

for s, name in sorted(signals_to_names.items()):
    handler = signal.getsignal(s)
    if handler is signal.SIG_DFL:
        handler = 'SIG_DFL'
    elif handler is signal.SIG_IGN:
        handler = 'SIG_IGN'
    print '%-10s (%2d):' % (name, s), handler

大事なことなので2回言いますが、それぞれの OS は違ったシグナルを定義しているかもしれないので、他のシステムでこのサンプルコードを実行すると出力結果は変わるかもしれません。次の実行結果は OS X のものです。

$ python signal_getsignal.py
SIGHUP     ( 1): SIG_DFL
SIGINT     ( 2): &lt;built-in function default_int_handler&gt;
SIGQUIT    ( 3): SIG_DFL
SIGILL     ( 4): SIG_DFL
SIGTRAP    ( 5): SIG_DFL
SIGIOT     ( 6): SIG_DFL
SIGEMT     ( 7): SIG_DFL
SIGFPE     ( 8): SIG_DFL
SIGKILL    ( 9): None
SIGBUS     (10): SIG_DFL
SIGSEGV    (11): SIG_DFL
SIGSYS     (12): SIG_DFL
SIGPIPE    (13): SIG_IGN
SIGALRM    (14): &lt;function alarm_received at 0x7c3f0&gt;
SIGTERM    (15): SIG_DFL
SIGURG     (16): SIG_DFL
SIGSTOP    (17): None
SIGTSTP    (18): SIG_DFL
SIGCONT    (19): SIG_DFL
SIGCHLD    (20): SIG_DFL
SIGTTIN    (21): SIG_DFL
SIGTTOU    (22): SIG_DFL
SIGIO      (23): SIG_DFL
SIGXCPU    (24): SIG_DFL
SIGXFSZ    (25): SIG_IGN
SIGVTALRM  (26): SIG_DFL
SIGPROF    (27): SIG_DFL
SIGWINCH   (28): SIG_DFL
SIGINFO    (29): SIG_DFL
SIGUSR1    (30): SIG_DFL
SIGUSR2    (31): SIG_DFL

シグナルを送る

シグナルを送るための関数は os.kill() です。その使用方法は PyMOTW の os モジュール や Creating Processes with os.fork() で説明しています。

アラーム

アラームは一定時間が経過した後にプログラムがそのことを通知するために OS へ依頼する特殊なシグナルの種類です。 os の標準モジュールドキュメント によると、I/O 操作や他のシステムコールが永久にブロッキングしないようにするために役に立ちます。

import signal
import time

def receive_alarm(signum, stack):
    print 'Alarm :', time.ctime()

# 2秒で receive_alarm を呼び出す
signal.signal(signal.SIGALRM, receive_alarm)
signal.alarm(2)

print 'Before:', time.ctime()
time.sleep(4)
print 'After :', time.ctime()

この例では sleep() の呼び出しは4秒間継続されません。

$ python signal_alarm.py
Before: Sun Aug 17 10:51:09 2008
Alarm : Sun Aug 17 10:51:11 2008
After : Sun Aug 17 10:51:11 2008

シグナルを無視する

シグナルを無視するためにはハンドラとして SIG_IGN を登録します。このスクリプトは SIGINT のデフォルトハンドラを SIG_IGN で置き換えて SIGUSR1 のために登録します。その後、受け取るシグナルを待つために signal.pause() を使用します。

import signal
import os
import time

def do_exit(sig, stack):
    raise SystemExit('Exiting')

signal.signal(signal.SIGINT, signal.SIG_IGN)
signal.signal(signal.SIGUSR1, do_exit)

print 'My PID:', os.getpid()

signal.pause()

通常では SIGINT (Ctrl-C を入力したとき、シェルからプログラムへ送信されるシグナル)は KeyboardInterrupt を発生させます。この例では SIGINT を無視して SIGUSR1 を受け取ったときに SystemExit を発生させます。 ^C はターミナルからそのスクリプトを終了させようと Ctrl-C を入力したことを表しています。 kill -USR1 72598 を他のターミナルから実行することで最終的にそのスクリプトを終了させます。

$ python signal_ignore.py
My PID: 72598
^C^C^C^CExiting

シグナルとスレッド

シグナルとスレッドを同時に使用することは一般的にうまくいきません。1つのプロセスのメインスレッドのみがシグナルを受け取るからです。次の例では、1つのスレッドでシグナルを待つシグナルハンドラをセットします。そして、他の一方からシグナルを送ります。

import signal
import threading
import os
import time

def signal_handler(num, stack):
    print 'Received signal %d in %s' % (num, threading.currentThread())

signal.signal(signal.SIGUSR1, signal_handler)

def wait_for_signal():
    print 'Waiting for signal in', threading.currentThread()
    signal.pause()
    print 'Done waiting'

# シグナルを受け取らないスレッドを開始する
receiver = threading.Thread(target=wait_for_signal, name='receiver')
receiver.start()
time.sleep(0.1)

def send_signal():
    print 'Sending signal in', threading.currentThread()
    os.kill(os.getpid(), signal.SIGUSR1)

sender = threading.Thread(target=send_signal, name='sender')
sender.start()
sender.join()

# シグナルを確認するためにスレッドを待ちます(発生しない!)
print 'Waiting for', receiver
signal.alarm(2)
receiver.join()

複数のシグナルハンドラがメインスレッドで全て登録されていることに注意してください。これはスレッドとシグナルを同時に使用するために根本となるプラットホームの機能に関係なく、Python の signal モジュール実装の必須条件です。receiver スレッドは signal.pause() を呼び出しますが、そのスレッドはシグナルを受け取りません。receiver スレッドは決して終了しないので永久にブロッキングしないようにサンプルコードの最後付近で signal.alarm(2) を呼び出します。

$ python signal_threads.py
Waiting for signal in <Thread(receiver, started)>
Sending signal in <Thread(sender, started)>
Received signal 30 in <_MainThread(MainThread, started)>
Waiting for <Thread(receiver, started)>
Alarm clock

アラームはスレッドの中でセットされていますが、そのシグナルもまたメインスレッドで受け取られます。

import signal
import time
import threading

def signal_handler(num, stack):
    print time.ctime(), 'Alarm in', threading.currentThread()

signal.signal(signal.SIGALRM, signal_handler)

def use_alarm():
    print time.ctime(), 'Setting alarm in', threading.currentThread()
    signal.alarm(1)
    print time.ctime(), 'Sleeping in', threading.currentThread()
    time.sleep(3)
    print time.ctime(), 'Done with sleep'

# シグナルを受け取らないスレッドを開始する
alarm_thread = threading.Thread(target=use_alarm, name='alarm_thread')
alarm_thread.start()
time.sleep(0.1)

# シグナルを確認するためにスレッドを待ちます(発生しない!)
print time.ctime(), 'Waiting for', alarm_thread
alarm_thread.join()

print time.ctime(), 'Exiting normally'

そのアラームは use_alarm()sleep() 呼び出しを中断させないことに注意してください。

$ python signal_threads_alarm.py
Sun Aug 17 12:06:00 2008 Setting alarm in <Thread(alarm_thread, started)>
Sun Aug 17 12:06:00 2008 Sleeping in <Thread(alarm_thread, started)>
Sun Aug 17 12:06:00 2008 Waiting for <Thread(alarm_thread, started)>;
Sun Aug 17 12:06:03 2008 Done with sleep
Sun Aug 17 12:06:03 2008 Alarm in <_MainThread(MainThread, started)>
Sun Aug 17 12:06:03 2008 Exiting normally

See also

signal
本モジュールの標準ライブラリドキュメント
Creating Processes with os.fork()
kill() 関数はプロセス間でシグナルを送るために使用されます。
Bookmark and Share