Linuxカーネル Docker関連 namespaceのメモ

Linux_Containers_logo_150

最近はやっとまともにDockerを使い始めたということもあり、基盤技術を追う必要性も強く感じてきました。Linuxカーネルのコンテナ技術周りのコードリーディングを再開しているのですが楽しいです。

環境

* CentOS 7.2 (kernel-3.10.0-327.4.5.el7.x86_64)
* Ubuntu 14.04 (3.13.0-77-generic)

読んでいるのはほとんどnamespaceとcgroup周りですけれど、この2つは違う機能なのでごっちゃにして覚えないようにしたいところです。このエントリーではnamespaceについて、利用方法やカーネルの実装なども含めてメモしておきます。

namespace (名前空間)

Linuxにおける namespace(名前空間) はプロセスに対して以下の6種類のリソースを分離するための機能として提供されています。

名前空間 定数 概要
IPC名前空間 CLONE_NEWIPC IPC(Inter-Process Communication:プロセス間通信)リソースであるSystem V IPCオブジェクト、POSIXメッセージキューを分離する。異なる名前空間の共有メモリやセマフォにアクセスできないようにする。
マウント名前空間 CLONE_NEWNS ファイルシステムツリーを分離する。異なる名前空間のファイルシステムにアクセスできないようにする。全てのユーザースペースはDockerイメージからマウントされる。chroot は使用しない。
ネットワーク名前空間 CLONE_NEWNET ネットワークデバイスやIPアドレス、ルーティングテーブルなどのネットワークインタフェースを分離する。異なる名前空間でそれぞれ仮想ネットワークを構築することができる。
PID名前空間 CLONE_NEWPID PID(プロセスID)空間を分離する。異なる名前空間で同じPIDのプロセスを作ることができる。
ユーザー名前空間 CLONE_NEWUSER UID/GIDを分離する。異なる名前空間で同じUIDのユーザーを作ることができ、root(UID=0)を名前空間外で操作の特権(root権限)を持たないようにセキュリティを設定する。
UTS名前空間 CLONE_NEWUTS uname() システムコールから返される2つのシステム識別子(nodename および domainname)を分離する。これにより各コンテナはそれぞれ独自のホスト名とNISドメイン名を持つことができる。

これらのリソースを分離することで、Dockerはコンテナ内外で別々の権限、リソース体系を構築することができるようになっています。ちなみにUTS名前空間のUTSという名前、元々は Unix Time-sharing System の略とのことで、現在は既にその意味は失われているようです。

/proc/{pid}/ns/ 以下で名前空間の一覧が確認できます。これらは特別なシンボリックリンクになっていて直接操作することはできません。

関連するシステムコール

名前空間をプロセス(プログラム)から利用する方法を整理しておきます。関連するシステムコールは以下の3つ。

clone(2) 新しいプロセスを生成する。 プロセス生成と同時に子プロセスを異なる名前空間に所属ことができる。新しく名前空間を作成するには clone(2) 呼び出し時に flags 引数で CLONE_NEW* のフラグを一つ以上指定する(上記の名前空間の表を参照)。例えばUTF名前空間を分離する際は CLONE_NEWUTS 、マウント名前空間を分離するには CLONE_NEWNS のように指定する。
unshare(2) 新しい名前空間を作成する。flags引数はclone(2)と同様だが新しいプロセスは作成しない。
setns(2) 既存の名前空間に呼び出したプロセスをアタッチする(参加させる)。clone(2)やunshare(2)のように新しい名前空間を作るわけではない。

clone(2)は名前空間を扱う以外にもスレッドの実装など様々な場所で使われていますね。他の2つのシステムコールは今回初めて知りました。

上記のシステムコール clone(2) を使ってPID名前空間の確認をしてみます。

通常、clone(2)のchild_stack引数は子プロセスのために用意したスタック(メモリ空間)の一番大きいアドレスを指定します(スタックはアドレスが小さい方向へと伸びるため)。

確認のため、PID名前空間を作成せずに(CLONE_NEWPIDフラグを指定せずに)実行してみます。

なるほど、PID名前空間はわかりやすいですね。次はネットワーク名前空間も加えて試してみます(※動作確認を簡単に行うためにsystem()関数を使っています)。

上述のようにclone(2)のflags引数は複数指定することができます。

新しく作成されるネットワーク名前空間はloだけ設定されるようです。ついでにUTS名前空間も確認。

ホスト名を変更しています。UTS名前空間もわかりやすいですね。

前述のCプログラムからのネットワーク名前空間の確認だけではわかりにくいので、実際に2つのネットワーク名前空間を作って相互に通信できるか試してみたいと思います。ip コマンドを使うと便利です。

仮想ネットワークを作ってネットワーク名前空間を跨いだ通信を確認できました。上記のように毎回 sudo ip netns exec netns1 ~ と打つのが面倒な場合、作成した名前空間で最初にシェルを立ち上げておけば、そのシェル上でいろいろ操作できるので便利です(sudo ip netns exec netns1 /bin/bash)。

次にDockerコンテナに対して名前空間の切り替えを確認したいと思います。事前にDockerでコンテナを作っておきます。

各名前空間(ユーザー名前空間以外)の番号が異なっていますので、異なる名前空間上でDockerコンテナが動作していることを確認できます。このコンテナに対してsetns(2)を使って名前空間の切り替えを行います。
* ns_exec.c

実行中のDockerコンテナ内のファイルシステムでBashが起動していることがわかります。

カーネルの実装

名前空間関連のカーネルの実装を一部紹介します。
* include/kernel/nsproxy.h

名前空間は nsproxy 構造体で管理されています。ここでは5つの名前空間が定義されています。ユーザー名前空間は他の名前空間と異なり独立していません。

sethostname(2) の実装を見てみます。
* kernel/sys.c

ns_capable関数でケーパビリティ(capability)のチェック、ここではCAP_SYS_ADMIN(root)権限で実行されているか確認しています。引数のcurrentは現在のプロセスのtask_struct構造体で、前述のnsproxy構造体をメンバに持っています。ここではUTS名前空間内のユーザー名前空間を参照してケーパビリティのチェックしています。また、utsname()関数でホスト名を管理するnew_utsname構造体へのポインタを返しています。task_struct構造体というのはLinuxのプロセスを表現する大きな構造体で、プロセスの実行状態やメモリ、ソケット、ファイル情報、他プロセス(親や子)との関係など多くの情報を管理しています。定義は include/linux/sched.h にありますが長いためここでは割愛します。

* include/uapi/linux/utsname.h, include/linux/utsname.h

このように名前空間の利用を前提とした処理になっています。UTS名前空間はシンプルなため比較的読みやすいですが、他の名前空間の実装も確認したら後で追記します。コンテナ技術の支えるもうひとつの重要機能である cgroup についても整理・メモしておこうと思います。

ここ1, 2年くらいは技術の現場から離れていたこともあり、Linuxカーネル読書会・勉強会などにも参加していなかったので効率良くコードリーディングできていません、。やはりもうしばらくリハビリ期間が必要です。

参考

あわせて読む:

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です