guest@blog.cmj.tw: ~/posts $

Signal Handler


Software Architecture~

一個有趣的問題:如果一個大型的 Python 程式,需要根據 Signal 來處理各種不同的情境並回收資源的話, 在各種不同情況開怎樣實作相對的 signal handler。首先我們考慮的狀況有下面幾種:

  • os.fork
  • multiprocessing.Process
  • multiprocessing.Pool
  • multiprocessing

我們都固定使用一個基本的函數來決定是否正確收到 SIGINT 這個 signal,這也是 Ctrl-C 所送出的 signal。這個函數會跑一個 busy-loop 並且等待接收到 KeyboardInterrupt 這個例外。

def Foo():
    import time, os

    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt as e:
        print 'Catch SIGINT on {0}'.format(os.getpid())
        raise

接下來,利用 signal.signal 這個函數可以遮蔽或者轉換接收到的 signal。 首先,需要了解的第一件事情:signal 可以一再的遮蔽、處理。以下面的例子,我們有一個 signal handler 接收 signal 並且再次丟出 KeyboardInterrupt 這個例外。

def handler(*args):
    raise KeyboardInterrupt

if __name__ == '__main__':
    import signal, os

    # Block the SIGINT signal
    signal.signal(signal.SIGINT, signal.SIG_IGN)
    # Hangle the SIGINT by function 'handler'
    signal.signal(signal.SIGINT, handler)

    try:
        Foo()
    except KeyboardInterrupt as e:
        print 'Finally Catch the SIGINT on {0}'.format(os.getpid())

接著,我們思考處理 os.fork 的狀況,會發現到即使使用到 os.fork,SIGINT 也會傳遞到其他的 process。 在這個例子中,我們直接執行主要的程式:Process (67746) 並且送出 SIGINT,但是 fork 出來的 Process (67747) 同時也會收到 SIGINT。因此,在這個例子中知道:fork 出來的 process 會延用原本 程式的 signal mask。

if __name__ == '__main__':
    import signal, os

    signal.signal(signal.SIGINT, signal.SIG_IGN)
    signal.signal(signal.SIGINT, handler)


    pid = os.fork()
    '''
    Catch SIGINT on 67746
    Finally Catch the SIGINT on 67746
    Catch SIGINT on 67747
    Finally Catch the SIGINT on 67747
    '''
    try:
        if pid == 0:
            Foo()
        elif pid > 0:
            Foo()
    except KeyboardInterrupt as e:
        print 'Finally Catch the SIGINT on {0}'.format(os.getpid())

另一個 case 則是在 fork 出來的 child process 中使用 multiprocessing.Process 來再次產生下一個 process。這個 case 中我們會得到一個很詭異的現象:雖然我們在 Foo 中都有接收到 KeyboardInterrupt 這個例外,我們也處理了這個例外 (也繼續 raise 這個例外),但是在最外層的 try-except 中, 我們只有接收到一開始的主程式,而沒有接收到利用 multiprocessing.Process 產生出來的 Process (13003)。 因此,我們又發現到 Signal Hangle 只要處理過一個 signal 就會忽略掉其他一樣的 signal:在這裡, 也就是 except KeyboardInterrupt 這個處理邏輯。

def Foo():
    import time, os

    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt as e:
        print 'Catch SIGINT on {0}'.format(os.getpid())
        raise

if __name__ == '__main__':
    import signal, os
    from multiprocessing import Process

    '''
    Catch SIGINT on 13002
    Catch SIGINT on 13003
    Finally Catch the SIGINT on 13002
    Process Process-1:
    Traceback (most recent call last):
      File "/usr/lib/python2.7/multiprocessing/process.py", line 258, in _bootstrap
        self.run()
      File "/usr/lib/python2.7/multiprocessing/process.py", line 114, in run
        self._target(*self._args, **self._kwargs)
      File "test.py", line 8, in Foo
        time.sleep(1)
    KeyboardInterrupt
    '''
    try:
        proc = Process(target=Foo)
        proc.start()
        Foo()
        proc.join()
    except KeyboardInterrupt as e:
        print 'Finally Catch the SIGINT on {0}'.format(os.getpid())
    finally:
        proc.join()

接下來,使用 multiprocessing.Pool 來產生一個 Process Pool:他會產生固定數量的 Process 並且依序處理送進來的參數。在沒有強制關閉的情況下,Pool 會盡可能的處理所有送進來的參數。 以下面的例子來看,我們會產生一個 Pool(4) 來處理八個狀況,所以一開始除了 main process 支外, 還會有另外四個 Process。當第一次送出 SIGINT 的時候,主要的 process 以及目前運作的四個 subprocess 都會處理 signal,但是 pool 會緊接著繼續處理剩於的四個 case。

def Foo(pid=0):
    import time, os

    print 'Run on {0}'.format(os.getpid())
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt as e:
        print 'Catch SIGINT on {0}'.format(os.getpid())
if __name__ == '__main__':
    import signal, os
    from multiprocessing import Pool

    '''
    Run on 1530
    Run on 1532
    Run on 1533
    Run on 1535
    Run on 1534
    Catch SIGINT on 1535
    Catch SIGINT on 1534
    Catch SIGINT on 1532
    Catch SIGINT on 1530
    Catch SIGINT on 1533
    Run on 1535
    Run on 1533
    Run on 1532
    Run on 1534
    Catch SIGINT on 1534
    Catch SIGINT on 1535
    Catch SIGINT on 1533
    Catch SIGINT on 1532
    Finally Catch the SIGINT on 1530
    '''
    try:
        pool = Pool(4)
        req = pool.map_async(Foo, range(8))
        Foo()
        req.get(10240)
    except KeyboardInterrupt as e:
        print 'Finally Catch the SIGINT on {0}'.format(os.getpid())

因此我們可以了解到:無論 process 怎樣產生,signal mask 是不會改變的。