linux:kernel:namespace:more_on_pid_namespaces

文書の過去の版を表示しています。


PID 名前空間について更にもう少し

この記事では,PID 名前空間の先週の議論を続ける (そして名前空間に関する進行中のシリーズを続ける).PID 名前空間の利用法の一つは,パッケージ化したプロセス (コンテナ) を実装することである.これは独立した Linux システムのように振る舞う.伝統的なシステムのキーとなる部分は init プロセスである.そしてこれは PID 名前空間を使ったコンテナでも同様である.なので,init プロセスの特別な役割を調べ,伝統的な init プロセスとは違う点について 1 つ,2 つ述べる.加えて,PID 名前空間に適用する時の名前空間 API の他の詳細を調べる.

PID 名前空間内で最初に作られるプロセスは,名前空間内でプロセス ID 1 を取得する.このプロセスは伝統的な Linux システムの init プロセスと同様の役割を持つ.特に,init プロセスは PID 名前空間全体 (例えば多分,開始する名前空間の標準部分であるであろう他のプロセス) に必要な初期化を行う能力があり,他とは独立した名前空間内のプロセスの親になる.

PID 名前空間の操作を説明するために,特定の目的で作られたいくつかのサンプルプログラムを利用する.このプログラムの最初は ns_child_exec.c で,以下のようなコマンドライン文法である.

   ns_child_exec [options] command [arguments]

ns_child_exec プログラムは,子プロセスを作成するのに clone() システムコールを使用する.そして,子供は与えられた command を指定した arguments を引数に実行する.options の主目的は clone() の呼び出しで作成される新しい名前空間を指定するためのものである.例えば,-p オプションでは,以下のように子供が新しい PID 名前空間内に作られる.

    $ su                  # Need privilege to create a PID namespace
    Password:
    # ./ns_child_exec -p sh -c 'echo $$'
    1

コマンドで子プロセスが新しい PID 名前空間内に作られ,シェルの echo コマンドが実行され,シェルの PID が表示されている.1 という PID を持ち,シェルは実行中である間(少しの間だけ)存在した PID 名前空間に対する init であった.

次のプログラムは simple_init.c であり,これは PID 名前空間の init プロセスとして実行されるプログラムである.

simple_init プログラムは init の主要な 2 つの機能を実行する.1 つ目は『システムの初期化』である.ほとんどの init システムはもっと複雑なプログラムであり,システムの初期化に対してテーブル駆動のアプローチを取る.ここでの (もっとシンプルな) simple_init プログラムは,シンプルなシェルであり,ユーザが手動で名前空間の初期化に必要などのようなコマンドでも実行が可能なものである.このアプローチは名前空間内での実験を実行するためのシェルコマンドを自由に実行することも可能となる.simple_init が実行する 2 つ目の機能は,waitpid() を使って子供が終了するステータスを取得することである.

それゆえ,例えば,PID 名前空間内で実行する init プロセスを起動するために,ns_child_exec プログラムを simple_init と連携させて使うことが可能である.

    # ./ns_child_exec -p ./simple_init
    init$

init$ プロンプトは sinple_init プログラムがシェルコマンドを読み取り実行する準備ができたことをしめします.

さて,orphan.c という他の小さなプログラムを,これまで紹介した 2 つのプログラムと連携させて使ってみよう.これは,PID 名前空間内で親がいなくなるプロセスが,システムワイドの init プロセスでなく,PID 名前空間内の init プロセスによって拾われる事をデモするものである.

orphan プログラムは子プロセスを作成するのに fork() を実行する.そして親プロセスは exit し,子プロセスは実行しつづける.親プロセスが終了したとき,子プロセスは孤児プロセスとなる.子プロセスは孤児プロセスになるまで (getppid() が 1 を返すまで) ループしつづけ,孤児プロセスになると終了する.親子は 2 つのプロセスが終了したこと,子プロセスが孤児プロセスになったことがわかるようにメッセージを表示する.

どのように simple_init プログラムが孤児プロセスを刈り取るのかを見るために,プログラムの -v オプションを使う.これは自身が作成した子プロセスに関する verbose メッセージを生成し,自身が刈り取った終了した子プロセスのステータスに関する verbose メッセージを生成するものである.

    # ./ns_child_exec -p ./simple_init -v
            init: my PID is 1
    init$ ./orphan
            init: created child 2
    Parent (PID=2) created child with PID 3
    Parent (PID=2; PPID=1) terminating
            init: SIGCHLD handler: PID 2 terminated
    init$                   # simple_init prompt interleaved with output from child
    Child  (PID=3) now an orphan (parent PID=1)
    Child  (PID=3) terminating
            init: SIGCHLD handler: PID 3 terminated

上の出力で,init: と冒頭に付いたインデントしたメッセージは simple_init プログラムの verbose モードによる出力である.(init$ プロンプト以外の) 他のメッセージは orphan プログラムが生成したものである.出力から,子プロセス (PID 3) が,親 (PID 2) が終了した時点で孤児プロセスになるのがわかる.この時点で,子プロセスは PID 名前空間の init プロセス (PID 1) の子プロセスになっている.init プロセスは,子プロセスが終了した時点で子プロセスを刈り取っている.

伝統的な Linux の init は,シグナルに関して特別な扱いを行う.init に届けられることのできるシグナルは,(その) プロセス (訳注: init のこと?) がシグナルハンドラを確立したシグナルだけに限られる.他の全てのシグナルは無視される.これにより,その存在がシステムの安定した動きに極めて重要である init プロセスが,スーパーユーザによってでも,誤って kill されるのを防ぐ.

PID 名前空間は,名前空間個別の init プロセスに対していくつか似た振る舞いを実装する.名前空間内の他のプロセスは (特権プロセスであっても) init プロセスがハンドラを確立したシグナルのみを送ることができる.このことは,名前空間のメンバーが名前空間内で不可欠なプロセスを不注意に kill することを防ぐ.しかし,(伝統的な init プロセスに関しては) カーネルは通常の状況 (ハードウェアの例外,端末の生成する SIGTTOU のようなシグナル,タイマの Expire 等) 全てで,依然として PID 名前空間の init プロセスに対してシグナルを生成することが可能であることに注意が必要である.

シグナルは (通常のパーミッションのチェックを受け),祖先の PID 名前空間のプロセスから PID 名前空間の init プロセスにも送られる.ここでも,そのシグナルのためのハンドラが設定されたシグナルのみ送る事ができる.ここで 2 つだけ例外があり,SIGKILL と SIGSTOP である.祖先の PID 名前空間内のプロセスがこれらの 2 つのシグナルを init に送った時,シグナルは強制的に配送される (そして受け取られないかもしれない).SIGSTOP シグナルは init プロセスを停止させ,SIGKILL は終了させる.init プロセスは PID 名前空間を機能させるのに必要であるから,もし init プロセスが SIGKILL によって (もしくは他の理由で) 終了した場合,カーネルは SIGKILL シグナルを送り名前空間内の他の全てのプロセスを終了させる.

通常は PID 名前空間も,空間内の init プロセスが終了する場合には終了する.しかし,通常ではあまりないケースがある.名前空間内のプロセスの一つの /proc/PID/ns/pid ファイルが bind mount され,open し続けている間は,名前空間は終了しない.しかし,名前空間内では新しいプロセスは (setns() や fork() では) 作成できない.init プロセスが存在しないことが fork() 呼び出しの間検知される.これは ENOMEM エラーで失敗する (PID を割り当てる事ができないことを示す伝統的なエラー).言い換えると,PID 名前空間は存在し続けるが,使う事ができないということである.

このシリーズの先の記事で,PID 名前空間のための /proc ファイルシステム (procfs) を伝統的な /proc マウントポイント以外の他の場所にマウントした.これは,root PID 名前空間内で見えるプロセスを見るために ps コマンドを使うのと同時に,新しい PID 名前空間それぞれと一致する /proc/PID ディレクトリのコンテンツを見るためにシェルコマンドを使えるようにするためである.

しかし ps のようなツールは,必要な情報を取得するために /proc にマウントされた procfs のコンテンツに頼っている.それゆえ,もし PID 名前空間内で正しく ps コマンドを使いたい場合,その名前空間用の procfs をマウントする必要がある.simple_init プログラムはシェルコマンドの実行が可能なので,この処理をコマンドラインから以下のように実行可能である.

    # ./ns_child_exec -p -m ./simple_init
    init$ mount -t proc proc /proc
    init$ ps a
      PID TTY      STAT   TIME COMMAND
        1 pts/8    S      0:00 ./simple_init
        3 pts/8    R+     0:00 ps a

ps コマンドは /proc 経由でアクセス可能な全てのプロセスをリストアップしている.この場合,2 つのプロセスのみ見る事ができる.このことからすると,この名前空間では 2 つのプロセスのみが実行されている.

上記のように ns_child_exec コマンドを実行したとき,プログラムの -m オプションを使っている.これは子プロセス (つまり simple_init で実行するプロセス) を,分離されたマウント名前空間を作成し,その中に置く.結果として,mount コマンドは,名前空間外のプロセスから見える /proc には影響を与えない.

このシリーズの Part.2 の記事で,名前空間 API の一部をなす 2 つのシステムコール unshare() と setns() について説明した.Linux 3.8 以降,これらのシステムコールは PID 名前空間で使用可能になった.しかし,これらのシステムコールは,これらの (訳注: この?) 名前空間で使用する場合は特異な点がいくつかある.

unshare() の呼び出しで CLONE_NEWPID フラグを指定すると新しい PID 名前空間が作られる.しかし,この新しい名前空間内に呼び出し元は入らない.呼び出し元が作成した子プロセスは全て,新しい名前空間内に入る.このような最初の子プロセスは名前空間の init プロセスになるだろう.

setns() システムコールも現在 PID 名前空間をサポートしている.

    setns(fd, 0);   /* 2 つ目の引数は,fd が PID 名前空間を参照するかをチェックするために
                       CLONE_NEWPID を指定する事もできる */
  • linux/kernel/namespace/more_on_pid_namespaces.1377353410.txt.gz
  • 最終更新: 2013/08/24 14:10
  • by tenforward