PythonでUnixシグナルを扱う

UNIX/Linuxシステムでは、個々のプロセス間で通信を行うための特別な仕組みが用意されています。

これらの機構の1つがシグナルであり、プロセス間のさまざまな通信方法(Inter Process Communication、IPCと略す)に属している。

つまり、シグナルとは、プログラム(またはプロセス)に対して、重要なイベントを通知したり、特別なコードシーケンスを実行するためにプログラムに対して要求するソフトウェア割り込みのことである。

シグナルを受信したプログラムは、命令の実行を停止または継続するか、メモリダンプを伴うか伴わないかのいずれかで終了するか、あるいは単にシグナルを無視することさえある。

POSIX規格では定義されていますが、実際には開発者がどのようにスクリプトを書き、シグナルの処理を実装するかによって反応が異なります。

この記事では、シグナルとは何かを説明し、コマンドラインから別のプロセスにシグナルを送信する方法と、受信したシグナルを処理する方法を紹介します。

他のモジュールの中で、プログラムコードは主にシグナルモジュールに基づいています。

このモジュールは、オペレーティングシステムのCヘッダをPythonの世界と接続します。

シグナルズ入門

UNIXベースのシステムでは、シグナルは3つのカテゴリーに分類されます。

  • システムシグナル (ハードウェアとシステムのエラー)。システムシグナル (ハードウェアおよびシステムエラー): sigill, sigtrap, sigbus, sigfpe, sigkill, sigsegv, sigxcpu, sigxfsz, sigio
  • デバイス・シグナル デバイス信号:sighup,sigint,sigpipe,sigalrm,sigchld,sigcont,sigstop,sigttin,sigttou, sigurg,sigwinch,sigio(※)。
  • ユーザー定義信号 sigquit, sigabrt, sigusr1, sigusr2, sigterm。

各シグナルは整数値で表され、利用可能なシグナルのリストは比較的に長く、異なるUNIX/Linuxのバリエーション間で一貫性がありません。

Debian GNU/Linuxシステムでは、kill -l コマンドを実行すると、以下のようにシグナルのリストが表示されます。

$ kill -l
 1) SIGHUP   2) SIGINT   3) SIGQUIT  4) SIGILL   5) SIGTRAP
 6) SIGABRT  7) SIGBUS   8) SIGFPE   9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG  24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF 28) SIGWINCH    29) SIGIO   30) SIGPWR
31) SIGSYS  34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX


Debian GNU/Linux では,kill -l コマンドを実行すると,次のようなシグナルの一覧が表示されます.

  • 1 (SIGHUP): 接続の終了、またはデーモンの設定の再読み込み
  • 2 (SIGINT): 対話ステーションからのセッションの中断
  • 3 (SIGQUIT): 対話ステーションからのセッションを終了する。
  • 4 (SIGILL): 不正な命令が実行された。
  • 5 (SIGTRAP): 単発命令(トラップ)を行う。
  • 6 (SIGABRT): 異常終了。
  • 7 (SIGBUS): システムバスにエラーが発生した。
  • 8 (SIGFPE): 浮動小数点エラー
  • 9 (SIGKILL): プロセスを直ちに終了させる。
  • 10 (SIGUSR1): ユーザ定義シグナル
    11 (SIGSEGV): メモリセグメントへの不正アクセスによるセグメンテーションフォールト * 12 (SIGUSR2): メモリセグメントへの不正アクセスによるセグメンテーションフォールト
  • 12 (SIGUSR2):ユーザ定義信号
  • 13 (SIGPIPE): パイプに書き込んでいるが、誰もパイプから読み出していない。
  • 14 (SIGALRM): タイマーが終了した(アラーム)
  • 15 (SIGTERM): ソフト的にプロセスを終了させる。

Linux ターミナルでプロセスにシグナルを送るには、上のリストにあるシグナル番号 (またはシグナル名) とプロセスの ID (pid) の両方を指定して kill コマンドを呼び出します。

次のコマンド例では、シグナル 15 (SIGTERM) を pid 12345 のプロセスに送っています。

$ kill -15 12345


番号の代わりにシグナル名を使用する方法もあります。

$ kill -SIGTERM 12345


どちらの方法を選ぶかは、あなたにとってより便利な方法を選ぶことによる。

どちらの方法も同じ効果をもたらします。

その結果、プロセスはシグナル SIGTERM を受け取り、直ちに終了する。

Pythonシグナルライブラリの使用

Python 1.4 以降、すべての Python リリースにおいて、 signal ライブラリは定期的なコンポーネントとして提供されています。

signal` ライブラリを使用するには、まず、以下のようにして Python プログラムにインポートします。

import signal


受信したシグナルを捕捉して適切に反応させるには、コールバック関数、いわゆるシグナルハンドラを使用します。

receiveSignal()`という名前のシグナルハンドラは、次のように書くことができます。

def receiveSignal(signalNumber, frame):
    print('Received:', signalNumber)
    return


このシグナルハンドラは、受信したシグナルの番号を報告する以外には何もしない。

次のステップは、シグナルハンドラによって捕捉されたシグナルを登録することです。

Pythonのプログラムでは、9のSIGKILLを除くすべてのシグナルをスクリプトで捕捉することができます。

if __name__ == '__main__':
    # register the signals to be caught
    signal.signal(signal.SIGHUP, receiveSignal)
    signal.signal(signal.SIGINT, receiveSignal)
    signal.signal(signal.SIGQUIT, receiveSignal)
    signal.signal(signal.SIGILL, receiveSignal)
    signal.signal(signal.SIGTRAP, receiveSignal)
    signal.signal(signal.SIGABRT, receiveSignal)
    signal.signal(signal.SIGBUS, receiveSignal)
    signal.signal(signal.SIGFPE, receiveSignal)
    #signal.signal(signal.SIGKILL, receiveSignal)
    signal.signal(signal.SIGUSR1, receiveSignal)
    signal.signal(signal.SIGSEGV, receiveSignal)
    signal.signal(signal.SIGUSR2, receiveSignal)
    signal.signal(signal.SIGPIPE, receiveSignal)
    signal.signal(signal.SIGALRM, receiveSignal)
    signal.signal(signal.SIGTERM, receiveSignal)


次に、現在のプロセスのプロセス情報を追加し、 os モジュールの getpid() というメソッドを使用してプロセス ID を検出します。

エンドレスの while ループで、シグナルが入力されるのを待ちます。

さらに2つのPythonモジュール – os と time – を使ってこれを実装します。

これらのモジュールもPythonスクリプトの冒頭でインポートしています。

import os
import time


メインプログラムの while ループでは、print 文が “Waiting…” と出力しています。

time.sleep()`関数の呼び出しは、プログラムを3秒間待たせています。

    # output current process id
    print('My PID is:', os.getpid())


# wait in an endless loop for signals 
    while True:
        print('Waiting...')
        time.sleep(3)


最後に、このスクリプトをテストしてみましょう。

スクリプトを signal-handling.py という名前で保存したら、ターミナルで次のように実行してください。

$ python3 signal-handling.py 
My PID is: 5746
Waiting...
...


2つ目のターミナルウィンドウで、プロセスにシグナルを送ります。

最初のプロセスであるPythonスクリプトは、上の画面に表示されているプロセスIDで識別します。

$ kill -1 5746


Pythonプログラムのシグナルイベントハンドラは、私たちがプロセスに送ったシグナルを受け取ります。

そして、受信したシグナルを確認する処理を行います。

...
Received: 1
...


信号の無視

signal モジュールは受信したシグナルを無視する方法を定義しています。

これを行うには、シグナルは定義済みの関数 signal.SIG_IGN で接続されている必要があります。

以下の例では、その結果、Python プログラムはもう CTRL+C で中断することができなくなりました。

Pythonスクリプトを停止させるために、このサンプルスクリプトでは別の方法が実装されています – シグナルSIGUSR1がPythonスクリプトを終了させるのです。

さらに、無限ループの代わりに signal.pause() というメソッドを使っています。

これはシグナルを受信するのを待つだけです。

import signal
import os
import time


def receiveSignal(signalNumber, frame):
    print('Received:', signalNumber)
    raise SystemExit('Exiting')
    return


if __name__ == '__main__':
    # register the signal to be caught
    signal.signal(signal.SIGUSR1, receiveSignal)


# register the signal to be ignored
    signal.signal(signal.SIGINT, signal.SIG_IGN)


# output current process id
    print('My PID is:', os.getpid())


signal.pause()


信号の正しい扱い方

ここまでで使用したシグナルハンドラは、受信したシグナルを報告するだけのシンプルなものです。

これは、Pythonスクリプトのインターフェイスがうまく機能していることを示しています。

では、それを改良してみましょう。

シグナルをキャッチすることはすでに良い基礎になっていますが、POSIX標準の規則に準拠するためにいくつかの改良が必要です。

より高い精度のために、各シグナルには適切な反応が必要です(上のリストを参照)。

つまり、Pythonスクリプトのシグナルハンドラは、シグナルごとに特定のルーチンで拡張される必要があります。

これは、シグナルが何をするのか、そして一般的な反応は何なのかを理解している場合に最も効果的です。

シグナル1、2、9、15を受信したプロセスは終了します。

それ以外の場合は、コアダンプを書くことも期待されます。

今までは、すべてのシグナルをカバーする1つのルーチンを実装し、同じように処理してきました。

次のステップは、信号ごとに個別のルーチンを実装することです。

次のコード例では、シグナル1(SIGHUP)とシグナル15(SIGTERM)に対して、これを実演しています。

def readConfiguration(signalNumber, frame):
    print ('(SIGHUP) reading configuration')
    return


def terminateProcess(signalNumber, frame):
    print ('(SIGTERM) terminating the process')
    sys.exit()


上記の2つの関数は、以下のようにシグナルと接続されています。

    signal.signal(signal.SIGHUP, readConfiguration)
    signal.signal(signal.SIGTERM, terminateProcess)


Pythonスクリプトを実行し、UNIXコマンド kill -1 16640kill -15 16640 によってシグナル1 (SIGHUP) とシグナル15 (SIGTERM) を送ると、次のような出力が得られます。

$ python3 daemon.py
My PID is: 16640
Waiting...
Waiting...
(SIGHUP) reading configuration
Waiting...
Waiting...
(SIGTERM) terminating the process


このスクリプトはシグナルを受信し、適切に処理します。

わかりやすくするために、このスクリプト全体を示します。

import signal
import os
import time
import sys


def readConfiguration(signalNumber, frame):
    print ('(SIGHUP) reading configuration')
    return


def terminateProcess(signalNumber, frame):
    print ('(SIGTERM) terminating the process')
    sys.exit()


def receiveSignal(signalNumber, frame):
    print('Received:', signalNumber)
    return


if __name__ == '__main__':
    # register the signals to be caught
    signal.signal(signal.SIGHUP, readConfiguration)
    signal.signal(signal.SIGINT, receiveSignal)
    signal.signal(signal.SIGQUIT, receiveSignal)
    signal.signal(signal.SIGILL, receiveSignal)
    signal.signal(signal.SIGTRAP, receiveSignal)
    signal.signal(signal.SIGABRT, receiveSignal)
    signal.signal(signal.SIGBUS, receiveSignal)
    signal.signal(signal.SIGFPE, receiveSignal)
    #signal.signal(signal.SIGKILL, receiveSignal)
    signal.signal(signal.SIGUSR1, receiveSignal)
    signal.signal(signal.SIGSEGV, receiveSignal)
    signal.signal(signal.SIGUSR2, receiveSignal)
    signal.signal(signal.SIGPIPE, receiveSignal)
    signal.signal(signal.SIGALRM, receiveSignal)
    signal.signal(signal.SIGTERM, terminateProcess)


# output current process id
    print('My PID is:', os.getpid())


# wait in an endless loop for signals 
    while True:
        print('Waiting...')
        time.sleep(3)


参考文献

signal` モジュールとそれに対応するイベントハンドラを使用すると、比較的簡単にシグナルをキャッチすることができます。

異なるシグナルの意味を知り、POSIX 標準で定義されているように適切に反応させることが次のステップです。

イベントハンドラが異なるシグナルを区別し、すべてのシグナルに対して別々のルーチンを持っていることが必要です。

タイトルとURLをコピーしました