Terminator が起動しない原因を探る

こんにちは、最近趣味でいろんなものをデバッグばかりしている気がする masm11 です。 先日は scp の問題でしたね。

blog.ingage.jp

今回問題だったのは、ターミナルエミュレータ (端末) である Terminator です。

私はデスクトップ環境にボタンを配置し、そのボタンをクリックすると Terminator が 起動するように設定しています。しかし、デスクトップ環境起動後、一度目のボタン クリックは問題ないのに、二度目以降は無反応なのです。これを調べてみました。

調査開始

とりあえず調べるのは標準エラー出力です。以下のエラーが発生していました。

Traceback (most recent call last):
  File "/usr/bin/terminator", line 114, in <module>
    TERMINATOR = Terminator()
  File "/usr/lib/python2.7/site-packages/terminatorlib/terminator.py", line 72, in __init__
    self.prepare_attributes()
  File "/usr/lib/python2.7/site-packages/terminatorlib/terminator.py", line 97, in prepare_attributes
    self.pid_cwd = get_pid_cwd()
  File "/usr/lib/python2.7/site-packages/terminatorlib/cwd.py", line 42, in get_pid_cwd
    system = platform.system()
  File "/usr/lib/python2.7/platform.py", line 1303, in system
    return uname()[0]
  File "/usr/lib/python2.7/platform.py", line 1270, in uname
    processor = _syscmd_uname('-p','')
  File "/usr/lib/python2.7/platform.py", line 1005, in _syscmd_uname
    rc = f.close()
IOError: [Errno 10] 子プロセスがありません

そもそも Terminator は Python 2.7 で書かれているのですね。

例外を見たところ、何故なのかはよくわかりませんが、platform.system() を使って 環境がどんなシステムなのかを取得しているようです。 platform.system() のその奥では uname -p を実行して、パイプ経由で 出力を取得しているのでしょう。その後に子プロセスの終了を待とうとして 例外が発生しているのでしょう。このくらいの情報がこの例外メッセージから得られます。

では、何故子プロセスを作っているにも関わらず、「子プロセスがありません」という エラーが発生するのでしょうか?

プロセスの扱い方

ここで Linux のプロセスの扱い方についておさらいをしましょう。

別プロセスでプログラムを実行するには、以下のようにします。

  1. fork() で子プロセスを作る
  2. 子プロセスでは exec() でプログラムを実行する
  3. 親プロセスでは wait() で子プロセスの終了を待つ (その返り値として、子プロセスが正常に終了したのかどうかが得られる)

親プロセスが wait() しなかった場合、子プロセスが終了しても看取ってくれる プロセスがいないため、子プロセスはゾンビとなります。ゾンビは ps コマンドで見ると defunct と表示されます。 ゾンビを大量に作ってしまうと、カーネルのプロセステーブルが溢れてしまい、 プロセスがそれ以上作れなくなってしまいます。

かと言って、ただ wait() で待っていると、親プロセスは他の処理が何もできません。 それでいいならいいのですが、それでは困ることもよくあります。 そういう場合の回避策がいくつか用意されています。その一つが、SIGCHLD と呼ばれるシグナルです。

  • SIGCHLD をデフォルトのまま何も設定しなければ、普通に wait() する必要が あります。
  • SIGCHLD にハンドラを設定すると、子プロセスが終了した時に SIGCHLD が発生 し、指定のハンドラが実行されます。その時に wait() してあげます。
  • SIGCHLD を無視するように設定すると、子プロセスが終了した時に、ゾンビに ならず、勝手に消滅します。

SIGCHLD 無視って、便利そうですよね。ただ、勝手に消滅するということは、 wait() できないということです。正常終了したのかどうか、判断することが できません。

さて、おさらいはこのくらいにしておきます。

解決編

私はこの症状に1ヶ月程前に遭遇し、Python 自身を適当にいじってエラーにならないように 無理矢理回避して、そのまま忘れていました。最近、Python がアップデートされたことで いじった部分が元に戻り、再び遭遇してしまったので、もう一度調査してみたわけです。

1ヶ月前には SIGCHLD の存在を忘れていましたが、今回の調査で思い出しました。 そうです、デスクトップ環境が SIGCHLD を無視する設定にしていたのです。

問題が起きるまでの流れは以下のようになります。

  1. デスクトップ環境を起動する。この状態では SIGCHLD はデフォルト
  2. ボタンをクリックする
  3. デスクトップ環境内で fork が実行される (この時、SIGCHLD のデフォルト設定が引き継がれる)
  4. 子プロセスでは Terminator が実行される (この時は問題なく成功する)
  5. 親プロセスでは、wait() したくないし、終了コードも別に要らないので、 SIGCHLD を無視する設定にする
  6. もう一度ボタンをクリックする
  7. デスクトップ環境内で fork が実行される (この時、SIGCHLD の無視設定が引き継がれる)
  8. 子プロセスでは Terminator が実行される

二度目に子プロセスで Terminator が実行された時、そのプロセスは SIGCHLD を 無視する設定になっているので、子プロセスを作って uname -p を実行した後、 その子プロセスはすぐに消滅していたのです。

どういうことか、簡単に図にしてみました (図中の番号と上に書いた項目の番号は無関係です)。

f:id:masm11:20200327161353p:plain

これで、「一度目は成功するのに二度目以降は失敗する」という超不可解な現象が 説明できました。

では、どう修正するのが良いでしょうか? 私の答えは、

  • 子プロセスでは、SIGCHLD をデフォルト設定に戻した上で Terminator が実行される

とすることです。これで Terminator が正常に wait() することができます。

まとめ

以上、プロセスの扱い方を含め、調査開始から解決までご紹介しました。

私が使っているデスクトップ環境は同種のソフトウェアの中でもかなりの新参者です。 ですので、全然枯れておらず、いろんなバグに遭遇します。 難しい症状を解決できると、それはもう何とも言えない達成感が得られます。 なかなかに楽しめます。

ではまた!!