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)