Pythonで学ぶ「詳解 UNIXプログラミング」(その10) 第10章 シグナル
10.1 SIGUSR1とSIGUSR2を捕捉する簡単なプログラム
import signal def sigusr(signo, frame): # one signal handler for both signals print "received", "SIGUSR1" if signo == signal.SIGUSR1 else "SIGUSR2" signal.signal(signal.SIGUSR1, sigusr) signal.signal(signal.SIGUSR2, sigusr) while True: signal.pause()
10.2 シグナルハンドラからの再入不可能な関数の呼び出し
import signal, pwd def my_alarm(signo, frame): print "in signal handler" rec = pwd.getpwnam("root") signal.alarm(1) signal.signal(signal.SIGALRM, my_alarm) signal.alarm(1) while True: rec = pwd.getpwnam("ishimoto") if rec.pw_name != "ishimoto": print "return value corrupted"
実は、Python版のこのスクリプトは問題なく動いてしまう。Pythonのシグナルハンドラは、シグナルを通知された時点で即座に実行されるわけではなく、シグナル通知後、Pythonインタープリタがバイトコード実行中に実行されるようになっている。
この例で言えば、getpwnam()
を開始してからPythonがその結果を読み取って結果を返すまでの間にSIGALRM
が発生しても、シグナルハンドラが実行されるのは pwd.getpwnam("root")
という式が終了してから、ということになる。
しばらくPythonでリエントラントでない関数をシグナルハンドラから呼び出す方法を考えていたのだが、標準ライブラリの範囲では思いつかなかった。
10.3 システムVにおいて正しく動作しないSIGCLDハンドラ
import signal, os, time def sig_cld(signo, frame): print "SIGCLD received" pid, status = os.wait() print "pid = ", pid signal.signal(signal.SIGCLD, sig_cld) pid = os.fork() if pid == 0: # child time.sleep(2) else: # parent signal.pause()
Pythonのシグナルハンドラは再設定する必要がないため、問題は発生しない。Linux上では、C言語版でも問題は発生しないようである。SVR2以降ではどうなんだろうか?Solaris等ではエラーが発生するのだろうか?
10.4 sleepの単純な(不完全な)実装
import signal, time def sig_alrm(signo, frame): pass # nothing to do, just return to wake up the pause def sleep1(nsecs): signal.signal(signal.SIGALRM, sig_alrm) signal.alarm(nsecs) # start the timer signal.pause() # next caught signal wakes us up signal.alarm(0) # turn off timer, return unslept time
10.5, 10.6
setjump()
とか知らん。
10.7 時間切れ付きのreadの呼び出し
import signal, sys, errno def sig_alarm(signo, frame): pass # noting to do, just return to interrupt the read signal.signal(signal.SIGALRM, sig_alarm) signal.alarm(10) try: line = sys.stdin.readline() except IOError, e: if e.errno == errno.EINTR: line = '' finally: signal.alarm(0) print "result:", line
10.8 longjump()を用いた時間切れ付きのreadの呼び出し
longjump()
とか知らん。
10.9 sigaddset, sigdelset, sigismemberの実装
import signal, errno def SIGBAD(signo): """Raise OSError if signo is invalid signal number >>> SIGBAD(1) >>> SIGBAD(0) Traceback (most recent call last): ... OSError: [Errno 22] Invalid signal number >>> SIGBAD(signal.NSIG) Traceback (most recent call last): ... OSError: [Errno 22] Invalid signal number """ if signo <= 0 or signo >= signal.NSIG: raise OSError(errno.EINVAL, "Invalid signal number") def sigaddset(sigset, signo): """Add signal signo to sigset >>> sigaddset(0, 1) 1 >>> sigaddset(1, 16) 32769 >>> sigaddset(0, 0) Traceback (most recent call last): ... OSError: [Errno 22] Invalid signal number """ SIGBAD(signo) sigset = sigset | (1 << (signo-1)) # turn bit on return sigset def sigdelset(sigset, signo): """Delete signal signo from sigset >>> sigdelset(1, 1) 0 >>> sigdelset(3, 2) 1 >>> sigdelset(0, 0) Traceback (most recent call last): ... OSError: [Errno 22] Invalid signal number """ SIGBAD(signo) sigset = sigset & ~(1 << (signo-1)) # turn bit off return sigset def sigismember(sigset, signo): """Tests whether signum is a member of set >>> sigismember(1, 1) 1 >>> sigismember(3, 3) 0 >>> sigdelset(0, 0) Traceback (most recent call last): ... OSError: [Errno 22] Invalid signal number """ SIGBAD(signo) return True if sigset & (1 << (signo-1)) else False if __name__ == "__main__": import doctest doctest.testmod()
そのまま書いても愛嬌がないので、doctestを付けてみた。どっちにしろ、32bit Ubuntu 10.10 では、この実装だとビット数が足りなくて役に立たない。
10.10 プログラムのシグナルマスクを出力する
import signal from ctypes import * libc = CDLL("libc.so.6") _SIGSET_NWORDS = 1024 / (8 * sizeof(c_ulong)) class sigset_t(Structure): _fields_ = [ ("__val", c_ulong*_SIGSET_NWORDS), ] sigprocmask = libc.sigprocmask sigprocmask.argtypes = [c_int, POINTER(sigset_t), POINTER(sigset_t)] sigprocmask.restype = c_int sigismember = libc.sigismember sigismember.argtypes = [POINTER(sigset_t), c_int] sigismember.restype = c_int def pr_mask(str): errno_save = get_errno() # we can be called by signal handlers sigset = sigset_t() if sigprocmask(0, None, byref(sigset)) < 0: raise OSError(get_errno(), '') print str, if sigismember(byref(sigset), signal.SIGINT): print "SIGINT", if sigismember(byref(sigset), signal.SIGQUIT): print "SIGQUIT", if sigismember(byref(sigset), signal.SIGUSR1): print "SIGUSR1", if sigismember(byref(sigset), signal.SIGALRM): print "SIGALRM", # remaining signals can go here print set_errno(errno_save)
Pythonではsigxxxxx
が用意されていないので、ctypes
を使って実装してみた。
以下のサンプルでは、次のコードを posixsignal.py
として保存し、インポートできるようにして試していただきたい。
from ctypes import * libc = CDLL("libc.so.6") _SIGSET_NWORDS = 1024 / (8 * sizeof(c_ulong)) class sigset_t(Structure): _fields_ = [ ("__val", c_ulong*_SIGSET_NWORDS), ] SIG_BLOCK = 0 SIG_UNBLOCK = 1 SIG_SETMASK = 2 sigprocmask = libc.sigprocmask sigprocmask.argtypes = [c_int, POINTER(sigset_t), POINTER(sigset_t)] sigprocmask.restype = c_int sigpending = libc.sigpending sigpending.argtypes = [POINTER(sigset_t)] sigpending.restype = c_int sigemptyset = libc.sigemptyset sigemptyset.argtypes = [POINTER(sigset_t)] sigemptyset.restype = c_int sigaddset = libc.sigaddset sigaddset.argtypes = [POINTER(sigset_t), c_int] sigaddset.restype = c_int sigfillset = libc.sigfillset sigfillset.argtypes = [POINTER(sigset_t)] sigfillset.restype = c_int sigdelset = libc.sigdelset sigdelset.argtypes = [POINTER(sigset_t), c_int] sigdelset.restype = c_int sigismember = libc.sigismember sigismember.argtypes = [POINTER(sigset_t), c_int] sigismember.restype = c_int sighandler_t = CFUNCTYPE(None, c_int) class sigaction_t(Structure): _fields_ = [ ("sa_handler", sighandler_t), ("sa_mask", sigset_t), ("sa_flags", c_int), ("sa_restorer", CFUNCTYPE(None)), ] sigaction = libc.sigaction sigaction.argtypes = [c_int, POINTER(sigaction_t), POINTER(sigaction_t)] sigaction.restype = c_int SA_RESTART = 0x10000000 SIG_ERR = cast(-1, sighandler_t) sigsuspend = libc.sigsuspend sigsuspend.argtypes = [POINTER(sigset_t)] sigsuspend.restype = c_int
10.11 シグナルの集合と、sigprocmaskの例
import signal, time from ctypes import * from posixsignal import * def sig_quit(signum, frame): print "caught SIGQUIT" signal.signal(signal.SIGQUIT, signal.SIG_DFL) signal.signal(signal.SIGQUIT, sig_quit) newmask = sigset_t() oldmask = sigset_t() pendmask = sigset_t() # block SIGQUIT and save current signal mask sigemptyset(byref(newmask)) sigaddset(byref(newmask), signal.SIGQUIT) if sigprocmask(SIG_BLOCK, byref(newmask), byref(oldmask)) < 0: print "SIG_BLOCK error" time.sleep(5) # SIGQUIT here will remain pending if sigpending(byref(pendmask)) < 0: print "sigpending error" if sigismember(byref(pendmask), signal.SIGQUIT): print "SIGQUIT pending" # Reset signal mask which unblocks SIGQUIT if sigprocmask(SIG_SETMASK, byref(oldmask), None) < 0: print "SIG_SETMASK error" print "SIGQUIT unblocked" time.sleep(5) # SIGQUIT here will terminate with core file
10.12 sigactionを用いたsignalの実装
import signal as _signal from ctypes import * from posixsignal import * def signal(signo, func): act = sigaction_t() oact = sigaction_t() act.sa_handler = cast(func, sighandler_t) sigemptyset(byref(act.sa_mask)) act.sa_flag = 0 if signo != _signal.SIGALRM: act.sa_flag = SA_RESTART if sigaction(signo, byref(act), byref(oact)) < 0: return SIG_ERROR return oact.sa_handler
以下はC言語によるハンドラのサンプルである。
#include <stdio.h> void sighandle(int signum) { printf("called\n"); }
このソースをsighandle.c
として保存したとすると、
gcc -fPIC -shared sighandle.c -o sighandle.so
でビルドし、
import signal as _signal handlerdll = CDLL("./sighandle.so") signal(_signal.SIGALRM, handlerdll.sighandle) _signal.alarm(5) import time time.sleep(10)
として登録することができる。
Pythonでシグナルハンドラを実装してsigaction
に渡すことはできるが、実際に動作はしない。Pythonインタープリタはリエントラントではないからだ。
10.13 signal_intr関数
def signal_intr(signo, func): act = sigaction_t() oact = sigaction_t() act.sa_handler = cast(func, sighandler_t) sigemptyset(byref(act.sa_mask)) act.sa_flag = 0 if sigaction(signo, byref(act), byref(oact)) < 0: return SIG_ERROR return oact.sa_handler
signal()
関数以外の部分は、10.12と同様である。
10.14 シグナルマスク、sigsetjump、siglongjmp
setjumpもlongjmpも知らん。
10.15 シグナルから臨界領域を保護する
import signal from ctypes import * from posixsignal import * def sig_int(signum, frame): pr_mask("in sig_int") signal.signal(signal.SIGQUIT, signal.SIG_DFL) signal.signal(signal.SIGINT, sig_int) newmask = sigset_t() oldmask = sigset_t() zeromask = sigset_t() sigemptyset(byref(newmask)) sigemptyset(byref(oldmask)) sigemptyset(byref(zeromask)) # block SIGINT and save current signal mask sigaddset(byref(newmask), signal.SIGINT) if sigprocmask(SIG_BLOCK, byref(newmask), byref(oldmask)) < 0: print "SIG_BLOCK error" # critical region of code pr_mask("in critical region") # allow all signals and pause if sigsuspend(byref(zeromask)) != -1: sys.exit("sigsuspend error") pr_mask("after return from sigsuspend") # reset signal mask which unblocks SIGINT if sigprocmask(SIG_SETMASK, byref(oldmask), None) < 0: sys.exit("SIG_SETMASK error") # # and continue processing #
10.16 大域変数の設定を待ち合わせるためのsigsuspendの使い方
import signal from ctypes import * from posixsignal import * quitflag = 0 def sig_int(signum, frame): global quitflag if signum == signal.SIGINT: print "interrupt" elif signum == signal.SIGQUIT: quitflag = 1 # set flag for main loop signal.signal(signal.SIGQUIT, sig_int) signal.signal(signal.SIGINT, sig_int) newmask = sigset_t() oldmask = sigset_t() zeromask = sigset_t() sigemptyset(byref(newmask)) sigemptyset(byref(oldmask)) sigemptyset(byref(zeromask)) # block SIGQUIT and save current signal mask sigaddset(byref(newmask), signal.SIGQUIT) if sigprocmask(SIG_BLOCK, byref(newmask), byref(oldmask)) < 0: print "SIG_BLOCK error" while quitflag == 0: sigsuspend(byref(zeromask)) # SIGQUIT has been caught and is now blocked; do whatever
なぜか、PyErr_CheckSignals()
を呼び出していないのに、シグナルハンドラが即座に実行されるのが不思議。本来、こういう使い方をするときには、sigsuspend()
の直後にpythonapi.PyErr_CheckSignals()
を呼び出さないと、シグナルハンドラの起動が後回しにされてquitflag
の更新が次のwihle quitflag == 0
の行までに行われない可能性があるのだが…
おそらく、sigsuspend()
の直後にpythonapi.PyErr_CheckSignals()
を追加するべきだろう。
10.17 親と子の同期のためのルーティン
import signal from ctypes import * from posixsignal import * newmask = sigset_t() oldmask = sigset_t() zeromask = sigset_t() sigemptyset(byref(newmask)) sigemptyset(byref(oldmask)) sigemptyset(byref(zeromask)) sigflag = 0 def sig_usr(signum, frame): global sigflag sigflag = 1 def TELL_WAIT(): signal.signal(signal.SIGUSR1, sig_usr) signal.signal(signal.SIGUSR2, sig_usr) sigemptyset(byref(newmask)) sigemptyset(byref(zeromask)) # block SIGUSR1 and SIGUSR2 and save current signal mask sigaddset(byref(newmask), signal.SIGUSR1) sigaddset(byref(newmask), signal.SIGUSR2) if sigprocmask(SIG_BLOCK, byref(newmask), byref(oldmask)) < 0: sys.exit("SIG_BLOCK error") def TELL_PARENT(pid): os.kill(pid, signal.SIGUSR2) # tell parent we're done def WAIT_PARENT(): global sigflag while sigflag == 0: sigsuspend(byref(zeromask)) # and wait for parent sigflag = 0 # reset the signal mask to original value if sigprocmask(SIG_SETMASK, byref(oldmask), None) < 0: sys.exit("SIG_SETMASK error") def TELL_CHILD(pid): os.kill(pid, signal.SIGUSR1) # tell child we're done def WAIT_CHILD(): global sigflag while sigflag == 0: sigsuspend(byref(zeromask)) # and wait for child sigflag = 0 # reset signal mask to original value if sigprocmask(SIG_SETMASK, byref(oldmask), None) < 0: sys.exit("SIG_SETMASK error")
10.18 POSIX.1のabortの実装
import signal, os from ctypes import * from posixsignal import * def abort(): mask = sigset_t() action = sigaction_t() # caller can't ignore SIGABRT, if so reset to default sigaction(signal.SIGABRT, None, byref(action)) if action.sa_handler == cast(signal.SIG_IGN, sighandler_t): action.sa_handler = cast(signal.SIG_DFL, sighandler_t) sigaction(signal.SIGABRT, byref(action), None) if action.sa_handler == cast(signal.SIG_DFL, sighandler_t): libc.fflush(None) # flush all open stdio streams # caller can't block SIGABRT; make sure it's unblocked sigfillset(byref(mask)) sigdelset(byref(mask), signal.SIGABRT) # mask has only SIGABRT turned off sigprocmask(SIG_SETMASK, byref(mask), None) os.kill(os.getpid(), signal.SIGABRT) # send the signal # if we are here, process caught SIGABRT and returned libc.fflush(None) # flush all open stdio streams action.sa_handler = cast(signal.SIG_DFL, sighandler_t) sigaction(signal.SIGABRT, byref(action), None) # reset disposition to default sigprocmask(SIG_SETMASK, byref(mask), None) # just in case os.kill(os.getpid(), signal.SIGABRT) # and one more time sys.exit(1) # this should never be execused ...
10.19 edエディタを起動するためにsystemを利用する
import os, signal def sig_int(signo, frame): print "caught SIGINT" def sig_chld(signo, frame): print "caught SIGCHLD" signal.signal(signal.SIGINT, sig_int) signal.signal(signal.SIGCHLD, sig_chld) system("/bin/ed")
10.20 POSIX.2の正しいsystem関数の実装
import signal, os from ctypes import * from posixsignal import * def system(cmdstring): if not cmdstring: return 1 # always a command processor with Unix ignore = sigaction_t() saveintr = sigaction_t() savequit = sigaction_t() chldmask = sigset_t() savemask = sigset_t() # ignore SIGINT and SIGQUIT ignore.sa_handler = cast(signal.SIG_IGN, sighandler_t) sigemptyset(byref(ignore.sa_mask)) ignore.sa_flags = 0 if sigaction(signal.SIGINT, byref(ignore), byref(saveintr)) < 0: return -1 if sigaction(signal.SIGQUIT, byref(ignore), byref(savequit)) < 0: return -1 # now block SIGCHLD sigemptyset(byref(chldmask)) sigaddset(byref(chldmask), signal.SIGCHLD) if sigprocmask(SIG_BLOCK, byref(chldmask), byref(savemask)) < 0: return -1 pid = os.fork() if pid == 0: # child # restore previous signal actions & reset signal mask sigaction(signal.SIGINT, byref(saveintr), None) sigaction(signal.SIGQUIT, byref(savequit), None) sigprocmask(SIG_SETMASK, byref(savemask), None) os.execl("/bin/sh", "sh", "-c", cmdstring) sys.exit(127) # exec error else: # parent while True: try: pid, status = os.waitpid(pid, 0) break except OSError, e: if e.errno != errno.EINTR: status = -1 # error other than EINTR from waitpid() break # restore previous signal actions & reset signal mask if sigaction(signal.SIGINT, byref(saveintr), None) < 0: return -1 if sigaction(signal.SIGQUIT, byref(savequit), None) < 0: return -1 if sigprocmask(SIG_SETMASK, byref(savemask), None) < 0: return -1 return status
10.21 sleepの信頼性のある実装
import signal from ctypes import * from posixsignal import * def sig_alrm(signo, frame): pass # nothing to do, just returning wakes up sigsuspend() def sleep(nsecs): newact = sigaction_t() oldact = sigaction_t() newmask = sigset_t() oldmask = sigset_t() suspmask = sigset_t() unslept = 0 # set out handler, save previous information signal.signal(signal.SIGALRM, sig_alrm) sigaction(signal.SIGALRM, None, byref(oldact)) # block SIGALRM and save current signal mask sigemptyset(byref(newmask)) sigaddset(byref(newmask), signal.SIGALRM) sigprocmask(SIG_BLOCK, byref(newmask), byref(oldmask)) signal.alarm(nsecs) memmove(byref(suspmask), byref(oldmask), sizeof(suspmask)) sigdelset(byref(suspmask), signal.SIGALRM) # make sure SIGALRM isn't blocked sigsuspend(byref(suspmask)) # wait for any signal to be caught # some signalshas been caught, SIGALRM is now blocked unslept = signal.alarm(0) sigaction(signal.SIGALRM, byref(oldact), None) # reset previous action # reset signal mask, which unblocks SIGALRM sigprocmask(SIG_SETMASK, byref(oldmask), None) return unslept
10.22 SIGTSTPの処理方法
import signal, errno, os from ctypes import * from posixsignal import * def sig_tstp(signo, frame): mask = sigset_t() # move cursor to lower left corner, reset tty mode ... # unblock SIGTSTP, since it's blocked while we're handling it sigemptyset(byref(mask)) sigaddset(byref(mask), signal.SIGTSTP) sigprocmask(SIG_UNBLOCK, byref(mask), None) # reset disposition to default signal.signal(signal.SIGTSTP, signal.SIG_DFL) # and set the signal to ourself os.kill(os.getpid(), signal.SIGTSTP) # we won't return from the kill untill we're continued # reestablish signal handler signal.signal(signal.SIGTSTP, sig_tstp) # reset tty mode, redraw screen, ... # only catch SIGTSTP if we' re running with a job-control shell if signal.signal(signal.SIGTSTP, signal.SIG_IGN) == signal.SIG_DFL: signal.signal(signal.SIGTSTP, sig_tstp) signal.signal(signal.SIGTSTP, sig_tstp) while True: try: buf = os.read(0, 1024) except OSError, e: if e.errno == errno.EINTR: continue if not buf: break os.write(0, buf)