Apple Developer Connection
高度な検索
Member Login ログイン | ご入会 ADC連絡先

Technical Note TN2083
デーモンとエージェント

デーモンとエージェントは、まとめてバックグラウンドプログラムと呼ばれており、グラフィカルユーザインターフェイスなしで機能するプログラムです。開発者であれば、ユーザの対話なしにアクションを実行したり、他の複数のプログラムで共有の状態情報を管理するためにバックグラウンドプログラムを使用できます。

このテクニカルノートでは、デーモンとエージェントを開発するときに直面する最も一般的な問題について説明し、そういった問題の解決に関する詳細なアドバイスを提供します。

Mac OS X用のバックグラウンドプログラムを開発する場合は、このテクニカルノートをお読みください。また、バックグラウンドプログラム(CUPS filterなど)をホストとして動作するプラグインを開発する場合も、プラグインはホスト環境のルールを順守する必要があるので、このテクニカルノートをお読みください。





はじめに

多くの問題はバックグラウンドで実行するプログラムで解決できます。これらのプログラムにはグラフィカルユーザインターフェイス(GUI)がなく、ユーザとは間接的にのみ対話します。たとえば、次のようになります。

  • Webサーバはバックグラウンドで動作し、クライアントからのHTTPリクエストに応答する。

  • カレンダーアプリケーションは、カレンダーデータベースを管理するバックグラウンドプログラムを導入し、カレンダーイベントが発生したときにGUIアプリケーションを再起動する。

このテクニカルノートでは、バックグラウンドで実行するプログラムの作成に関する問題について説明します。まず、作成できるバックグラウンドプログラムのタイプに関する形式的な定義から始めます(Daemonomicon)。次に、バックグラウンドプログラムが遭遇する異常な実行状況(実行コンテキスト)と、Mac OS Xが階層化フレームワークを使ってそのような状況が引き起こす問題を管理する方法(階層化フレームワーク)について説明します。また、設計に関するいくつかの一般的な推奨事項(設計の考慮事項)を皮切りに、バックグラウンドプログラムを作成する方法についてのアドバイス、さらにバックグラウンドプログラムの起動(起動)およびプログラムのさまざまなコンポーネント間で通信する方法(デーモンIPCに関する推奨事項)についてのアドバイスも提供します。最後に、コーディングに関していくつかの具体的な推奨事項(コーディングに関する推奨事項)や種々雑多なヒントとこつ(ヒントとこつ)について述べます。

このテクニカルノートを読む前に、『System Startup Programming』および『Multiple User Environments』の正式な文書に目を通してください。

重要:このテクニカルノートでは、複雑なトピックを明確に説明するよう努力しています。多くの場合、あることを理解してもらうための最善の方法は、具体的な例を示すことです。これらの例は、システムを実装する方法に関して詳細な情報を示します。重要なのは、将来変更されるかもしれない特定の実装の詳細ではなく、例の目的に集中することです。

実装が旧システムで異なっていたり、将来変更される可能性がある場合は、文中にそのような変更について警告します。

このテクニカルノートに掲載した例はMac OS X 10.4以降に対応したものです。

先頭に戻る

Daemonomicon

バックグラウンドで動作するプログラムを表すために、さまざまな用語が使用されています。多くの場合、同じ用語でも、いろいろな人がそれぞれに異なる意味で使っています。このセクションでは、以降のセクションで使用する用語を定義します。

バックグラウンドプログラムは、特にGUIを表示することなくバックグラウンドで動作するプログラムです。このカテゴリは、デーモン(システム全体を対象とするバックグラウンドプログラム)とエージェント(特定ユーザのために動作)に分けられます。次の2つのセクションでは、これらの二次分類について詳しく説明します。

デーモン

デーモンは、全体システムの一部としてバックグラウンドで動作する(つまり、特定のユーザとは結びついていない)プログラムです。デーモンではGUIを表示できません。もっと明確にいえば、ウインドウサーバに接続することが許可されていません。Webサーバはデーモンの好例です。

さまざまなデーモンがありますが、それぞれについては以降のセクションで説明します。

注:従来のUNIXに関する予備知識がある場合は、Mac OS Xでデーモンを起動する方法としては、/etc/rc*に変更を加える方法はサポートされていないことに留意してください。正しくは、launchd(Mac OS X 10.4以降)でデーモンを起動するか、起動項目に組み込んでください。

起動項目

起動項目は、起動時にSystemStarterプログラムに起動されるデーモンです。サードパーティの起動項目は、/Library/StartupItemsディレクトリにインストールしてください。起動項目の詳細については、『System Startup Programming Topics』を参照してください。

Mac OS X 10.4からは、起動項目に代わり、launchdデーモンの利用が推奨されています。

重要:互換性の理由から、Mac OS X 10.4以降では、ネットワークが起動して稼働するまで、システムは起動項目を起動しません。ネットワークが立ち上がるまで少し時間が(数十秒)かかることもよくあるので、起動項目が以前のシステムよりも起動が遅いことに気付くかもしれません。この問題を回避する最善の方法は、launchdデーモンを使用するように切り替えることです。

注:Mac OS X 10.3.xの「アカウント」環境設定パネルでは、ログイン項目を指して「起動項目」という用語を使用していました。この紛らわしい用語の使い方はMac OS X 10.4以降では修正されました。

先頭に戻る

mach_initデーモン

mach_initデーモンは、Machの初期化プロセス(Mac OS X 10.3.x以前ではmach_init、Mac OS X 10.4以降ではlaunchd)に起動されます。mach_initデーモンをインストールするには、/etc/mach_init.dディレクトリにプロパティリストファイルを配置します。アップルでは、サードパーティが開発したmach_initデーモンをサポートしていません。

先頭に戻る

inetdおよびxinetdデーモン

inetdデーモンおよびxinetdデーモンは、インターネットスーパーサーバ(もともとはinetdで、現在はxinetd)に起動されます。inetdデーモンをインストールするには、/etc/inetd.confに1行を追加します。xinetdデーモンをインストールするには、/etc/xinetd.dディレクトリに設定ファイルを追加します。

xinetdはMac OS X 10.2で導入されました。xinetdが使用できる場合は、設定が簡単なので、inetdではなく、こちらを選んでください。

Mac OS X 10.4からは、inetdデーモン群とxinetdデーモン群の代わりにlaunchdデーモンの使用が推奨されています。

inetdおよびxinetdデーモンの詳細については、『UNIX Network Programming』およびxinetd Webサイトを参照してください。

先頭に戻る

launchdデーモン

Mac OS X 10.4で、アップルは新しいデーモン起動プログラムとしてlaunchdを導入しました。このプログラムが、システムのほとんどのデーモンを起動する責任を引き受けます。厳密にいうと、launchdはmach_initデーモンと起動項目を起動する責任を負います。

しかし、launchdデーモンは高度なプロパティリストファイルで設定する新しいタイプのデーモンです。このファイルを使用することで、さまざまな基準(リッスンしているソケットへの接続、ファイルシステム内の項目の変更、定期的になど)に基づいてデーモンを起動できます。詳細については、launchd.plistのmanページを参照してください。

サードパーティのlaunchdデーモンをインストールするには、/Library/LaunchDaemonsディレクトリにプロパティリストファイルを追加します。

launchdデーモンは他のタイプのデーモンとは多少異なります。たとえば、launchdデーモンは自身をデーモン化してはなりません。詳細については、launchdのmanページを参照してください。

先頭に戻る

エージェント

エージェントは、特定ユーザのためにバックグラウンドで動作するプロセスです。エージェントが役に立つのは、ウインドウサーバへの接続など、デーモンにはできないことができるからです。カレンダーモニタリングプログラムは、次の理由からエージェントの好例です。

  • ログインユーザごとにプログラムのインスタンスがある。

  • 各エージェントから個別のユーザのカレンダーにアクセスできる。

  • 特定のイベントが発生したときに、エージェントからカレンダーアプリケーションを起動できる。

エージェントとデーモンの違いは、エージェントでは必要に応じてGUIを表示できるのに対し、デーモンではそれができないことです。エージェントと通常のアプリケーションの違いは、エージェントでは通常はGUIを表示しない(または非常に限られたGUIを表示する)ことです。

エージェントは長年にわたって、さまざまな名前を付けられてきました。たとえば、BOA (background-only application)、FBA (faceless background-only application)、UI要素(エージェントは多少のGUIを表示するけれども、メニューバーを備えた本格的なアプリケーションではないというニュアンス)などがあります。これらの名前は、実際のところ、ここでの説明とは無関係です。何が重要で、何が各種のエージェントの違いになるのかと言えば、エージェントを起動する方法です。

注:従来のMac OSに慣れているユーザは、FBAやBOAをデーモンと言い表すかもしれませんが、Mac OS Xではこれは正しくありません。Mac OS XではFBAやBOAはエージェントであり、エージェントはデーモンとはまったく異なります。

注:洞察力の鋭い読者は、システムのKernelEventAgentというプロセスがエージェントと呼ばれているのに、実際にはデーモンであることに気付くでしょう。これはまさに、このテクニカルノートで排除しようとしている類の混乱です。

ログイン項目

ログイン項目は、ユーザがGUIを使ってログインしたときに起動されます。ログイン項目としては、開くことが可能な任意の項目を指定できますが、一般的にはアプリケーションかエージェントです。

ログイン項目をインストールするための唯一のサポートされている方法は、「システムイベント」プロセスにアップルイベントを送信することです。サンプルコードプロジェクト「LoginItemsAE」に、その方法の1つを示します。

先頭に戻る

グローバルログイン項目

グローバルログイン項目は、ユーザがログインしたときに起動されるログイン項目です。グローバルログイン項目をインストールするということは、システムの全ユーザに対して当該ログイン項目をインストールすることとほぼ同じです。ユーザがログインするたびに、loginwindowがそのユーザのログイン項目とすべてのグローバルログイン項目を起動します。

グローバルログイン項目をインストールするためのサポートされている方法はありません。この機能が必要な場合は、Developer Technical Support (DTS)にお問い合わせください。

先頭に戻る

システムログイン項目

システムログイン項目は、ユーザがログインする前に、それぞれのGUIログインセッションの中で起動されるグローバルログイン項目です。ログイン画面が表示されているときにウインドウサーバを使用する必要がある場合は、システムログイン項目が役立ちます。たとえば、リモートユーザがログインウインドウにログインできるようにするスクリーン共有プログラムなどです。

システムログイン項目をインストールするためのサポートされている方法はありません。この機能が必要な場合は、Developer Technical Support (DTS)にお問い合わせください。

先頭に戻る

mach_initエージェント

mach_initエージェントは、特定ユーザとの関連で実行される点を除けば、mach_initデーモンに似ています。GUIユーザにログインするプロセスの一部として、loginwindowに(/usr/libexec/register_mach_bootstrap_serversを通じて)起動されます。mach_initエージェントをインストールするには、/etc/mach_init_per_user.dディレクトリにプロパティリストファイルを配置します。アップルでは、サードパーティが開発したmach_initエージェントをサポートしていません。

重要:mach_initエージェントは、非GUIログインセッションでは起動されません。

先頭に戻る

launchdエージェント

launchdエージェントは、特定ユーザのログインセッションのために実行される点を除けば、launchdデーモンに似ています。ユーザのログインプロセスの一部として、loginwindowに(/usr/libexec/register_mach_bootstrap_serversおよびlaunchdを通じて)起動されます。

サードパーティのlaunchdエージェントをインストールするには、~/Library/LaunchAgentsディレクトリ(該当するユーザのためにのみ呼び出す場合)または/Library/LaunchAgents(すべてのユーザのために呼び出す場合)にプロパティリストファイルを追加します。

現在のシステムソフトウェアにおける制限(r. 4255854)に起因し、launchdエージェントは思うほどには有用ではありません。具体的にいうと、システムはログインセッションごとではなく、システムのユーザごとにlaunchdの1つのインスタンスを作成します。そのため、同じユーザがGUIとSSHでログインしても、利用できるlaunchdエージェントのインスタンスは1つだけなので、どちらのコンテキストを継承するかは保証できません。

先頭に戻る

実行コンテキスト

従来のBSDプログラミングに慣れていれば、プロセスに対応するユーザID(UID。しばしば「プロセスの所有者」という)については知っているでしょう。従来のBSDシステムでは、これらのUIDに応じて当該プロセスの能力が決まります。UIDが一致する2つのプロセスの能力は多かれ少なかれ同じであると想定できます。

これはMac OS Xには当てはまりません。プロセスのコンテキストには、その能力を大きく左右する要素が他にもあります。そのため、たとえば、UIDがコンソールにログインしたユーザのUIDに設定されるデーモンは、そのユーザが起動したアプリケーションと同じではありません。

次のセクションでは、プロセスのコンテキスト要素と、それらがバックグラウンドプログラムに与える影響について説明します。

UID

プロセスのUID(その実効UID (EUID)、実UID (RUID)、および保存UID (SUID))は、プロセスのコンテキスト要素として最もよく知られています。これらのUIDは、プロセスのさまざまな能力を制御しますが、大部分がシステムのBSD部分(ファイルシステム、ネットワーク、BSDプロセス制御)に集中しています。たとえば、プロセスのファイルを開く能力はプロセスのEUIDに応じて決まり、別のプロセスにシグナルを送る能力は送信元のEUIDおよび送信先のEUIDとRUIDに応じて決まります。

先頭に戻る

Machブートストラップの基礎

多くのMac OS Xサブシステムは、中央サービスとMachメッセージを交換することで機能します。このようなサブシステムが機能するには、サービスを見つけられる必要があります。これは通常、Machブートストラップサービスを使用して実行されます。これによって、プロセスはサービスを名前で検索できます。プロセスはすべて、その親からブートストラップサービスの参照を継承します。

この動作を把握するには、BootstrapDumpプログラム(サンプルコードプロジェクト「BootstrapDump」)を実行してください。このプログラムでは、特定のプロセスで利用できるサービスがすべてリストアップされます。リスト1に、「ターミナル」からBootstrapDumpを実行すると得られる出力の例を示します。

リスト1:「ターミナル」からのBootstrapDump

$ BootstrapDump
"com.apple.KernelExtensionServer"
"com.apple.FontObjectsServer" by "/System/Library/Frameworks/Ap[...]
"com.apple.SystemConfiguration.configd" by "/usr/sbin/configd"
"com.apple.audio.coreaudiod" by "/usr/sbin/coreaudiod" is inact[...]
"com.apple.CoreServices.coreservicesd" by "/System/Library/Core[...]
"com.apple.DirectoryService" by "/usr/sbin/DirectoryService"
"com.apple.DiskArbitration.diskarbitrationd" by "/usr/sbin/disk[...]
"com.apple.distributed_notifications.2" by "/usr/sbin/distnoted
"com.apple.system.hdiejectd" by "/System/Library/PrivateFramewo[...]
"com.apple.IIDCAssistant" by "/Library/Audio/Plug-Ins/HAL/iSigh[...]
"com.apple.KerberosAutoConfig" by "/sbin/kerberosautoconfig -x"[...]
"com.apple.system.Kernel[UNC]Notifications" by "/usr/libexec/ku[...]
[...]
"com.apple.dock.server"
[...]
"com.apple.iChatAgent"
[...]

ご覧のように、ブートストラップサービスを通じて多数のサービスが公開されています。これらはほぼすべて外部に対して閉じています。たとえば、“com.apple.dock.server”サービスにメッセージを直接送信することは期待されていません。むしろ、フレームワークを通じてエクスポートされているルーチン(SetApplicationDockTileImageなど)を呼び出すことで、舞台裏で、サービスとの間でメッセージが交換され、要求したジョブが実行されます。

ブートストラップ名前空間

前の例には興味深い点があります。SetApplicationDockTileImageはどのDockと対話するのでしょうか。複数のユーザが(ファストユーザスイッチで)ログインしてれば、Dockの複数のインスタンスが動作します。SetApplicationDockTileImageはどのようにして正しいDockを知るのでしょうか。また、これらのDockは、名前の衝突に苦しむことなく、同じサービス名(“com.apple.dock.server”)を登録できるのはなぜなのでしょうか。

このような問題の解決策は、ブートストラップサービスが複数のブートストラップ名前空間を作成できることです。ログインセッションはそれぞれにブートストラップ名前空間を持っており、当該セッション内で実行されているプロセスはすべて当該空間への参照を継承します。そのため、Dockがそのサービスを登録すると、その登録はログインセッションの名前空間に入ります。そのセッション内にある他のプロセスはすべて同じ名前空間への参照を継承するため、Dockのサービスを参照することができます。他のログインセッションにあるプロセスは別の名前空間を参照するので、このサービスを参照できません。

注:ただし、このようなプロセスでも、その名前空間に登録されているDockの別のインスタンスを参照できます。

GUIおよび非GUIセッション単位ブートストラップ名前空間の違いは注目に値します。GUIセッション単位ブートストラップ名前空間は、ユーザがGUI経由でログインするときに、GUIインフラ(loginwindowおよびウインドウサーバ)によって作成されます。非GUIセッション単位ブートストラップ名前空間は、ユーザがsshd経由でログインするときに作成されます。これらの名前空間に基本的な違いはありませんが、GUIベースのサービスは、Dock同様に、GUIセッション単位ブートストラップ名前空間にのみ登録されます。

注:Mac OS X 10.3より前は、非GUIログインセッションがグローバルブートストラップ名前空間で実行されていました。

注:sshdのプロパティリストファイル(/System/Library/LaunchDaemons/ssh.plist)にはSessionCreateプロパティが含まれているので、非GUIセッション単位名前空間がsshdではなく、launchdによって作成されます。Mac OS X 10.3.xでは、xinetdが類似の機能を実行していました。

先頭に戻る

名前空間階層

ブートストラップ名前空間は階層的に配置されています。グローバルブートストラップ名前空間は1つしかありません。システムがログインセッションの新しいブートストラップ名前空間を作成すると、このセッション単位ブートストラップ名前空間はグローバル名前空間を親として扱います。当該セッションで実行されているプロセスがサービスを登録すると、サービスはセッション単位名前空間のみに出現するので、当該セッション内の他のプロセスにのみ認識されます。しかし、グローバル名前空間に登録されたサービスは、すべてのプロセスに認識されます。

これは実質的には、次のことを意味します。

  • グローバル名前空間を使用しているプロセスは、グローバル名前空間のサービスのみを認識します。

  • セッション単位名前空間を使用しているプロセスは、セッション単位名前空間とグローバル名前空間にあるサービスを認識できます。

  • セッション単位名前空間に登録されたサービスは、セッション単位名前空間を使用しているプロセスにのみ認識されます。

BootstrapDumpを使用してグローバルブートストラップ名前空間を表示するには、プロセスID 1(Mac OS 10.4以降ではlaunchd、それ以前のシステムではinit)を指定します。リスト2にその一例を示します。

リスト2:グローバルブートストラップ名前空間のダンプ

$ sudo BootstrapDump 1
Password:********
"com.apple.KernelExtensionServer"
"com.apple.FontObjectsServer" by "/System/Library/Frameworks/Ap[...]
"com.apple.SystemConfiguration.configd" by "/usr/sbin/configd"
"com.apple.audio.coreaudiod" by "/usr/sbin/coreaudiod" is inact[...]
"com.apple.CoreServices.coreservicesd" by "/System/Library/Core[...]
"com.apple.DirectoryService" by "/usr/sbin/DirectoryService"
"com.apple.DiskArbitration.diskarbitrationd" by "/usr/sbin/disk[...]
"com.apple.distributed_notifications.2" by "/usr/sbin/distnoted
"com.apple.system.hdiejectd" by "/System/Library/PrivateFramewo[...]
"com.apple.IIDCAssistant" by "/Library/Audio/Plug-Ins/HAL/iSigh[...]
"com.apple.KerberosAutoConfig" by "/sbin/kerberosautoconfig -x"[...]
"com.apple.system.Kernel[UNC]Notifications" by "/usr/libexec/ku[...]
[...]
$ sudo BootstrapDump 1 | grep dock

重要:プログラムでは、グローバルブートストラップ名前空間をダンプする機能に依存しないでください。先の例で利用しているテクニックは、アップルが長期的にサポートする予定のものではありません。デバッグと説明にはふさわしいものですが、これに依存したプログラムは出さないでください。

これを見ると、“com.apple.KernelExtensionServer”サービスがグローバルブートストラップ名前空間に登録されており、システムのすべてのプロセスから認識できることが分かります。他方、(リスト1の)“com.apple.dock.server”サービスはログインセッション単位名前空間に登録されており、その名前空間を使用しているプロセスのみが認識できます。

したがって、次のルールを覚えておきましょう。

  • エージェントを開発する場合は、正しいセッション単位ブートストラップ名前空間の参照を継承するようにします。表1に、バックグラウンドプログラムのタイプ別に実行コンテキストを示します。

  • デーモンを開発する場合は、グローバルブートストラップ名前空間の参照を継承するようにします。

  • デーモンを開発する場合は、グローバルブートストラップ名前空間に登録されているサービスだけを使用するようにします。この詳細については、「階層化フレームワーク」を参照してください。

これらのルールに従わないと、正常に機能しているように見えても、いずれ分かりにくい問題に出くわすことになります。次のセクションでは、そのような問題の一例を示します。これは一例にすぎませんが、さらに多くの問題が潜んでいます。このような問題を避ける唯一の保証された方法は、上記のルールを順守することです。

先頭に戻る

名前空間の探索

これまでの説明は理論の上でのことのように思えるかもしれません。また、これがどのように影響するのか思われていることでしょう。ここでは、ブートストラップ名前空間がアプリケーション機能にどのように作用するかについて、具体的な例を検討することではっきり分かるように説明します。

  1. まず、サンプルコードプロジェクト「DockBrowser」をダウンロードすることから始めます。

  2. アーカイブを展開して、「build」フォルダに入っている事前にビルドされたバイナリを実行します。アプリケーションのDockタイルに(小さな赤い円の)カウンタが付いていることに気が付くでしょう。

  3. アプリケーションの環境設定ウインドウを開き(DockBrowserのメニューから「Preferences」を選択)、controlキーを押しながらアプリケーションのDockタイルをクリックすると、環境設定ウインドウを開くコンテキストメニュー項目(「DockBrowser 環境設定」)が表示されます。図1にその様子を示します。

  4. ここで、アプリケーションを終了します。

  5. 次に、「ターミナル」ウインドウを開き、リスト3に示すように、ローカルホストにssh接続します。

    リスト3:sshでローカルホストに接続

    $ ssh localhost
    Password:********
    Last login:Fri Sep 30 10:07:59 2005
    Welcome to Darwin!
    $
    

    注:これを正常に行うためには、「システム環境設定」の「共有」パネルで「リモートログイン」を有効にする必要があります。

    sshを使用してログインすると、システムによってセッションの新しいセッション単位ブートストラップ名前空間が作成されます。そのため、「ターミナル」ウインドウで実行するものはすべて、実行している他のアプリケーションと異なるセッション単位ブートストラップ名前空間で実行されます。

  6. 今度は、リスト4に示すように、この「ターミナル」ウインドウからDockBrowserを実行します。

    リスト4:sshログインセッションからのDockBrowserの実行

    $ Desktop/DockBrowser/build/DockBrowser.app/Contents/MacOS/DockBrowser
    

    図2に、何が表示されるかを示します。次の2つの異常に気が付くでしょう。

    • Dockタイルのカウンタが表示されていません。

    • 環境設定ウインドウを開き、Controlキーを押しながらDockアイコンをクリックすると、コンテキストメニューに「Application Not Responding」項目が表示されます。

    これらの問題が発生するのは、このアプリケーションが別のセッション単位ブートストラップ名前空間で実行されていて、正しく動作するために必要なサービス(“com.apple.dock.server”など)を検索できないためです。

重要:sshセッションから実行されているDockBrowserがGUIを表示できていること自体、実に意外なことです。これが可能なのは、DockBrowserがグローバルウインドウサーバサービスに接続されているためです。この件については、次のセクションで詳しく説明します。

図1:DockにおけるDockBrowser

図1 DockにおけるDockBrowser

図2:sshを通じて起動されたDockBrowser

図2 sshを通じて起動されたDockBrowser

先頭に戻る

ウインドウサーバ

ウインドウサーバ(10.4では/System/Library/Frameworks/ApplicationServices.framework/Frameworks/CoreGraphics.framework/Resources/WindowServerに格納)は、すべてのアプリケーション同士の唯一の接点です。GUIフレームワークの実装(AppKitとHIToolbox)および他のサービス(プロセスマネージャなど)の中核となります。

重要:ウインドウサーバは単にウインドウを管理するだけではありません。ユーザインターフェイスのないアプリケーション(バックグラウンド専用アプリケーションなど)でもウインドウサーバに依存しています。

ウインドウサーバが提供するサービスの大部分は、Machメッセージを使用して実装されています。そのため、ウインドウサーバを確実に使用するには、有効なセッション単位ブートストラップ名前空間の参照を継承する必要があります。これは前述のルールから予期される結果です。

しかし、意外なのは、アプリケーションをGUIログインセッション以外(つまり、アプリケーションがグローバルブートストラップ名前空間、または非GUIセッション単位ブートストラップ名前空間の参照を継承している場合)から実行しても(多少なりとも)動作することです。これは、ウインドウサーバがグローバルブートストラップ名前空間でそのサービスを公開しているからです。これはグローバルウインドウサーバサービスと呼ばれています。

この不明瞭な動作の理由は歴史の奥底に失われています。しかし、いくらかでも動作するという事実は、ほとんど無意味です。というのも、真に有益であることを妨げる重要な問題点があるからです。以下のセクションでは、これらの問題点について詳しく説明します。

警告:アップルはMac OS Xの今後のリリースで、グローバルウインドウサーバサービスを使用不可にする予定です。グローバルウインドウサーバサービスを利用する新しいコードは書かないようにしてください。このサービスを利用する既存のコードがある場合は、長期的な互換性を確保するために、そのような依存関係を排除する必要があります。この問題を回避するためにコードを構造化する方法の具体的なアドバイスについては、「モノリスの危険」を参照してください。

ウインドウサーバを超えて

アプリケーションを正しく機能させるのに必要なサービスは、ウインドウサーバだけではありません。前述したように、Dockサービスも必要ですが、これはGUIセッション単位ブートストラップ名前空間にのみ登録されます。

先頭に戻る

接続許可

コンソールユーザは、GUIログインセッションにコンソールを使用しているユーザです。コンソールデバイス、/dev/consoleは、コンソールユーザが所有しています。

プロセスがグローバルウインドウサーバサービスを利用できるのは、そのEUIDが0(rootとして実行)であるか、コンソールユーザのUIDと同じ場合だけです。それ以外のユーザはこのサービスを利用できません。

これを確認するには、ローカルホストにssh接続し、openツールを使って「アドレスブック」を開いてみます。このツールはLaunchサービスを利用します。つまり、ウインドウサーバに依存しているということです。リスト5に、「ターミナル」からこれを実行する例を示します。最初のopenコマンドは、「ターミナル」と同じユーザとして実行されるので正常に機能します。2番目のopenコマンドは機能しません。テストユーザ(mrgumby)がコンソールユーザと同じではなく、グローバルウインドウサーバサービスにアクセスできないからです。

リスト5:コンソールユーザおよび非コンソールユーザからウインドウサーバへのアクセス

$ ssh ${USER}@localhost
Password:********
Last login:Fri Sep 30 11:50:11 2005
Welcome to Darwin!
$ id
uid=2000(quinn) gid=2000(quinn) groups=2000(quinn), 81(appserveradm), 79(appserverusr), 80(admin)
$ ls -l /dev/console
crw-------   1 quinn  quinn    0,   0 Oct  3 21:27 /dev/console
$ open /Applications/Address\ Book.app/
$ logout
Connection to localhost closed.
$ ssh mrgumby@localhost
Password:********
Welcome to Darwin!
$ id
uid=502(mrgumby) gid=502(mrgumby) groups=502(mrgumby)
$ ls -l /dev/console
crw-------   1 quinn  quinn    0,   0 Oct  3 21:31 /dev/console
$ open /Applications/Address\ Book.app/
kCGErrorRangeCheck :Window Server communications from outside of \
session allowed for root and console user only
INIT_Processeses(), could not establish the default connection to \
the WindowServer.Abort trap
$ logout
Connection to localhost closed.

この制限によって、グローバルウインドウサービスを確実に使用するのが非常に難しくなります。その理由は次のとおりです。

  • 標準的なセキュリティ運用として、デーモンをrootとして実行するべきではなく、専用ユーザとして実行するべきです(つまり、“wombatd”デーモンは専用の“wombat”ユーザが実行するようにします)。したがって、rootとして実行することで問題を解決するのは、セキュリティの点ではやってはいけないことです。

  • コンソールユーザとしてデーモンを実行して、問題を解決する簡単な方法はありません。ファストユーザスイッチが有効な場合、コンソールユーザがいつでも変わる可能性があるからです。

先頭に戻る

ウインドウサーバのライフサイクル

グローバルウインドウサーバサービスの棺に打つ最後の釘は、ウインドウサーバのライフサイクルに関わります。予想とは裏腹に、ウインドウサーバは常に実行されているわけではありません。むしろ、場合によって(下記の注を参照)、ウインドウサーバは終了されたり、launchdに再起動されたりします。グローバルウインドウサーバサービスと同様に、その理由も歴史の奥底に失われています。しかし、その結果は重大です。ウインドウサーバが終了すると、それに接続していたプロセスはすべて強制終了するからです。

注:ウインドウサーバが終了する正確な条件は実装固有のものなので、それに依存しないでください。現在(Mac OS X 10.4)、ウインドウサーバは最後のGUIログインセッションが終わると終了しますが、これは必ずしも最後のGUIユーザがログアウトするときではありません。

リスト6のプログラムを使用すると、この動作を見ることができます。このプログラムはプロセスマネージャのルーチンGetCurrentProcessを呼び出すことでウインドウサーバに強制的に接続し、CFRunLoopRun内で実行を無期限に継続します。

リスト6:ウインドウサーバに接続する小プログラム

#include <Carbon/Carbon.h>
#include <stdio.h>

int main (int argc, const char * argv[])
{
    ProcessSerialNumber psn;

    fprintf(stderr, "Hello Cruel World!\n");

    (void) GetCurrentProcess(&psn);

    fprintf(
        stderr,
        "psn = %#08lx:%#08lx\n",
        psn.highLongOfPSN,
        psn.lowLongOfPSN
    );

    fprintf(stderr, "Running runloop\n");
    CFRunLoopRun();

    return 0;
}

このプログラムをテストするには、別のマシンからsshを使用してログインする必要があります(ウインドウの1つで実行されているプログラムがあると、「ターミナル」はGUIログアウトを保留するからです)。リスト7に、このセッションの様子を示します。

リスト7:GUIログアウトによる死

$ ssh tragic.local.
Password:********
Last login:Fri Sep 30 12:07:18 2005
Welcome to Darwin!

tragic$ ./WindowServerTest
Hello Cruel World!
psn = 00000000:0x0e0001
Running runloop
Killed
Tragic:~ quinn$ echo $?
137

これを自分でやってみるには、次の手順を実行します。

  1. 犠牲マシンのコンソール(この例ではtragic.local.)にログインします。

  2. 別のマシンから、犠牲マシンにssh接続して、WindowServerTestプログラムを実行します。プログラムのプロセスのシリアル番号が出力され、その実行ループの中で無期限に待機します。

  3. GUIを使用してログアウトします。

  4. ログアウトの処理に遅れが生じたことに気が付くはずです。これは、loginwindowが'quit'アップルイベントに対するWindowServerTestの応答を待っているためです。

  5. WindowServerTestプログラムが終了します。終了ステータスを出力すると、killシグナルが原因で終了したことが明らかになります(この終了ステータスは、<sys/wait.h>のマクロを使用してデコードできます。詳細については、waitのmanページを参照)。

このプログラムが強制終了されたのは、ウインドウサーバがそのサービスを使用しているプロセスを追跡・管理しているからです。ログアウトすると、システム(実際にはloginwindow)がそれらのプロセスを終了しようとします。GUIプロセスごとに、プロセスに対して'quit'アップルイベントを送ります。いずれかのGUIプロセスが終了を拒否すると、loginwindowはログアウトを停止し、ユーザにメッセージを表示します。

非GUIプロセスの場合は、若干事情が異なります。loginwindowはまず、'quit'アップルイベントを使用してプロセスを終了しようとします。これが失敗すると、killシグナル(SIGKILL)を送信してプログラムを強制終了します。このシグナルをキャッチしたり無視したりする方法はありません。

その結果、ウインドウサーバに接続しているプロセスは、通常のログアウトで存続できません。

この他にも、ウインドウサーバのライフサイクルに関連してデーモンにとって問題となる問題点があります。ウインドウサーバに実際に接続していない場合でも、デーモンがログインセッション単位ブートストラップ名前空間にサービスを登録すると、トラブルが生じる可能性があります。ウインドウサーバが終了すると、ウインドウサーバによって作成されたログインセッション単位ブートストラップ名前空間は無効になります。この詳細については、「ブートストラップ名前空間:補足」を参照してください。

先頭に戻る

セキュリティコンテキスト

セキュリティコンテキストは、プロセスと関連するもう1つの実行コンテキストです。セキュリティコンテキストは、Mac OS Xセキュリティサーバ(securityd)によって明示的に管理されます。セキュリティコンテキストはほとんどの場合、ブートストラップ名前空間に従います。つまり、ログインセッションごとに、単一のグローバルセキュリティコンテキストrootセキュリティコンテキストともいう)とセッション単位セキュリティコンテキストがあります。

注:セキュリティコンテキストとブートストラップ名前空間が互いに結び付いていない唯一のケースは、独自にブートストラップ名前空間を作成した場合だけです。

ほとんどの場合、セキュリティコンテキストはプログラムと直接的な関係はありません。たいてい、足元をすくわれる原因はブートストラップ名前空間です。

他方、セキュリティコンテキストには素晴らしい特性が1つあります。つまり、(<Security/AuthSession.h>の)SessionGetInfoルーチンを使用すると、コンテキストに関する有用な情報が得られるのです。このルーチンは、現在のセキュリティコンテキストを説明する一連の属性を返します。次のものが含まれます。

  • sessionIsRoot ― グローバルセキュリティコンテキストである場合に設定されている

  • sessionHasGraphicAccess ― このコンテキストで実行されているプログラムがウインドウサーバにアクセスできる場合に設定されている

  • sessionHasTTY ― このコンテキストで実行されているプログラムがターミナル(/dev/tty)にアクセスできる場合に設定されている

  • sessionIsRemote ― このコンテキストがネットワーク経由で実行されている場合に設定されている

重要:Mac OS X 10.4以降、非GUIログインセッションではsessionHasTTYおよびsessionIsRemoteフラグは正しくありません(r. 4280953)。これらのフラグは設定されていませんが、設定されているべきです。

先頭に戻る

実行コンテキストの要約

表1に、デーモンまたはエージェントの実行コンテキストが起動に使用されるメカニズムによって受ける影響を示します。

表1:コンテキスト相互参照

プログラムタイプUIDブートストラップセキュリティ
起動項目rootグローバルグローバル
mach_initデーモンrootグローバルグローバル
inetdデーモン設定どおり[1]グローバルグローバル
inetdデーモン設定どおり[2]設定どおり[3]設定どおり[4]
launchdデーモン設定どおり[5]設定どおり[6]設定どおり[7]
ログイン項目ユーザユーザユーザ
グローバルログイン項目ユーザユーザユーザ
システムログイン項目rootユーザユーザ
mach_initエージェントユーザユーザユーザ
launchdエージェントユーザ信頼不可[8]信頼不可[8]

注記:

  1. inet.conf内でデーモンのエントリの5列目を使用して設定します。

  2. 環境設定ファイルのuser属性を使用して設定します。この属性が指定されていない場合は、rootにデフォルト設定されます。

  3. 環境設定ファイルにsession_create属性が指定されていないかぎり、グローバルブートストラップ名前空間を使用します。指定されている場合、デーモンはそれ自身のセッション単位ブートストラップ名前空間で実行されます。

  4. 環境設定ファイルにsession_create属性が指定されていないかぎり、グローバルセキュリティコンテキストを使用します。指定されている場合、デーモンはそれ自身のセッション単位セキュリティコンテキストで実行されます。

  5. 環境設定ファイルのUserName属性を使用して設定します。この属性が指定されていない場合は、rootにデフォルト設定されます。

  6. 環境設定ファイルにSessionCreate属性が指定されていないかぎり、グローバルブートストラップ名前空間を使用します。指定されている場合、デーモンはそれ自身のセッション単位ブートストラップ名前空間で実行されます。

  7. 環境設定ファイルにSessionCreateキーが指定されていないかぎり、グローバルセキュリティコンテキストを使用します。指定されている場合、デーモンはそれ自身のセッション単位セキュリティコンテキストで実行されます。

  8. launchdエージェントは特定ログインセッションのコンテキストで実行されること前提ですが、これは現在のシステムソフトウェアでは機能しません。この説明については、「launchdエージェント」を参照してください。

図3はその情報を図解したものです。それぞれの四角はプロセスを表します。プロセス名の後に続くテキストは、当該プロセスのUID(丸括弧内のテキスト)であるか、後述の注記の番号(角括弧内のテキスト)です。矢印付きの直線は、2つのプロセス間の親子関係を示します。それぞれのグレーの四角はセキュリティコンテキストの範囲(および関連するブートストラップ名前空間)の範囲を示します。3つのセッション単位セキュリティコンテキストがあり、2つはGUIログインセッション用で(ユーザAおよびB)、1つはsshログインセッション用です(ユーザC)。グレーの四角に入っていない項目は、グローバルセキュリティコンテキストに含まれます。

図3:プロセスの関係

図3 プロセスの関係

注記:

  1. WindowServerは、windowserverのEUID (88)とrootのRUID (0)を使って実行されます。

  2. loginwindowは、ログインユーザのEUIDとrootのRUID (0)を使って実行されます。

  3. ftpdは、ログインユーザのEUIDとrootのRUID (0)を使って実行されます。現在、ftpdはグローバルセキュリティコンテキストで実行されますが、これは今後のシステムで変更される可能性があります。変更された場合、sshdのように機能し、FTPセッションごとにユーザ単位セキュリティコンテキストを作成するようになります。

図3を見ながら、そこに示されているさまざまなプロセスを検討してください。

  • グローバルセキュリティコンテキストには、WindowServerプロセスの単一インスタンスがあります。しかし、ウインドウサーバはすべてのGUIセキュリティコンテキストを認識しています。この場合は、ユーザAのログインセッションに対する1つのセキュリティコンテキスト、そしてユーザBのログインセッションに対するもう1つのセキュリティコンテキストがあります。

  • すべてのアプリケーションはウインドウサーバの子であり、ウインドウサーバは各アプリケーションが正しいセキュリティコンテキストで実行されるように機能します。

  • loginwindowの最初のインスタンスは、ユーザAのログインセッションに対応するものであり、launchdの子です。2番目のインスタンスは、ユーザAからユーザBへのファストユーザスイッチが行われたときに作成されるものであり、ウインドウサーバの子です。

  • loginwindowの各インスタンスが、対応するログインセッションのセキュリティコンテキストを管理します。

  • また、loginwindowは、pbs(ペーストボードサーバ)も直接実行します。

  • Crash Reporterアプリケーションは、実際にはmach_initエージェントです。その親プロセスはlaunchdですが、適切なユーザのセキュリティコンテキストで実行されます。

  • sshdは、SessionCreateプロパティセットを持ったlaunchdデーモンなので、自身のセキュリティコンテキストで実行されます。

  • lookupdはmach_initデーモンです。

  • mds(Spotlightデーモン)は起動項目です。

  • ftpdはlaunchdデーモンです。

警告:この例はMac OS X 10.4に基づいています。説明のためだけに掲載しています。システムで実行される各種プロセス間の正確な関係はこれまでにも変更されており、今後も変更される可能性があります。

先頭に戻る

階層化フレームワーク

Mac OS Xのほとんどの機能は大きなシステムフレームワークによって実装されています。これらのフレームワークの多くは、ブートストラップサービスを利用してルックアップするMachベースのサービスを使用しています。そのため、誤ったブートストラップ名前空間を参照するプログラムからこれらのサービスを呼び出すと、あらゆる問題が引き起こされるおそれがあります。

この問題に対するアップルの解決策は階層化です。フレームワークをレイヤに分割し、レイヤごとに、グローバルブートストラップ名前空間での操作をサポートするかどうかを決めています。基本的なルールとして、CoreService以下のもの(System、IOKit、System Configuration、Foundation)はすべて任意のブートストラップ名前空間で機能し(これらはデーモンセーフフレームワークです)、CoreServicesより上位のもの(ApplicationServices、Carbon、AppKitなど)はすべてGUIセッション単位ブートストラップ名前空間を必要とします。

唯一解決されずに残っている問題は、一部のフレームワークが適切に階層化されていないことです。QuickTimeがその好例です。従来のMac OSから引き継いでいる遺産に起因して、QuickTimeは「コア」と「アプリケーション」フレームワークに明確に階層化されていません。むしろ、1つの大きなフレームワークがあり、どの部分はデーモンから機能するのか文書化されていません。100%の安全を確保する唯一の方法は、これらのフレームワークを使用しないことですが、それは多くの開発者にとって選択肢になりません。ただし、呼び出すルーチンのセットを制限することで、リスクを最小限に抑えられます。この提案については、「危ない橋を渡る」で詳しく説明します。

要約すると、具体的な提案は次のとおりです。

  • デーモンを作成するときには、デーモンセーフなフレームワークにのみリンクします(「フレームワーク相互参照」を参照)。

  • エージェントを開発するときには、任意のフレームワークとリンクできます。

  • デーモンを開発するときに、デーモンセーフでないフレームワークとリンクする必要がある場合は、コードをデーモンコンポーネントとエージェントコンポーネントに分割することを検討してください。これが可能でない場合は、(次のセクションで説明するように)デーモンと安全でないフレームワークとのリンクに伴う潜在的な問題に気を付けてください。

危ない橋を渡る

デーモンセーフでないフレームワークをデーモンが使用する場合は、さまざまな問題に遭遇する可能性があります。

  • 一部のフレームワークはロード時に機能しなくなります。つまり、そのようなフレームワークには、セッション単位コンテキストで実行されることを前提としている初期化ルーチンがあり、そうでない場合は機能しなくなるのです。

  • フレームワークがロード時に機能しなくならなくても、そのフレームワークのさまざまなルーチンを呼び出すときに問題に直面するかもしれません。

    • ルーチンが害なく機能しないことがあります。たとえば、ルーチンが何もメッセージを発せずに失敗したり、stderrにメッセージを出力したり、意味のあるエラーコードを返したりします。

    • ルーチンが害をなして機能しないことがあります。たとえば、GUIフレームワークがデーモンによって実行されている場合に、abortを呼び出すことはよくあることです。

    • フレームワークが正式にはデーモンセーフでなくても、ルーチンが正常に機能することはあります。

    • ルーチンの動作が入力パラメータに応じて異なることもあります。たとえば、画像展開ルーチンが画像のタイプに応じて正常に機能したり、機能しなかったりすることがあります。

  • フレームワークの動作と、そのフレームワーク内のルーチンは、リリースごとに変わる可能性があります。

そのため、デーモンセーフでないフレームワークとデーモンがリンクしている場合、その一般動作は予測できません。ご使用のマシンでは機能するかもしれませんが、他のユーザのマシン、今後のシステムリリース、または異なる入力データで機能しない可能性もあります。このように、危ない橋を渡ることになります。

デーモンセーフでないフレームワークを呼び出す必要がある場合は、バグを登録して、行おうとしていることとその理由を説明するところから始めてください。アップルでは、今後のシステムソフトウェアを開発するときに、ユーザから得た情報を検討します。

次に、潜在的に安全でないルーチンの呼び出しは最小限に抑えるようにしてください。これによって互換性リスクが軽減されます(排除されるわけではありません)。

先頭に戻る

フレームワーク相互参照

表2に、デーモンセーフなフレームワークを要約しています。

表2:デーモンセーフなフレームワーク

フレームワークデーモンセーフかフレームワークデーモンセーフか
AccelerateInterfaceBuilder×
AGL×InstantMessage×
AppKit×IOBluetooth×
AppKitScripting×IOBluetoothUI×
AppleScriptKit×IOKit
AppleShareClient×JavaEmbedding×
AppleShareClientCoreJavaVM○[2]
AppleTalkKerberos○[3]
ApplicationServices×LDAP
AudioUnitMessage×
Automator×OpenAL
AudioToolboxOpenGL×
Carbon×OSAKit×
Cocoa×PCSC
CoreAudioPreferencePanes×
CoreAudioKit×Python
CoreFoundationQTKit×
CoreMIDIQuartz×
CoreMIDIServer○[1]QuartzCore×
CoreServicesQuickTime×
CPlusTestScreenSaver×
DirectoryServiceScripting×
DiscRecording×Security
DiscRecordingUI×SecurityFoundation
DiskArbitrationSecurityInterface×
DrawSprocket×SyncServices×
DVComponentGlue×System
DVDPlayback×SystemConfiguration
ExceptionHandling×Tcl
ForceFeedback×Tk×
FoundationTWAIN×
GLUT×vecLib
ICADevices×WebKit×
InstallerPlugins×  

注記:

  1. これは通常呼び出すフレームワークではありません。

  2. Javaプロパティjava.awt.headlesstrueと定義すると、ウインドウサーバに接続するコードを実行した場合、Javaが例外処理を実行します。テクニカルQ&A、QA1328「Server Processes and the Dock」を参照してください。

  3. 制限あり。詳細については、Developer Technical Support (DTS)に電子メールでお問い合わせください。

この表には、Mac OS X 10.4現在のフレームワークをリストアップしています。フレームワークがここに掲載されていない場合は、デーモンセーフでないと見なすのが最善です。

サブフレームワークの状態に関心があり、そのサブフレームワークがここに掲載されていない場合は、当該フレームワークのアンブレラ(包括)フレームワークの状態を調べてください。たとえば、アンブレラフレームワークのCoreServicesがセーフなので、そのサブフレームワークのOSServicesはセーフだといえます。

フレームワークがセーフとして記載されている場合は、今後のシステムリリースでも引き続きセーフです。フレームワークがセーフでないと記載されていても、今後のリリースでセーフになる可能性があります。

先頭に戻る

設計上の考慮事項

このセクションでは、バックグラウンドプログラムを設計するときに考慮すべき重要なポイントを示します。

必要なのか?

バックグラウンドプログラムを検討する際にまず考慮すべきことは、そのプログラムがそもそも必要かどうかということです。バックグラウンドプログラムは絶えずリソースを消費するので(もっとも、オンデマンド起動することで消費されるリソースを最小限に抑えることができます)、使用を回避できるのであれば、ユーザのためになります。また、バックグラウンドプログラムは共通障害点を持ち込むので、システムを弱体化させる可能性があります(この点で、デーモンはエージェントに劣り、バックグラウンドプログラムがないほうがまだましです)。他方、バックグラウンドプログラムを使用するのは多くの場合、分散状態管理の複雑なメカニズムを使用するのよりはましです。

バックグラウンドプログラムを実装すると決めたら、エージェントかデーモンのどちらが必要なのか決める必要があります。デーモンを使用する主な理由は、さまざまなログインセッションの複数のプロセス間で何らかの状態を共有する必要があるからです。そのような必要がない場合は、エージェントの使用を検討します。

先頭に戻る

モノリスの危険

DTSに届くよくある質問は、「どうすればデーモンからGUIアプリケーションを起動できるのか」というものです。その答えは、「できない」ということになります。これはMac OS Xの階層化アーキテクチャの直接的な結果です。デーモンは、適切でないコンテキストで実行されるので、GUIアプリケーションを起動できません。たとえGUIアプリケーションを実行できるコンテキストを選べたとしても、どれを選ぶでしょうか。また、コンピュータにloginwindowが表示されている場合、有効なGUIコンテキストがありません。何が起こるのでしょうか。

この問題に対する正しい解決策は、プログラムを複数のコンポーネントに分割し、それぞれを特定の役割に分化させることです。たとえば、全体的な状態を管理するデーモンと、ログインする各ユーザのために動作するエージェントを用意することが考えられます。ユーザに特定の質問に答えてもらう必要がデーモンに生じた場合、デーモンからすべてのエージェントにシグナルを送り、エージェントはローカル情報を使用して、実行すべきことを決定します。たとえば、エージェントはGUIまたは非GUIログインセッションのどちらで実行されるかに応じて、あるいはGUIセッションがアクティブである(ファストユーザスイッチ環境でコンソールを使用している)かどうかに応じて動作が異なります。

このアプローチの優れた面は、デーモンの状態が簡素化されることです。デーモンはただ単に、いずれかのエージェントが質問の答を提供することを知っているだけです。いくつのエージェントがあるのか、エージェントがどのタイプのログインコンテキストで実行されているのか、ユーザとどのように対話しているのかについては関心がありません。

先頭に戻る

クライアント/サーバモデル

プログラムを複数のコンポーネントに分割する場合は、クライアント/サーバモデルを利用するようにしてください。厳密にいえば、クライアントがサーバに接続するのであって、逆ではありません。これは分かりきったことに思われるかもしれませんが、このポイントは忘れやすく、いつの間にか苦境に陥ることがあります。

この文脈で言えば、サーバは共有の全体状態を含むバックグラウンドプログラムであり、クライアントはその状態にアクセスするプログラムです。サーバはエージェントの場合もあれば(すべてのクライアントが同じログインセッションで実行される場合)、デーモンの場合もあります(複数のログインセッションのクライアントに対応する場合、または他のデーモンをクライアントとして扱う場合)。重要なポイントは、サーバとそのクライアントの間に1対多の関係があることです。

このような設計では、サーバがクライアントを検出しようとしたり、クライアントに接続しようとしたりしてはなりません。むしろ、クライアントがサーバに接続しなければなりません。この方式は一般的なプロセス間通信(IPC)のAPIにぴったりです。たとえば、UNIXドメインソケットを使用すれば、デーモンを1つのUNIXドメインソケットをリッスンするようにし、クライアントをそのソケットに接続するようにするのは簡単です。また、サーバは(終了または強制終了による)クライアントの接続解除を簡単に処理できます。逆のことをすると、事態が複雑になります。

先頭に戻る

起動

基本設計が落ち着いたら、各コンポーネントを起動する方法を決める必要があります。図4および図5に、ニーズを評価して最善のアプローチを考える方法を示します。

図4:デーモンの起動

図4 デーモンの起動

図5:エージェントの起動

図5 エージェントの起動

このアルゴリズムが示すように、アップルでは可能であれば、launchdを使用してバックグラウンドプログラムを実装することを推奨しています。その理由は次のとおりです。

  • launchdを使用すると、バックグラウンドプログラムをオンデマンド起動するのが簡単になります。

  • launchdを使用すると、バックグラウンドプログラムのインストールと管理が簡単になります(launchctlを使用)。

  • アップルでは、クライアント/サーバ通信にUNIXドメインソケットを使用することを推奨しており、launchdによってこれが簡単になります。

  • 最も一般的な代案である起動項目の使用は、Mac OS X 10.4以降では推奨されません。

先頭に戻る

デーモンIPCに関する推奨事項

ほとんどのデーモンはデーモンとそのクライアント間で通信するために、何らかのプロセス間通信(IPC)を使用しています。デーモンを開発する場合は、設計上の最初の判断事項の1つとして、使用するIPCメカニズムを決定することになります。このセクションでは、デーモンでMachメッセージベースのIPCを使用する場合の落とし穴と、代わりにUNIXドメインソケットを考慮すべき理由を説明します。

重要:このセクションの推奨事項は、異なるブートストラップ名前空間のクライアントと通信するデーモンを作成するときに最も重要です。すべてのプロセスが同じブートストラップ名前空間で実行される場合は、これらの推奨事項を無視できます。たとえば、エージェントを開発する場合に、すべてのクライアントが同じログインセッションのGUIアプリケーションであれば、アップルイベントを使って通信してもまったく差し支えありません。

Machに関する考慮事項

Mach APIはカーネルに対する最低レベルのインターフェイスです。そのため、システムが進化すると、Mach APIは変更される可能性が最も高いと考えられます。アップルでは一貫して、サードパーティにMach APIを使わないよう推奨しています。このことは他のすべてと同様に、デーモンとエージェントにも当てはまります。

ただし、デーモンとエージェントの場合、ブートストラップ名前空間のことを配慮する必要があるため、Mach APIはさらに厄介なものになります。ブートストラップ名前空間を正しく管理することは可能ですが(簡単だという意見もあります)、Mach APIを避けることで問題を完全に回避するほうが無難です。

Mach APIの使用を避けるためにアップルが提案している一般的な推奨事項には、高レベルのラッパーを使用するほうがよいというアドバイスも含まれています。たとえば、Mach APIを使用してMachメッセージを送受信するのではなく、CFMessagePortを使用して同等のことを実行します。これは一般的には良いアドバイスですが、デーモンまたはエージェントを開発する場合、これでもブートストラップ名前空間の問題に突き当たります。水面下では、CFMessagePortCreateLocalがサービスをブートストラップサービスに登録し、CFMessagePortCreateRemoteがブートストラップサービスを利用して登録されたサービスを名前で検索します。そのため、直接的にはMachメッセージの使用を避けていても、Machメッセージの上に階層化されたAPIを使用することになるので、依然としてブートストラップ名前空間のことを配慮する必要があります。

足元をすくわれる可能性が高い高レベルのIPC APIは次のとおりです。

  • CFMessagePort ― このAPIでは、すべての名前付きメッセージポートがブートストラップサービスを利用して実装されます。

  • Distributed Objects(DO) ― 名前付きDO接続(-[NSConnection registerName:]を使用して登録)が、ブートストラップサービスを利用して実装されます。

  • アップルイベント ― アップルイベントはMachメッセージに基づいて実装されます。(<AE/AEMach.h>のルーチンを使って)デーモンでアップルイベントを使用することは可能ですが、注意を必要とする作業になります。

全体的に見れば、Machメッセージを完全に避けるほうが簡単です。Mac OS Xには、代わりに使用できるIPCメカニズムがいろいろ用意されています。個人的には、UNIXドメインソケットが気に入っています。

先頭に戻る

有用なUNIXドメインソケット

UNIXドメインソケットは、通信が必ずコンピュータにローカルなものである点を除けば、TCP/IPソケットに多少似ています。UNIXドメインソケットにアクセスするには、TCP/IPソケットに使用するのと同じBSDソケットAPIを使用します。主要な違いはアドレス形式です。TCP/IPソケットの場合、(bindconnectなどに渡す)アドレス構造体は(struct sockaddr_in)であり、IPアドレスとポート番号が含まれています。UNIXドメインソケットの場合、アドレス構造体は(struct sockaddr_un)であり、パスが含まれています。

サーバがUNIXドメインソケットにバインドすると、ソケットを表すファイルシステムオブジェクトがシステムによって作成されます。たとえば、PPPデーモンのUNIXドメインソケットは/var/run/pppconfdです。このソケットをls -lリスト8を参照)で見ると、リストの最初の文字が「s」であり、このオブジェクトがソケットであることが分かります。

リスト8:PPPのUNIXドメインソケットの表示

$ ls -l /var/run/pppconfd
srwxrwxrwx  1 root  daemon  0  9 May 12:57 /var/run/pppconfd

サーバが実行されると、クライアントはそのパスをconnect呼び出しに渡すだけでサーバに接続できます。接続が確立されると、TCP/IPソケットの場合と同様に通信が進行します。

UNIXドメインソケットの詳細について、一般的な任意のUNIXリファレンスを参照してください。特に、『UNIX Network Programming』(Stevensほか)をお勧めします。

クライアント/サーバ環境におけるUNIXドメインソケットの使用例については、サンプルコードプロジェクト「CFLocalServer」を参照してください。

UNIXドメインソケットの利点

デーモンを実装する場合、UNIXドメインソケットAPIは他のIPCメカニズムに勝るいくつかの利点を備えています。

  • TCPソケットと比較すると、ローカルマシン上で実行されているプロセスだけがサーバに接続できることが保証されます。TCPでもこのような保証は得られますが、余分な作業が必要になります。

  • アップルイベントと比較すると、すべてのバージョンのMac OS Xで機能します(Mac OS X 10.2以前ではデーモンからアップルイベントを使用すると、問題が生じます)。また、これはコネクション型のAPIなので、サーバはクライアントの機能停止を自動的に認識し、クライアントに非同期で簡単に通知できます。

  • Machメッセージ(およびCFMessagePortなどの高レベルラッパー)と比較すると、ブートストラップ名前空間を配慮する必要がありません。また、Machメッセージを使用する場合、クライアントの突発的な機能停止をサーバに通知には余分な作業が必要になります。

  • スレッド、実行ループ(CFSocketを使用)、選択ループ、またはkqueueなどをベースにした任意のサーバアーキテクチャに簡単に統合できます。

  • よく知られたPOSIX APIであり、優れた資料がたくさん揃っています。

  • UNIXドメインソケットコードをベースにしたソースコードは、他のPOSIXプラットフォームに移植可能です。

  • launchdデーモンにうまく統合できます。

先頭に戻る

ベストプラクティス

UNIXドメインソケットを使用する場合は、次の点に留意してください。

  • UNIXドメインソケットは、ファイルシステム内の項目として表されます。クライアントとサーバは通常、このソケットへのパスをハードコーディングします。適切なディレクトリ(/var/tmpなど)へのパスを使用し、そのディレクトリで一意の名前をソケットに付けてください。たとえば、サンプルコードプロジェクト「CFLocalServer」では、/var/tmp/com.apple.dts.CFLocalServer/Socketというパスを使用しています。

  • UNIXドメインソケットを作成するときには、ファイルシステムの競合状態が引き起こすセキュリティ問題を回避するように注意してください。これについても、サンプルコードプロジェクト「CFLocalServer」に、回避方法の1つが示されています。サンプルをダウンロードして、Server.cSafeBindUnixDomainSocketルーチンを参照してください。

  • あるいは、デーモンをrootとして実行する場合は、UNIXドメインソケットを/var/runに置くことができます。このディレクトリは特権プロセスのみ書き込み可能なので、前述したセキュリティ問題を回避できます。

先頭に戻る

コーディングに関する推奨事項

次のセクションでは、バックグラウンドプログラムのコーディングに関するさまざまな推奨事項を示します。

オンデマンド起動

システムリソースの使用を最小限に抑えるには、バックグラウンドプログラム(デーモンまたはエージェント)をオンデマンドで起動するのが一番です。つまり、いつも起動時(またはエージェントの場合は、ログイン時)に起動するのではなく、サービスが必要なときにだけプログラムを起動します。

Mac OS Xでは、この目的を実現するためのさまざまな方法が用意されています。これらの方法はすべて、共通のアプローチを採用しています。

  • 起動の基準を示す環境設定ファイルをインストールします。

  • スーパーサーバがこれらのファイルを読み取り、起動基準が満たされるのを待ちます。

  • 起動基準が満たされると、スーパーサーバがプログラムを起動し、起動をトリガしたイベントに関する情報を取得する方法を提供します。

起動基準に応じて、多種多様なスーパーサーバを利用できます。

先頭に戻る

デーモン化

デーモンを開発する場合、起動時にデーモン化が必要になることもあります。これが必要な理由については本書の範囲を超えています。詳細については、『Advanced Programming In The UNIX Environment』を参照してください。

デーモン化が必要かどうかは、起動した方法に応じて決まります。表3に、デーモンのタイプごとにデーモン化の必要性を示します。

表3:デーモン化の必要性

デーモンタイプデーモン化
起動項目必要
mach_initデーモン非推奨
inetdデーモン禁止
inetdデーモン禁止
launchdデーモン禁止

デーモン化する必要がある場合は、daemonルーチンを使って実行できます。

先頭に戻る

ロギング

ほとんどの場合、バックグラウンドプログラムにアクティビティ情報を記録させたいと考えることでしょう。このようなログ記録は初期の立ち上げ時に役立ちますし、ユーザが問題をトラブルシューティングを行う場合にも使えます。Mac OS Xでは、いくつかのロギング機能をサポートしています。

Apple System Log

Apple System Log (ASL)はMac OS X 10.4で導入された新しいロギング機能です。これによって、柔軟で構造化されたログエントリを作成できます。また、これらのログを照会する管理ツールを開発することもできます。

ASLの詳細については、そのmanページを参照してください。その使用例については、サンプルコードプロジェクト「SampleD」を参照してください。

先頭に戻る

syslog

syslogはBSDの伝統的なロギング機能です。syslogを使用して(比較的構造化されていない)メッセージを記録でき、そのようなメッセージを処理する方法を設定できます。

syslogの詳細については、syslogdsyslog、およびloggerプログラム、syslog.conf環境設定ファイル、ならびにsyslog APIに関するmanページを参照してください。

先頭に戻る

コンソールログ

ほとんどのデーモンは、ASLまたはsyslogを使ってログを記録します。一般的なデーモンでは、stderrは何か有用なものにはつながっていません。実のところ、daemonルーチンはstdinstdout、およびstderr/dev/nullにリダイレクトします(このルーチンの詳細については、「デーモン化」を参照)。

対照的に、ほとんどのエージェントではstderrがコンソールに接続された状態で実行されるので、ロギングはfprintを呼び出すだけという簡単なものです。

コンソールへのロギングについては、テクニカルノートTN2124「Mac OS Xにおけるデバッグの魔法」を参照してください。

先頭に戻る

セキュリティの考慮事項

本質的に、デーモンはシステム全体を対象とするサービスです。そのため、デーモンを開発するときには、セキュリティについて配慮する必要があります。セキュアなプログラミングに関する詳細な議論は本書の対象外ですが、考慮すべき事項の簡単なリストを以下に示します。

  • できれば、デーモンではなくエージェントを開発します。これでセキュリティ問題の範囲が限定されます。

  • デーモンがネットワークを使用する場合は、ネットワークから受信するデータを信用しないでください。信用すると、リモートからの攻撃にデーモンが屈するおそれがあります。

  • デーモンをインストールするときには、ファイルシステム許可を正しく設定してください。アップルでは、デーモンは所有者をrootとし、所有グループをwheelとして、実行可能ファイルとディレクトリには755 (rwxr-xr-x)のパーミッション、ファイルには644 (rw-r--r--)パーミッションを使用することを推奨しています。これを正しく設定しないと、ローカルユーザがデーモンを変更してその特権をエスカレートするおそれがあります。

  • 引き上げた特権(rootとして実行するなど)でデーモンを実行しないでください。引き上げた特権でデーモンを実行する必要がある場合は、非特権プロセスから受信するデータを信用しないでください。信用すると、ローカルユーザが特権をエスカレートできるようになるおそれがあります。

  • サービス妨害攻撃を監視します。クライアントにメッセージを送信するデーモンの場合、クライアントからまったく応答がない場合でも正しく機能するようにします。クライアントによって、デーモンのメモリ、ファイル記述子、その他の任意のリソースが使い果たされることがないようにしてください。

  • その他の一般的なセキュリティ問題、特にバッファオーバーランに注意してください。デーモンの性質上、これらの問題は通常のプログラムよりも厄介なものになります。

セキュアなソフトウェアの開発の詳細については、このトピックに関して入手可能な多数のテキストのいずれかを参考にしてください。『Building Secure Software』を強くお勧めします。

先頭に戻る

エージェントとファストユーザスイッチ

エージェントを開発する場合は、ファストユーザスイッチにかかわる微妙な問題に気を付けてください。たとえば、エージェントは次のことを認識しなければならない場合もあります。

  • エージェントがGUIまたは非GUIログインセッションで実行されているかどうか

  • そのGUIログインセッションがアクティブ(つまり、コンソールを使用している)かどうか

  • その他

詳細については、『Multiple User Environments』を参照してください。

先頭に戻る

ヒントとこつ

このセクションでは、バックグラウンドプログラムを開発するときに役立つさまざまなヒントとこつについて説明します。

デーモンの起動

デーモンを開発する場合、インストール直後にデーモンを起動すると問題に遭遇することがあります。2つの本質的に矛盾する要求があります。

  • ユーザに再起動させたくない。

  • 設定アプリケーション(またはインストーラ)からデーモンを直接起動すると、デーモンが不適切なコンテキストを継承する(詳細については、「実行コンテキスト」を参照)。

この難問を解決する方法は3つあります。

  • デーモンがデーモンセーフなフレームワークのみを使用し、Machメッセージングサービスを直接または間接的に使用したり登録したりしない場合は、デーモンを直接起動できるはずです。デーモンが不適切なコンテキストで実行されますが、それで問題が生じることはないはずです。

  • launchdデーモンを使用する場合は、launchctlを使用してこれを起動できます。launchctlは、launchdにメッセージを送信し、ユーザに代わってデーモンを起動するように要求することで、デーモンがlaunchdの子となり、正しいコンテキストが継承されます。

    重要:これを適切に機能させるには、launchctlをrootとして実行する必要があります(EUIDとRUIDがともに0である必要があります)。これによって、launchctllaunchdのプライマリインスタンスと通信できます。

  • それ以外の場合、StartupItemContextツールを使用して、グローバルブートストラップ名前空間でデーモンを起動できます。このツールは完全ではありませんが(r. 4283301)、ほとんどの状況で正常に機能します。

注: StartupItemContextはMac OS X 10.3で導入されました。また、そのベースとなるAPI (bootstrap_parent)は、Mac OS X 10.3まで適切に機能しませんでした。旧システム上で設定アプリケーション(またはインストーラ)からグローバルブートストラップ名前空間の中でデーモンを起動する必要がある場合は、Developer Technical Support (DTS)までお問い合わせください。

先頭に戻る

起動のデバッグ

デーモンとエージェントは通常、間接的に実行されるので(つまり、launchdなどのシステムサービスによって従属起動されるので)、それらの起動コードをデバッグするのが難しいことがあります。この場合に利用できる2つの手段があります。

  • デーモンを標準ツールとして(たとえば、起動項目が自身をデーモン化せずに)実行するコマンドラインオプション(伝統的に、“debug”を意味する“-d”)を追加できます。そうすれば、起動コードをGDBから直接デバッグできます。

  • 先の方法が(一般的には、デーモンが実行されているコンテキストに関連するバグが原因で)十分でない場合、リスト9に示すように、デーモンの起動コードにpauseシステムコールを追加できます。これによって、シグナルが届くまでプログラムが停止します。その後、GDBをプロセスにアタッチするとシグナルがプロセスに送信され、停止が解除されます。

    注:デーモンが別のユーザとして実行されている場合、そのデーモンにアタッチしてデバッグするには、GDBを当該ユーザまたはrootとして開始する必要があります。通常、これはsudo gdbを使って行います。

リスト9:起動の一時停止

if (1) {
    fprintf(
        stderr,
        "Process %ld waiting for debugger.\n",
        (long) getpid()
    );
    pause();
}

先頭に戻る

ウインドウサーバの想定外使用のデバッグ

リスト5に示すようなメッセージを出してデーモンが機能停止し、ウインドウサーバに接続した理由が考えられない場合は、この問題をデバッグするために実行できることがいろいろあります。まず、INIT_Processes環境変数を設定することです。コンソールログの結果メッセージを目にしたら、GDBを使用してアタッチし、バックトレースを実行して何が接続を引き起こしたのかを確認できます。この環境設定変数の詳細についてはテクニカルノートTN2124「Mac OS Xにおけるデバッグの魔法」を参照してください。

先頭に戻る

ログの監視

バックグラウンドプログラムを開発するときには、ログファイルを常に監視することをお勧めします。予期しないログメッセージによって、不明瞭な問題の切り分けが迅速にできることがどれだけ多いか驚くかもしれません。これはsyslogを使用してログを記録し、バックグラウンドで「コンソール」プログラムの実行するという簡単な方法で実現できます。

同様に次の方法が考えられます。

  • 起動時に起動されるデーモンを開発する場合は、起動時にcommand-Vキーを押し続けることで、そのログメッセージを表示できます。冗長ブートを無期限に有効にするには、リスト10に示すように、boot-args Open Firmware変数に“-v”引数を追加します。

  • エージェントを開発する場合、そのログメッセージは通常、コンソールログに表示されます。

リスト10:冗長ブートの有効化

$ sudo nvram boot-args="-v"
Password:********

先頭に戻る

ブートストラップ名前空間:補足

ブートストラップ名前空間に関する先の議論(「ブートストラップ名前空間」を参照)では、その作成方法について広く述べましたが、それらを破棄する方法に関する問題を積み残していました。最終的には、ブートストラップ名前空間は、それを参照する最後のプロセスが終了するまで存続します。そのため、プロセスが実行されているかぎりは、ブートストラップ名前空間が有効であることが保証されています。

しかし、落とし穴が1つあります。システムがブートストラップ名前空間を作成すると、名前空間はそれを作成したプロセスと関連付けられます。そのプロセスが終了すると、名前空間は非アクティブになります。たとえば、sshログインセッションからデーモンを起動して、その後ログアウトすると、ブートストラップ名前空間を作成したプロセス(事実上、これはsshd)が終了し、そのプロセスが作成したブートストラップ名前空間(つまり、デーモンが継承した名前空間)は非アクティブになります。

非アクティブになったブートストラップ名前空間でもサービスをルックアップできますが、新しいサービスを登録することはできません。非アクティブ化な名前空間にサービスを登録しようとすると、BOOTSTRAP_NOT_PRIVILEGED (1100)というエラーで失敗します。同様に、サービスを登録する高レベルラッパーも失敗します。たとえば、CFMessagePortCreateLocalは、名前空間が非アクティブになった後で呼び出すと、エラーメッセージを出力してNULLを返します。

初期バージョンのMac OS X(Mac OS X 10.2よりも前)では、ブートストラップ名前空間は非アクティブになりませんでした。名前空間を作成したプロセスが終了すると、ブートストラップサービスがすぐに名前空間を破棄していました。その名前空間を参照するプロセスはすべて、ブートストラップ名前空間のない状況になりました(厳密には、それらのブートストラップポートはMachデッド名になります)。

アップルは間違った名前空間で不適切に実行されているプログラム(一般的に、プログラム作成者のブートストラップ名前空間に対する理解不足が原因)の互換性を改善するために、一時しのぎの手段としてブートストラップ名前空間をそのように破棄するのではなく、非アクティブにすることにしたのです。将来のシステムでは、非アクティブなブートストラップ名前空間の考え方をなくし、10.2以前の動作に戻る可能性があります。プログラムが正しい名前空間で実行されているかぎり、これは互換性に関していかなる問題も引き起こしません。

先頭に戻る

参考資料

先頭に戻る

ドキュメント改訂履歴

日付メモ
2006-07-13
2006-01-03デーモンとエージェントに関する最も一般的な問題を説明し、詳細な解決策を提案。

掲載日: 2006-07-13




Did this document help you?
Yes: Tell us what works for you.

It’s good, but: Report typos, inaccuracies, and so forth.

It wasn’t helpful: Tell us what would have helped.