のようなツールの出現で Docker 、 Linuxコンテナ 、その他、Linuxプロセスを独自の小さなシステム環境に分離することが非常に簡単になりました。これにより、1台の実際のLinuxマシンですべてのアプリケーションを実行し、仮想マシンを使用することなく、2台のアプリケーションが相互に干渉しないようにすることができます。これらのツールは、 PaaS プロバイダー。しかし、内部で正確に何が起こるのでしょうか?
これらのツールは、Linuxカーネルの多くの機能とコンポーネントに依存しています。これらの機能のいくつかはごく最近導入されましたが、他の機能ではカーネル自体にパッチを適用する必要があります。しかし、Linux名前空間を使用する重要なコンポーネントの1つは、バージョン2.6.24が2008年にリリースされて以来、Linuxの機能となっています。
chroot
に精通している人Linux名前空間で何ができるか、そして名前空間を一般的に使用する方法についての基本的な考え方はすでにあります。 chroot
と同じようにプロセスが任意のディレクトリをシステムのルートとして(残りのプロセスとは無関係に)認識できるようにします。Linux名前空間を使用すると、オペレーティングシステムの他の側面も個別に変更できます。これには、プロセスツリー、ネットワークインターフェイス、マウントポイント、プロセス間通信リソースなどが含まれます。
シングルユーザーコンピューターでは、単一のシステム環境で問題ない場合があります。ただし、複数のサービスを実行するサーバーでは、サービスを可能な限り互いに分離することがセキュリティと安定性にとって不可欠です。複数のサービスを実行しているサーバーを想像してみてください。そのうちの1つが侵入者によって侵害されています。このような場合、侵入者はそのサービスを悪用して他のサービスに侵入する可能性があり、サーバー全体を危険にさらす可能性さえあります。名前空間の分離は、このリスクを排除するための安全な環境を提供できます。
たとえば、名前空間を使用すると、サーバー上で任意または不明なプログラムを安全に実行できます。最近、プログラミングコンテストや「ハッカソン」プラットフォームの数が増えています。 HackerRank 、 TopCoder 、 コードフォース 、 などなど。それらの多くは、自動パイプラインを利用して、出場者によって提出されたプログラムを実行および検証します。出場者のプログラムの本質を事前に知ることは不可能な場合が多く、悪意のある要素が含まれている場合もあります。システムの他の部分から完全に分離された名前空間でこれらのプログラムを実行することにより、マシンの他の部分を危険にさらすことなく、ソフトウェアをテストおよび検証できます。同様に、次のようなオンライン継続的インテグレーションサービス Drone.io 、コードリポジトリを自動的にフェッチし、独自のサーバーでテストスクリプトを実行します。繰り返しますが、名前空間の分離は、これらのサービスを安全に提供することを可能にするものです。
Dockerなどの名前空間ツールを使用すると、プロセスによるシステムリソースの使用をより適切に制御できるため、このようなツールはPaaSプロバイダーでの使用に非常に人気があります。のようなサービス Heroku そして Google App Engine このようなツールを使用して、同じ実際のハードウェア上で複数のWebサーバーアプリケーションを分離して実行します。これらのツールを使用すると、アプリケーションの1つがシステムリソースを使いすぎたり、同じマシン上に展開されている他のサービスと干渉したり、競合したりすることを心配せずに、各アプリケーション(多数の異なるユーザーのいずれかによって展開された可能性があります)を実行できます。このようなプロセスの分離により、分離された環境ごとに依存関係ソフトウェア(およびバージョン)のまったく異なるスタックを持つことさえ可能です!
Dockerなどのツールを使用したことがある場合は、これらのツールが小さな「コンテナ」内のプロセスを分離できることをすでにご存知でしょう。 Dockerコンテナーでプロセスを実行することは、仮想マシンでプロセスを実行することに似ています。これらのコンテナーのみが仮想マシンよりも大幅に軽量です。仮想マシンは通常、オペレーティングシステム上でハードウェア層をエミュレートし、その上で別のオペレーティングシステムを実行します。これにより、実際のオペレーティングシステムから完全に分離して、仮想マシン内でプロセスを実行できます。しかし、仮想マシンは重いです!一方、Dockerコンテナは、名前空間など、実際のオペレーティングシステムのいくつかの重要な機能を使用し、同様のレベルの分離を保証しますが、ハードウェアをエミュレートしたり、同じマシンでさらに別のオペレーティングシステムを実行したりすることはありません。これにより、非常に軽量になります。
歴史的に、Linuxカーネルは単一のプロセスツリーを維持してきました。ツリーには、親子階層で現在実行されているすべてのプロセスへの参照が含まれています。プロセスは、十分な特権を持ち、特定の条件を満たす場合、トレーサーをアタッチすることで別のプロセスを検査できます。また、プロセスを強制終了することもできます。
Linux名前空間の導入により、複数の「ネストされた」プロセスツリーを持つことが可能になりました。各プロセスツリーは、完全に分離されたプロセスのセットを持つことができます。これにより、1つのプロセスツリーに属するプロセスが、他の兄弟または親プロセスツリー内のプロセスを検査または強制終了できず、実際にはその存在を知ることさえできなくなります。
Linuxを搭載したコンピューターが起動するたびに、プロセスID(PID)1の1つのプロセスから開始します。このプロセスはプロセスツリーのルートであり、適切なメンテナンス作業を実行して開始することにより、システムの残りの部分を開始します正しいデーモン/サービス。他のすべてのプロセスは、ツリー内のこのプロセスの下から始まります。 PID名前空間を使用すると、独自のPID1プロセスを使用して新しいツリーをスピンオフできます。これを行うプロセスは、元のツリーの親名前空間に残りますが、子を独自のプロセスツリーのルートにします。
PID名前空間の分離では、子名前空間のプロセスは親プロセスの存在を知る方法がありません。ただし、親名前空間のプロセスには、親名前空間の他のプロセスであるかのように、子名前空間のプロセスの完全なビューがあります。
子名前空間のネストされたセットを作成することができます。1つのプロセスが新しいPID名前空間で子プロセスを開始し、その子プロセスが新しいPID名前空間でさらに別のプロセスを生成します。
化粧品業界の大きさ
PID名前空間の導入により、単一のプロセスに、該当する名前空間ごとに1つずつ、複数のPIDを関連付けることができるようになりました。 Linuxのソースコードでは、 見える 以前は単一のPIDのみを追跡していたpid
という名前の構造体が、upid
という名前の構造体を使用して複数のPIDを追跡するようになりました。
struct upid { int nr; // the PID value struct pid_namespace *ns; // namespace where this PID is relevant // ... }; struct pid { // ... int level; // number of upids struct upid numbers[0]; // array of upids };
新しいPID名前空間を作成するには、 clone()
特別なフラグを使用したシステムコールCLONE_NEWPID
。 (Cは、このシステムコールを公開するラッパーを提供し、他の多くの一般的な言語も同様です。)一方、以下で説明する他の名前空間は、unshare()
を使用して作成することもできます。システムコールの場合、PID名前空間は、clone()
を使用して新しいプロセスが生成されたときにのみ作成できます。一度clone()
このフラグを指定して呼び出されると、新しいプロセスは、新しいプロセスツリーの下の新しいPID名前空間ですぐに開始されます。これは、単純なCプログラムで実証できます。
#define _GNU_SOURCE #include #include #include #include #include static char child_stack[1048576]; static int child_fn() { printf('PID: %ld
', (long)getpid()); return 0; } int main() pid_t child_pid = clone(child_fn, child_stack+1048576, CLONE_NEWPID
このプログラムをコンパイルしてroot権限で実行すると、次のような出力が表示されます。
clone() = 5304 PID: 1
child_fn
内から印刷されるPIDは、1
になります。
上記のこの名前空間チュートリアルコードは、一部の言語では「Hello、world」よりも長くはありませんが、舞台裏で多くのことが行われています。 clone()
関数は、ご想像のとおり、現在のプロセスを複製して新しいプロセスを作成し、child_fn()
の開始時に実行を開始しました。関数。ただし、その際、元のプロセスツリーから新しいプロセスを切り離し、新しいプロセス用に別のプロセスツリーを作成しました。
static int child_fn()
を置き換えてみてください分離されたプロセスの観点から親PIDを出力するには、次のように機能します。
static int child_fn() { printf('Parent PID: %ld
', (long)getppid()); return 0; }
今回プログラムを実行すると、次の出力が得られます。
clone() = 11449 Parent PID: 0
分離されたプロセスの観点から見た親PIDが0であり、親がないことを示していることに注意してください。同じプログラムをもう一度実行してみてください。ただし、今回はCLONE_NEWPID
を削除します。 clone()
内からのフラグ関数呼び出し:
pid_t child_pid = clone(child_fn, child_stack+1048576, SIGCHLD, NULL);
今回は、親PIDが0ではなくなったことに気付くでしょう。
clone() = 11561 Parent PID: 11560
ただし、これはチュートリアルの最初のステップにすぎません。これらのプロセスは、他の共通または共有リソースに無制限にアクセスできます。たとえば、ネットワークインターフェイス:上記で作成した子プロセスがポート80でリッスンする場合、システム上の他のすべてのプロセスがそれをリッスンできなくなります。
ここで、ネットワーク名前空間が役立ちます。ネットワーク名前空間を使用すると、これらの各プロセスでまったく異なるネットワークインターフェイスのセットを確認できます。ループバックインターフェイスでさえ、ネットワーク名前空間ごとに異なります。
プロセスを独自のネットワーク名前空間に分離するには、clone()
に別のフラグを導入する必要があります。関数呼び出し:CLONE_NEWNET
;
#define _GNU_SOURCE #include #include #include #include #include static char child_stack[1048576]; static int child_fn() { printf('New `net` Namespace:
'); system('ip link'); printf('
'); return 0; } int main() SIGCHLD, NULL); waitpid(child_pid, NULL, 0); return 0;
出力:
Original `net` Namespace: 1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: enp4s0: mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000 link/ether 00:24:8c:a1:ac:e7 brd ff:ff:ff:ff:ff:ff New `net` Namespace: 1: lo: mtu 65536 qdisc noop state DOWN mode DEFAULT group default link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
何が起きてる?物理イーサネットデバイスenp4s0
この名前空間から実行される「ip」ツールで示されるように、グローバルネットワーク名前空間に属します。ただし、物理インターフェイスは新しいネットワーク名前空間では使用できません。さらに、ループバックデバイスは元のネットワーク名前空間ではアクティブですが、子ネットワーク名前空間では「ダウン」しています。
子名前空間で使用可能なネットワークインターフェイスを提供するには、複数の名前空間にまたがる追加の「仮想」ネットワークインターフェイスを設定する必要があります。それが完了すると、イーサネットブリッジを作成し、名前空間間でパケットをルーティングすることもできます。最後に、すべてを機能させるには、「ルーティングプロセス」をグローバルネットワーク名前空間で実行して、物理インターフェイスからトラフィックを受信し、適切な仮想インターフェイスを介して正しい子ネットワーク名前空間にルーティングする必要があります。たぶん、Dockerのような、このような手間のかかる作業をすべて行うツールがなぜそれほど人気があるのかがわかるでしょう!
これを手動で行うには、親名前空間から1つのコマンドを実行することにより、親名前空間と子名前空間の間に仮想イーサネット接続のペアを作成できます。
ip link add name veth0 type veth peer name veth1 netns
ここでは、親が監視する子名前空間内のプロセスのプロセスIDに置き換える必要があります。このコマンドを実行すると、これら2つの名前空間間にパイプのような接続が確立されます。親名前空間はveth0
を保持しますデバイス、およびveth1
を渡します子名前空間へのデバイス。 2つの実際のノード間の実際のイーサネット接続から予想されるように、一方の端に入るものはすべて、もう一方の端から出ます。したがって、この仮想イーサネット接続の両側にIPアドレスを割り当てる必要があります。
Linuxは、システムのすべてのマウントポイントのデータ構造も維持します。これには、マウントされているディスクパーティション、マウントされている場所、読み取り専用かどうかなどの情報が含まれます。 Linux名前空間を使用すると、このデータ構造のクローンを作成できるため、異なる名前空間のプロセスは、相互に影響を与えることなくマウントポイントを変更できます。
個別のマウント名前空間を作成すると、chroot()
を実行するのと同様の効果があります。 chroot()
は良好ですが、完全な分離は提供されず、その影響はルートマウントポイントのみに制限されます。個別のマウント名前空間を作成すると、これらの分離された各プロセスで、システム全体のマウントポイント構造を元のプロセスとはまったく異なるビューにすることができます。これにより、分離されたプロセスごとに異なるルートを設定したり、それらのプロセスに固有の他のマウントポイントを設定したりできます。このチュートリアルに従って注意して使用すると、基盤となるシステムに関する情報を公開することを回避できます。
clone()
これを達成するために必要なフラグはCLONE_NEWNS
です:
clone(child_fn, child_stack+1048576, CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS | SIGCHLD, NULL)
最初、子プロセスは、親プロセスとまったく同じマウントポイントを認識します。ただし、新しいマウント名前空間の下にあるため、子プロセスは必要なエンドポイントをマウントまたはアンマウントできます。この変更は、親の名前空間にも、システム全体の他のマウント名前空間にも影響しません。たとえば、親プロセスのルートに特定のディスクパーティションがマウントされている場合、分離されたプロセスには、最初にルートにマウントされたまったく同じディスクパーティションが表示されます。ただし、マウント名前空間を分離することの利点は、分離されたプロセスがルートパーティションを別のパーティションに変更しようとしたときに明らかになります。これは、変更が分離されたマウント名前空間にのみ影響するためです。
興味深いことに、これは実際には、ターゲットの子プロセスをCLONE_NEWNS
で直接スポーンすることをお勧めしません。国旗。より良いアプローチは、CLONE_NEWNS
で特別な「init」プロセスを開始することです。フラグを立て、その「init」プロセスに「/」、「/ proc」、「/ dev」、またはその他のマウントポイントを必要に応じて変更させてから、ターゲットプロセスを開始します。これについては、この名前空間チュートリアルの終わり近くでもう少し詳しく説明します。
これらのプロセスを分離できる他の名前空間、つまりユーザー、IPC、およびUTSがあります。ユーザー名前空間を使用すると、プロセスは、名前空間外のプロセスへのアクセスを許可せずに、名前空間内でroot権限を持つことができます。プロセスをIPC名前空間で分離すると、System VIPCやPOSIXメッセージなどの独自のプロセス間通信リソースが割り当てられます。 UTS名前空間は、システムの2つの特定の識別子を分離します。nodename
およびdomainname
。
UTS名前空間がどのように分離されるかを示す簡単な例を以下に示します。
#define _GNU_SOURCE #include #include #include #include #include #include static char child_stack[1048576]; static void print_nodename() { struct utsname utsname; uname(&utsname); printf('%s
', utsname.nodename); } static int child_fn() { printf('New UTS namespace nodename: '); print_nodename(); printf('Changing nodename inside new UTS namespace
'); sethostname('GLaDOS', 6); printf('New UTS namespace nodename: '); print_nodename(); return 0; } int main() SIGCHLD, NULL); sleep(1); printf('Original UTS namespace nodename: '); print_nodename(); waitpid(child_pid, NULL, 0); return 0;
このプログラムは、次の出力を生成します。
Original UTS namespace nodename: XT New UTS namespace nodename: XT Changing nodename inside new UTS namespace New UTS namespace nodename: GLaDOS Original UTS namespace nodename: XT
ここで、child_fn()
nodename
を印刷し、それを別のものに変更して、再度印刷します。当然、変更は新しいUTS名前空間内でのみ発生します。
すべての名前空間が提供および分離するものの詳細については、チュートリアルを参照してください。 ここに
多くの場合、親と子の名前空間の間で何らかの通信を確立する必要があります。これは、隔離された環境内で構成作業を行うためのものである場合もあれば、単に外部からその環境の状態を覗き見する機能を保持するためのものである場合もあります。これを行う1つの方法は、SSHデーモンをその環境内で実行し続けることです。各ネットワーク名前空間内に個別のSSHデーモンを含めることができます。ただし、複数のSSHデーモンを実行すると、メモリなどの貴重なリソースが大量に使用されます。ここで、特別な「init」プロセスを使用することをお勧めします。
「init」プロセスは、親名前空間と子名前空間の間に通信チャネルを確立できます。このチャネルは、UNIXソケットに基づくことも、TCPを使用することもできます。 2つの異なるマウント名前空間にまたがるUNIXソケットを作成するには、最初に子プロセスを作成し、次にUNIXソケットを作成してから、子を別のマウント名前空間に分離する必要があります。しかし、最初にプロセスを作成し、後でそれを分離するにはどうすればよいでしょうか。 Linuxは提供します unshare()
。この特別なシステムコールにより、プロセスは、最初に親に子を分離させる代わりに、元の名前空間からそれ自体を分離することができます。たとえば、次のコードは、ネットワーク名前空間のセクションで前述したコードとまったく同じ効果があります。
サーバー側のレンダリングとクライアント側
#define _GNU_SOURCE #include #include #include #include #include static char child_stack[1048576]; static int child_fn() { // calling unshare() from inside the init process lets you create a new namespace after a new process has been spawned unshare(CLONE_NEWNET); printf('New `net` Namespace:
'); system('ip link'); printf('
'); return 0; } int main() SIGCHLD, NULL); waitpid(child_pid, NULL, 0); return 0;
また、「init」プロセスはあなたが考案したものなので、最初に必要なすべての作業を実行させてから、ターゲットの子を実行する前にシステムの他の部分から分離することができます。
このチュートリアルは、Linuxで名前空間を使用する方法の概要にすぎません。それはあなたにどのように基本的な考えを与えるはずです Linux開発者 次のようなツールのアーキテクチャの不可欠な部分であるシステム分離の実装を開始する可能性があります Docker またはLinuxコンテナ。ほとんどの場合、すでによく知られていてテストされているこれらの既存のツールの1つを使用するのが最善です。ただし、場合によっては、独自のカスタマイズされたプロセス分離メカニズムを使用することが理にかなっていることがあります。その場合、この名前空間チュートリアルは非常に役立ちます。
この記事で説明したよりも多くのことが内部で行われています。安全性と分離を強化するために、ターゲットプロセスを制限する方法は他にもあります。しかし、うまくいけば、これは、Linuxでの名前空間の分離が実際にどのように機能するかについてもっと知りたいと思っている人にとって有用な出発点として役立つことができます。