プロセスの源流を見に行く

MacBookが起動している間、実に様々なアプリケーションが稼働して、快適な作業環境を提供してくれる。Dockやメニューバーには、自分の意志で明示的に起動されたアプリケーションが並ぶ。しかし、そこに見えているのはほんの一部で、その裏ではさらに多くのアプリケーションが稼働している。それらは常にバックグラウンドで稼働し、UI*1を持たない。だから、厳密にはアプリケーションとは言えないかもしれない。

普段はその存在をほとんど感じることはないが、アクティビティモニタ*2を起動すると簡単に確認できる。ツールバーで「すべてのプロセス」にしてみると、自分の環境ではその数およそ100プロセス*3。結構な数だ。

それらの暗黙的に稼働している(はるかに多い)プロセスは、一体いつ起動されたのだろう?それを調べるには、やはりアクティビティモニタツールバーで「すべてのプロセス(階層表示)」にしてみると、ある程度想像がつく。

プロセスには親子関係があって、あるプロセスBには、それを起動した別のプロセスAが必ず存在する。(BはAの子プロセス、逆にAはBの親プロセス、という関係になる)また、プロセスには起動した順番にプロセスID(PID)が付番されている。PIDが小さいほど早いタイミングで起動されたプロセス、大きければ遅いタイミングで起動されたプロセスになる。

PID昇順に並べ替えて、先頭を見てみる。すると、あらゆるOSX 10.6は以下のように始まっていると思う。

  • PID 0 = kernel_task
  • PID 1 = launchd

kernel_taskは子を持たず単独のプロセスだが、launchdは子を持っている。驚いたことに、launchd以降に起動したプロセスは、すべてlaunchdの子プロセスだったのである。また、よく見るとlaunchdの子プロセスの中にも、さらに別のlaunchdが見つかる。PID 1の方はroot権限のlaunchdだ。その子プロセスのlaunchdは、ユーザー権限のlaunchdだ。おそらく、OS共通のプロセス管理か、ログインしたユーザーのためのプロセス管理かで分担しているように見える。

  • root権限のlaunchdは、すべてのユーザー共通の環境を作るプロセスを管理する役割。
  • ユーザー権限のlaunchdは、ログイン後のそのユーザー環境を作るプロセスを管理する役割。

launchd

launchdは、すべてのプロセスの母だったのである。では、launchdとは何か?その概要については、以前の日記が参考になるかもしれない。

launchdの使い方

設定ファイルは、以下のように明示的にloadする必要がある。

  • plist形式(xml)の設定ファイルを作る。
    • 例1:~/Library/LaunchAgents/com.apple.MobileMeSyncClientAgent.plist(MobileMeと同期する設定)
    • 例2:/System/Library/LaunchAgents/com.apple.Finder.plist(ログインしたユーザーごとにFinderを常時起動する設定)
  • ロードする
$ sudo launchctl load FILE_PATH
  • アンロードする
$ sudo launchctl unload FILE_PATH
  • ロードされているリスト
$ launchctl list
自動ロード

OS起動時・ログイン時には、以下の設定ファイルは自動的にロードされる。

  • /Users/zari/Library/LaunchAgents
    • zariユーザーがログインした時だけ、ロードされる。
  • /Library/LaunchAgents
    • 各ユーザーがログインした時に、それぞれロードされる。(サードパーティの設定)
  • /System/Library/LaunchAgents
    • 各ユーザーがログインした時に、それぞれロードされる。(OSXの設定)
  • /System/Library/LaunchDaemons
    • OS起動時にロードされる。(OSXの設定)
LaunchAgentsとLaunchDaemonsの違い

LaunchAgents とは...

  • ログイン時にロードされる。
  • 各ユーザーごとのlaunchdの子プロセスとなる。
  • GUI環境を利用したプロセスも、起動できる。


LaunchDaemons とは...

  • OS起動時にロードされる。
  • PID 1 のlaunchdの子プロセスとなる。
  • GUI環境を利用しないプロセスのみ、起動できる。


例:

  • /System/Library/LaunchAgents/com.apple.Finder.plist によって、Finderは、各ユーザーがログインした時に、それぞれ起動される。
  • LaunchAgentsに設定されるのは、FinderがGUIアプリケーションであるため。
  • 仮に、/Users/zari/Library/LaunchAgents/com.apple.Finder.plist では、zariユーザーがログインした時しか起動しなくなってしまう。
  • /System/Library/LaunchDaemons/com.apple.syslogd.plist によって、システムのログをとるために、syslogdが起動される。
  • LaunchDaemonsに設定されるのは、ログイン前からのあらゆるシステムのログを取得するため。
  • 仮に、/System/Library/LaunchAgents/com.apple.syslogd.plistでは、誰かがログインした以降のログしか取得できなくなってしまう。

kernel_task

それでは、launchdよりも先に起動している、PID 0 のkernel_taskとは何だろうか?実は、これこそがOS本体(カーネルと呼ばれている部分)のプロセスである。

  • kernel_taskは、それ以降に起動されるすべてのプロセス(PID 1 のlaunchdも含む)に対して、作業する場所と時間を分け与える。
    • 作業する場所とは、メモリ領域である。時間とは、CPUを利用する時間である。
  • 起動するタイミングを決めるのはlaunchdだが、起動したプロセスはkernel_taskによって、完璧にコントロールされているのだ。
特権モードとユーザーモード
  • kernel_taskは特権モードで実行される唯一のプロセスである。
  • 一方、それ以外のプロセスは、すべてユーザーモードで実行される。
  • 特権モードはユーザーモードより優先されるので、kernel_taskはその他のプロセスがマナー違反を犯そうとしても、対抗できる。
  • また、ユーザーモードでは実行できない命令、アクセスできないメモリ領域がある。
制限と代理実行・最適分配
  • プロセスが分け与えたメモリ領域以外にアクセスしようとすると、kernel_taskはそのプロセスに処理を中止させる。
    • その代わりkernel_taskはプロセスより依頼を受ければ、ユーザーモードでアクセスできないメモリ領域に代理でアクセスしてあげる。(許可できるアクセスならば)
  • プロセスが分け与えた時間以上にCPUの利用を独り占めしようとしても、kernel_taskはタイマーを見て、そのプロセスにCPUの使用を中止させる。
    • しかし、kernel_taskはすべてのプロセスにCPUの利用時間を均等に分配している訳ではない。
    • 忙しいプロセスにはより多くの時間を、暇そうなプロセスにはより少ない時間を、それぞれの状況を察する気遣いもしている。
外界とのやり取り
  • CPUができることは非常に少なくて、実はCPU内部のレジスタとメモリにしかアクセスできない。(実際にはもう少し複雑だが)
  • でも現実には、ハードディスクからプログラムやデータを読み込んで、ディスプレイに処理結果を表示している。どうやっているのか?
  • 実はメモリ領域の一部が、外部機器に対応している。
    • 特定のメモリ領域が、ハードディスクをコントロールするスイッチになっていたり、
    • 特定のメモリ領域をVRAMとして利用して、メモリの値0・1が、ディスプレイの1ピクセルのオン・オフに対応していたりする*4
  • CPUにとってはあくまでメモリへのアクセスだけど、そのメモリの変化が外部機器に影響を与えてコントロールしているのだ。
  • このような外界と繋がる特殊なメモリ領域は、IO領域と呼ばれている。
ドライバ
  • CPUが外部機器にアクセスする場合は、IO領域と呼ばれる特殊なメモリ領域へアクセスすることで実現される。あるいは、IO領域へアクセスする専用の命令が用意されている。
  • どちらにしても、IO領域へのアクセスでは非常に低レベルな操作しかできず、しかも外部機器ごとに固有のルールがあるので、そのままではものすごく面倒なことになる。
  • そんな状況を解決すべく、ドライバと呼ばれる、外部機器をもっと簡潔に操作するためのソフトウェアが用意されるようになった。
ドライバが実行される場所
  • ドライバは、外部機器をコントロールするために、どこかで必ずIO領域へアクセスする必要がある。
  • IO領域は、特権モードのCPUしかアクセスできない領域になっている。
  • ドライバがユーザーモードのプロセスとなったことで、プロセス同士(プロセスとドライバ間)のデータ通信手段が必要になった。
  • また、ドライバがIO領域へアクセスする時に特権モードが必要になるので、カーネルに処理を依頼する必要もある。(2回の手間)
  • 仮に、ドライバがカーネルに組み込まれていたら、プロセスはカーネルにドライバの処理を依頼するだけで完了する。(1回で完了)
  • マイクロカーネルよりも、ドライバが組み込まれたカーネルの方が、処理効率としては良いのである。
  • そこでOSXでは処理効率を考えて、マイクロカーネルに再びドライバを組み込んでしまった*6
  • 但し、何の工夫もなくカーネルにドライバを組み込んでしまうと、ドライバを追加・変更する度にカーネルの再構築が必要になってしまう。
  • そのためOSXでは、稼働中のカーネルであっても、カーネル機能拡張としてドライバを自由に追加・削除できる仕組みになっている。

このカーネル機能拡張のおかげで、以下の恩恵を受けられるのだ。

  • カーネル本体は、小さくシンプルなまま、オープンソースとして公開できる。
  • ドライバは、外部機器のメーカーが開発して、提供してもらう。(カーネルとは分離されているので、オープンソースでなくても構わない)
  • ドライバは必要に応じてカーネルに組み込まれるので、効率も良い。
カーネル機能拡張の使い方

カーネル機能拡張は、以下のように明示的にカーネルにloadする必要がある。

  • ロードして、カーネルに組み込む。
    • カーネルは権限に厳格で、オーナー=root、グループ=wheel、でないとロードできない。
    • vオプションでメッセージを出力している例。
$ sudo cp -R MyDriver.kext /tmp
$ sudo chown -R root:wheel /tmp/MyDriver.kext
$ sudo kextload -v /tmp/MyDriver.kext
Requesting load of /tmp/MyDriver.kext.
/tmp/MyDriver.kext loaded successfully (or already loaded).
  • アンロードして、カーネルから取り除く。
    • vオプションでメッセージを出力している例。
$ sudo kextunload -v /tmp/MyDriver.kext
com.MyCompany.driver.MyDriver unloaded and personalities removed.
$ kextstat | grep -v com.apple
Index Refs Address    Size       Wired      Name (Version) 
  108    0 0x13bb000  0xe000     0xd000     com.iospirit.driver.rbiokithelper (1.5.4) <68 35 29 5 4 3 1>
  127    0 0x95e7a000 0x2000     0x1000     com.bresink.driver.BRESINKx86Monitoring (2.0) <12 11 10>
  128    3 0x9603b000 0x25000    0x24000    org.virtualbox.kext.VBoxDrv (3.2.8) <7 5 4 3 1>
  129    0 0x95f10000 0x7000     0x6000     org.virtualbox.kext.VBoxUSB (3.2.8) <128 49 35 7 5 4 3 1>
  130    0 0x95ee0000 0x4000     0x3000     org.virtualbox.kext.VBoxNetFlt (3.2.8) <128 7 5 4 3 1>
  131    0 0x95eb3000 0x5000     0x4000     com.google.driver.Gild (1.0.0) <5 4 3 1>
  132    0 0x95eae000 0x3000     0x2000     org.virtualbox.kext.VBoxNetAdp (3.2.8) <128 5 4 1>
  133    0 0x7f50b000 0x18000    0x17000    org.pqrs.driver.KeyRemap4MacBook (7.3.0) <29 5 4 3 1>
  144    0 0xb75000   0x2000     0x1000     com.bebekoubou.driver.ClamshellWake (1) <4 3>
自動ロード

OS起動時に、以下のフォルダ内のカーネル機能拡張は、自動的にロードされる。

  • /System/Library/Extensions/
  • /Library/Extensions/
      • 自作の ClamshellWake.kext を /Library/Extensions/ に入れても起動時にロードされなかった。(/System/Library/Extensions/ ならロードされるのだけど)
      • ところが、/Library/Extensions/VBox*.kext 関連は、ちゃんと起動時にロードされている。なぜだろう?
kernel_taskのメモリ
  • アクティビティモニタのシステムメモリで 固定中(Wired)は、kernel_taskが確保している分である。
  • kernel_taskは、常に物理メモリ上に展開されて、実行される。
  • 処理効率を低下させないことを目指しているので、スワップしない。
kernelのキャッシュ
  • 起動時にカーネル機能拡張を一つずつロードしていては、時間がかかってしまう。
  • そのため、終了時のkernel_taskのメモリに展開された状態をファイルに保存(キャッシュ)しておき、
  • 次回起動時には、キャッシュされたkernel_taskを素早く読み込んで、メモリに展開している。
  • OSインストールやアップデート後、2回目以降の起動時間が短縮されるのは、この仕組みによる。


...と、ここまで書いたが、全然源流にたどり着けない...。launchdとkernel_taskのことを調べるだけで手一杯になってしまった。

kernel_task以前はどうなっているのか?また、kernel_task以降についても、時系列にはまだ何も分かっていない。まだまだ疑問は山積みである。プロセスの源流を辿る旅は、まだまだ続く。

      • 以上のことは現状の自分の理解なので、不足しているところ、勘違いしている部分があるかもしれない。(教えて頂けると嬉しいです)

*1:ユーザー・インターフェース。ユーザーがマウス・キーボードで直接操作する手段。あるいは、その処理結果を確認する手段。

*2:/アプリケーション/ユーティリティ/アクティビティモニタ。/Applications/Utilities/Activity Monitor.app。

*3:OSは稼働中のプログラムをプロセスという単位で管理している。同じメモリ領域を分け与えられたプログラムの実行単位と考えられるだろうか。例:Safari 5.1は1つのアプリケーションだが、プロセスとしてはSafari 本体、Safari Webコンテンツ、Flash Player プラグイン、と3つのプロセスに分かれて稼働しているのだ。Chromeは、タブ1つひとつが独立した1プロセスとして稼働している。

*4:さらに、今時のパソコンには描画専用のCPU(GPU)が用意されていて、メインのCPUはGPUに描画を依頼している。VRAMにアクセスするのはGPUの役割になっていたりする。

*5:カーネルが小さくシンプルになり、カーネル自体の開発効率は良くなる。

*6:よって、OSXmachカーネルは純粋なマイクロカーネルとは言えない状態になっている。