はじめに
Qt Advent Calenderも4日目に突入しました。昨日は、informationseaさんによるMac OS Xでのメニューにヒントを与えることでうまく動かすテクニックでした。MacでQtを使う方はぜひお試しください。さて、本日4日目が空いてしまったので、せめて第一週くらいは全部埋めたいと、短いながら記事を書くことにしました。2日目に輪をかけて余裕のない中でなので、元ネタは公式ドキュメントから・・・。
本日は、UNIXシグナルのハンドラからQtを呼び出すテクニックについてをご紹介します。なお、Qtのシグナルと区別するため、Qtのシグナルをただのシグナル、UNIXのシグナルをUNIXシグナルと記載し、シグナルハンドラやスロットのように用語から明らかなものはそのままで記述しておきます。
なお、例によって古い環境で生きているので、書きっぷりはQt4の初期の頃のままです。connect周りとかincludeまわりとか、気になっちゃう人はごめんなさい。脳内変換してください。
UNIXシグナルとは
概要
UNIXシグナルは、POSIX準拠のOSに用意されている、ある条件が生起したことをプロセスに通知するための古くからある技法で、簡易的なプロセス間通信としても利用されています。Linux等でプログラムを書いている方は、SIGSEGV等を起こした経験があるかと思います。また、暴走させてしまったプログラムを終了するためにkillコマンドでSIGTERMを送ったり、最悪SIGKILLを送ったりした覚えがあるでしょう。
Unix系のOSでは、ゼロ除算、不正なアドレスアクセスのような問題発生時や、他のプロセスとの間で子プロセスの再開・終了通知、プログラムの停止要求、プログラムの終了要求など、さまざまな条件によって生起されます。
これらのUNIXシグナルは、シグナルの種類により発生した際にどのようにふるまうかのデフォルト定義がされています。この定義は、プログラマがシグナルハンドラと呼ばれる関数を登録することで変更することが可能です。
UNIXシグナルは大きくわけて2種類あり、同期シグナルと非同期シグナルに分かれます。同期シグナルは、そのスレッドが原因で発生するUNIXシグナルで、SIGSEGV,SIGBUS,SIGFPEといったものがそれに当たります。それ以外のものは、非同期シグナルといい発生源が現在動作中のスレッドに起因せず、どのタイミングでどう割り込まれるか不定となります。
UNIXシグナルの注意事項
同期シグナルは単純で、発生源は直前までの処理が要因であり、発生源だったスレッド自身に配送されます。多くがプログラムの問題で起こりますが、発生源だけではなくプロセス全体に影響するような重大な問題のケースが多く、復旧するのが難しいケースが多いかと思います。プラグイン機構等でプラグイン内部で問題が起こった時に適切に終了させたいとか、難しい事を考えないのであれば、そのまま不正終了するデフォルト動作のままにして、ソフトウェアをきちんとテストして発生しないようにする方が良いように思います。
他方、非同期シグナルは、自身のプログラムの遷移とは無関係に割り込み処理されます。このため直前のプログラムの状況は常に不明な状態のため、シグナルハンドラの中で利用できるAPIはシグナルセーフなAPIに限定されます1。たとえば、シグナルハンドラでprintf等が使われているケースをみかけることがありますが、これはシグナルセーフではないため、タイミングによってはデッドロック等を引き起こしかねません。シグナルハンドラを正しく作り込むためには、細心の注意が必要です。
QtはシグナルセーフなAPIではないため、シグナルハンドラの中ではQtは使わないようにしなくてはなりません。シグナルセーフなAPIの数はそれほど多くないのでこちらをご覧ください。
さらにマルチスレッドと非同期シグナルのハンドラの組み合わせは複雑になります。非同期シグナルは、マスクすることも可能なのですが、このマスクはスレッド単位で異なる事に注意が必要です2。また、非同期シグナルは全てのスレッドが受け取るわけではなく、どれか一つの任意のスレッドに配送されるという事も配慮しなくてはなりません。つまり、通常だと、どのスレッドがどのタイミングで受け取るのか予期できないのです。途中で処理が割り込まれて問題が起きないのか、テストをすべて作ることは現実的に不可能となるため、シーケンス図を書き、割り込まれての処理で問題になるケースが無いか、考えなくてはなりません。
また、シグナルハンドラはスレッド単位ではなく、全スレッドで共用されるという特徴があります。一つのスレッドでシグナルハンドラを設定して、他のスレッドでUNIXシグナルを無視するようにシグナルハンドラを設定すると、タイミングによって無視したり、ハンドラが呼び出されたりするタイミング依存の挙動の違いを生んでしまいます。
このあたりを理解したうえで、非同期シグナルについては適切な処理を考えなくてはなりません。
QtとUNIXシグナル
Qt内部のUNIXシグナル処理
QProcessは内部的にSIGCHLDやSIGPIPEをハンドリングして、処理してくれています。以前は、Qtのコード内でもQSegfaultHandlerといったものがあり、SIGSEGVでスタックトレースを出すような機構があったのですが、いつの間にか使われなくなっているようです。
Qtを使う場合のUNIXシグナル対応の必要性
SIGCHLDについてはQProcessを使っている限りは考慮されているため、GUIプログラムを書く際に、UNIXシグナルのハンドリングをする事はあまりないかもしれません。必要に応じてSIGTERM等のTerminate要求に対して、プログラムを適切に終わらせるための処理を書くくらいでしょうか。
デーモンプログラムを書く場合は、SIGHUPを受信した際に設定ファイルを読み直す等の動作を実装することが多いため、SIGHUPで必要な設定ファイルを読み直す処理を実装したい場合があるかと思います。
また、どうしてもUNIXシグナルを受け取りたくないスレッドがあったり、細かい制御を行う場合は、独自に実装しなくてはなりません。
これらはどうしてもプラットフォーム依存になってしまうため、クロスプラットフォーム向けに実装しているのであれば、プラットフォーム毎に#ifdef等で適切に処理するよう注意が必要です。
QtでのUNIXシグナルのハンドリング方法
A) 公式ドキュメントのサンプルの方法
公式ドキュメントでは、一番シンプルな方法としては、socketpairを用意して、シグナルハンドラではソケットにwriteだけを行って処理を終了し、Qtの側ではQSocketNotifierでsocketを監視して、UNIXシグナルの発生を検知したら、Qt側で必要な処理をさせています。
このサンプルでは、SIGHUPとSIGTERMの2つのUNIXシグナルについて処理が書かれています。
class MyDaemon : public QObject
{
Q_OBJECT
public:
MyDaemon(QObject *parent = 0);
~MyDaemon();
// Unix signal handlers.
static void hupSignalHandler(int unused);
static void termSignalHandler(int unused);
public slots:
// Qt signal handlers.
void handleSigHup();
void handleSigTerm();
private:
static int sighupFd[2];
static int sigtermFd[2];
QSocketNotifier *snHup;
QSocketNotifier *snTerm;
};
MyDaemonクラスのコンストラクタでは、socketpair関数を使って、それぞれのデスクリプタを初期化し、QSocketNotifierのactivated()シグナルを使ってsocketが読み取り可能になったら、SIGHUPはhandleSigHup()を、SIGTERMはhandleSigTerm()を呼び出すようにしています。このようにして、UNIXシグナルをQSocketNotifier::activated()シグナルへと変換しています。
MyDaemon::MyDaemon(QObject *parent)
: QObject(parent)
{
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, sighupFd))
qFatal("Couldn't create HUP socketpair");
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, sigtermFd))
qFatal("Couldn't create TERM socketpair");
snHup = new QSocketNotifier(sighupFd[1], QSocketNotifier::Read, this);
connect(snHup, SIGNAL(activated(int)), this, SLOT(handleSigHup()));
snTerm = new QSocketNotifier(sigtermFd[1], QSocketNotifier::Read, this);
connect(snTerm, SIGNAL(activated(int)), this, SLOT(handleSigTerm()));
...
}
スタートアップコードのどこかで、以下の関数を呼び出し、sigaction()でUNIXシグナルハンドラを登録します。
static int setup_unix_signal_handlers()
{
struct sigaction hup, term;
hup.sa_handler = MyDaemon::hupSignalHandler;
sigemptyset(&hup.sa_mask);
hup.sa_flags = 0;
hup.sa_flags |= SA_RESTART;
if (sigaction(SIGHUP, &hup, 0) > 0)
return 1;
term.sa_handler = MyDaemon::termSignalHandler;
sigemptyset(&term.sa_mask);
term.sa_flags |= SA_RESTART;
if (sigaction(SIGTERM, &term, 0) > 0)
return 2;
return 0;
}
int main(int argc, char* argv[])
{
:
:
int ret = setup_unix_signal_handlers();
if (ret) {
return ret;
}
:
:
}
シグナルハンドラでは、socket pairに対して1バイトの書き込みをします。これにより、QSocketNotifierのactivated()シグナルが発呼され、Qt側でスロットが呼び出されることになります。
void MyDaemon::hupSignalHandler(int)
{
char a = 1;
::write(sighupFd[0], &a, sizeof(a));
}
void MyDaemon::termSignalHandler(int)
{
char a = 1;
::write(sigtermFd[0], &a, sizeof(a));
}
QSocketNotifier::activated()シグナルとconnectしているスロットでは、送られたバイトをreadします。UNIXシグナルハンドラとは異なり、このhandleSigTerm()スロットの中では、安全にすべてのQtの機能を利用することができます。
void MyDaemon::handleSigTerm()
{
snTerm->setEnabled(false);
char tmp;
::read(sigtermFd[1], &tmp, sizeof(tmp));
// do Qt stuff
snTerm->setEnabled(true);
}
void MyDaemon::handleSigHup()
{
snHup->setEnabled(false);
char tmp;
::read(sighupFd[1], &tmp, sizeof(tmp));
// do Qt stuff
snHup->setEnabled(true);
}
B) 少し改造してみる
ところで、この公式の書きっぷりだと、対応するUNIXシグナルすべてにsocket pairが必要ですし、なにより処理がMyDaemonのスロットに集約しなくてはなりません。デーモン化の一環で実装するならいいのでしょうが、場合によってはシグナルを変換するだけのクラスと処理するクラスは分けた方が良いのかもしれません。
シグナルハンドラは、引数で発生したUNIXシグナルの番号が通知されます。ですので、以下のようにしてはどうでしょうか。
#pragma once
#include <QObject>
#include <signal.h>
class QSocketNotifier;
class UnixSignal : public QObject
{
Q_OBJECT
public:
static void handler(int signum);
UnixSignal(QObject* parent=0);
~UnixSignal();
private slots:
void notifySingals();
signals:
void sigterm();
void sighup();
private:
static int fds_[2];
QSocketNotifier* watcher_;
struct sigaction termOldAct_;
struct sigaction hupOldAct_;
};
#include <QSocketNotifier>
#include <QDebug>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
int UnixSignal::fds_[2] = {-1,-1};
void UnixSignal::handler(int signum)
{
::write(fds_[0], &signum, sizeof(signum));
}
UnixSignal::UnixSignal(QObject* parent)
: QObject(parent), watcher_(0)
{
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, fds_))
qFatal("Couldn't create socketpair");
watcher_ = new QSocketNotifier(fds_[1], QSocketNotifier::Read, this);
connect(watcher_, SIGNAL(activated(int)), SLOT(notifySingals()));
struct sigaction act;
act.sa_handler = UnixSignal::handler;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGTERM);
sigaddset(&act.sa_mask, SIGHUP);
act.sa_flags = SA_RESTART;
if (sigaction(SIGTERM, &act, &termOldAct_) < 0)
qFatal("Couldn't set SIGTERM action");
if (sigaction(SIGHUP, &act, &hupOldAct_) < 0)
qFatal("Couldn't set SIGHUP action");
}
UnixSignal::~UnixSignal()
{
sigaction(SIGHUP, &hupOldAct_, NULL);
sigaction(SIGTERM, &termOldAct_, NULL);
disconnect(this, SLOT(notifySingals()));
if (fds_[1] != -1) close(fds_[1]);
if (fds_[0] != -1) close(fds_[0]);
}
void UnixSignal::notifySingals()
{
watcher_->setEnabled(false);
int signum = 0;
::read(fds_[1], &signum, sizeof(signum));
switch (signum) {
case SIGTERM:
emit sigterm();
break;
case SIGHUP:
emit sighup();
break;
default:
qFatal("receive wrong IPC\n");
break;
}
watcher_->setEnabled(true);
}
#include <QApplication>
#include "unixsignal.h"
#include <QLabel>
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
UnixSignal sigwatch;
QLabel hello("hello");
QLabel hup("hup");
hello.show();
QObject::connect(&sigwatch, SIGNAL(sighup()), &hup, SLOT(show()));
QObject::connect(&sigwatch, SIGNAL(sigterm()),&app, SLOT(quit()));
return app.exec();
}
宿題
さらに考えると、この方法だとシグナルハンドラが予期せぬタイミングで呼ばれることに変わりはありません。
古いOSは考慮せず(考慮してもQtの最新版は動きませんし)、sigwaitがきちんと動作する環境だけで良いのなら、最初にSIGTERM,SIGHUPをマスクしたうえで、sigwaitでSIGTERMとSIGHUPを待ち受け受信を検知したらemitするだけのスレッドを1つ作るというのが正解でしょうか。
先日と昨年のエントリを参考にしていただきつつ、「詳細UNIXプログラミング」あたりを参考にしながら、ぜひちょろっと実装してみて下さい。
まとめ
本日は、Windowsの方々には涙を呑んでいただいて、公式のドキュメントから、UNIXシグナルについてQtアプリケーションでの扱い方を拾って書きつけてみました。明日は、asobot@githubさんが、Qt Creatorについて何か書いて下さるようです。お楽しみに~