MINCSコードリーディング

コンテナとは何か?

コンテナの基礎知識について非常によくまとめられている。おすすめ。

  • Namespace

  • cgroup

  • Capability

  • chroot/pivot_root

  • OverlayFS

MINCS

mhiramat/mincsarrow-up-right

シェルスクリプトで実装されている軽量のコンテナランタイム。Docker などといった高度に抽象化されているコンテナランタイムを比較すると、コンテナを構成する基礎的な要素をシェルスクリプトで直接的に操作している。コンテナを構成する要素をどのように作成しているのか分かりやすく、学習用のコンテナランタイムとして優れている。

今回は一番最初のステーブルブランチ(minc-0.1arrow-up-right)のソースコードを読んでいくことにする。master ブランチと比較すると機能は最小限のみになっていて、理解しやすいと判断。

予備知識

ソースコードを読むにあたってシェルスクリプトに関するいくつかの予備知識を整理しておく。

mount --bind

バインドマウントを実施する。

https://linuxjm.osdn.jp/html/util-linux/man8/mount.8.htmlarrow-up-right

以下のような動きになる。バインドマウントは、表面的な動作はシンボリックリンクと同じように見えるが違う。後述。

  • シンボリックリンクとバインドマウントの違い

What is the difference between ln -s and mount --bind?arrow-up-right

上記の記事では(少なくとも)以下の点で異なるとのこと

  • chroot を用いた場合は、シンボリックリンクが chroot の外側にある場合はシンボリックリンクが無効になる。バインドマウントの場合は引き続きアクセスすることができる。

  • 既存のディレクトリやファイルにディレクトまたはファイルをバインドマウントし、元のコンテンツを保護することができる。(別の場所からバインドマウントされていない限り、元のコンテンツにアクセスできなくなる)。シンボリックリンクでは元のコンテンツを移動または削除する必要がある。

  • シンボリックリンクはファイルだが、バインドマウントはカーネルがメモリ上に保持するレコードである。(再起動したときなどにそのまま保持されるかどうかなどの違いがある)

※全然関係ないけど、ここで表示されている 合計 xx は割り当てられているブロック数

$0

実行したシェルスクリプト名を取得できる。

$@

実行時のパラメータをそれぞれクオートして渡す。 $* の場合は全体として一つのパラメータとして渡す。

mount --make-rprivate /

以下のコマンドはプライベートとして再マウントするコマンド。

マウントの伝播を private に設定

systemd は mount propagation を shared にマークしてしまうので、せっかく新しい Mount Namespace を作成しても、Mount Namespace 内のマウントが他の Namespace に伝播してしまいます。これではコンテナにならないので、まずはこれを private にしています。

デフォルトのカーネルのマウントのモードだと、共有モード(MS_SHARED)でマウントされる。完全に独立したマウント名前空間を必要としている場合はマウントポイントをプライベート(MS_PRIVATE)にする必要があるとのこと。

mountpoint

マウントポイントかどうか調べるコマンド

/proc/self/mountinfo に存在するかどうかをチェックしている。マウントポイントの場合は返り値が 0 でそうでない場合は非 0 を返す。

http://man7.org/linux/man-pages/man1/mountpoint.1.htmlarrow-up-right

mount -t overlay

overlayfsはunion filesystemの実装の1つで,ディレクトリを重ね合わせて1つのディレクトリツリーが構成できます。

overlay_constructs.jpg https://docs.docker.com/storage/storagedriver/images/overlay_constructs.jpgarrow-up-right

オプション

概要

lowerdir

重ねあわせの下層側のディレクトリ。複数指定可能。

upperdir

重ねあわせの上層側のディレクトリ。書き込み可能なローカルのファイルシステムの必要がある。

workdir

overlayfs が使う作業ディレクトリ。upperdir と同じファイルシステムの名前空間内に存在する必要がある。

実践! OverlayFSarrow-up-right

キュメントには色々書いているが、Overlayfsの基本的な考え方は以下の通り

  1. Readonlyな下の層と、Writableな上の層を重ね合わせ1つのファイルシステムに見せる

  2. このとき、ディレクトリは重ね合わされ、ファイルは上から見ていく

    • コンフリクトしていたらファイルだと上が優先され、ディレクトリだとマージされる

  3. 下(readonly)にしかないファイルを編集すると上にコピーが作成される

image https://www.slideshare.net/akachochin/overlayfsa-brief-report-of-overlayfs-source-code-readingarrow-up-right

pivot_root

root ファイルシステムを変更するコマンド

pivot_root はカレントプロセスの root ファイルシステムを put_old ディレクトリに移動し、 new_root を新しい root ファイルシステムにする。 pivot_root(8) は pivot_root(2) を呼び出しているだけ

https://linuxjm.osdn.jp/html/util-linux/man8/pivot_root.8.htmlarrow-up-right https://linuxjm.osdn.jp/html/LDP_man-pages/man2/pivot_root.2.htmlarrow-up-right

Linuxのkernel 2.4系にはpivot_rootという仕組みがあり, これによってrootファイルシステムそのものを取り替えてしまうことができます. これは,rootディレクトリを変更するという点ではchrootにも似ていますが, chrootとは違って一部のプロセスだけでなく,OS全体のrootファイルシステムが 根本的に変更されます.

pivot_rootを実行すると,以前のrootファイルシステムはpivot_rootコマンドの 第2引数で指定したディレクトリにマウントされた状態になります.

CD-ROMだけで動作するオリジナルLinuxを作ろうarrow-up-right

動作としては pivot_rootchroot に大きな違いはないが、適用するコンテキストが異なる。(OS 全体 or プロセス)

chroot

ルートディレクトリを変更してコマンドを実行するコマンド

コマンドや対話的シェルを特別なルートディレクトリで実行する。ルートディレクトリを NEWROOT に設定してコマンドを実行する。 通常、ファイル名はディレクトリ構造のルート (root=根)、つまり `/' を起点として調べられる。 chroot はこのルートをディレクトリ directory に変更して command を実行する (directory は存在するディレクトリでなければならない)。

https://linuxjm.osdn.jp/html/gnumaniak/man1/chroot.1.htmlarrow-up-right

ディレクトリ構成

全体的な処理の流れ

  1. unshareによる、名前空間の(分岐という意味での)フォーク

  2. mount/umountによる、新しいrootfsに対するoverlayや様々なマウントポイントの再整備

  3. chroot(or pivot_root)による新しいrootfsへの移行と対象コマンドの実行

ソースコードの追い方

シェルスクリプトを上から読んでも問題ないが、一番初期のstableバージョン(minc-0.1)でも結構本格的な実装になっている。処理の動き的にはデバッグログを中心に見たほうがわかりやすい気がした。なので ./minc --debug /bin/bash として出力される内容から抜粋して紹介する。

実行時の主要なコマンド

1.unshare で名前空間を分離

カーネルの名前空間は全部で 6 つあった。minc の実装ではデフォルトだとネットワーク名前空間は分離されない。オプションで付与することは可能。

名前空間の名前

隔離されるリソース

unshare で対応するオプション

マウント名前空間

マウントの集合、操作

-m --mount

UTS名前空間

ホスト名、ドメイン名

-u --uts

PID名前空間

プロセスID

-p -pid

IPC名前空間

SysV IPCオブジェクト、POSIXメッセージキュー

-i --ipc

ユーザ名前空間

UID、GID

-u --uts

ネットワーク名前空間

ネットワークデバイス、アドレスポート、ルーティングテーブルなど

-U --user

unshare コマンドによって新しい名前空間上で子プロセスが実行される。が名前空間の実態は親の名前空間を指している。PID名前空間を新しく作成すると、プロセスを新しく作成しないと新しい名前空間に入らないよう。

2.プライベートマウント

上記の通り。コンテナホストとファイルシステムを分離する。

3.overlayfs でマウント

ディレクトリ名は minc の中で mktemp -d /tmp/minc$$-XXXXXX として作成している。

ディレクトリ レイヤー

デバッグ実行で対応するPath

upperdir

/tmp/minc12443-1ur2tR/storage

lowerdir

/

workdir

/tmp/minc12443-1ur2tR/work

overlayfs

/tmp/minc12443-1ur2tR/root

4.周辺ディレクトリ(/dev, sysfs, procfs)の再構築

コンテナホストと分離するために周辺ディレクトリ(/dev, sysfs, procfs)をバインドマウントする

5.2 回に分けて root ファイルシステムを変更

まずコンテナホストのルートファイルシステムを .orig としてマウントポイントを付け替える

この状態だと元のファイルシステムは .orig/ にマウントされている。

この段階で /.orig を umount したいがそれは失敗する。overlayfs の lowerdir を元の / にしているため。

umount できるマウントポイントは umount してしまう。残っているコンテナホストのマウントポイントは再度 pivot_root する。

ここまで来ると、/mnt ではコンテナホストの proc や sys といったマウントポイントは外れていて overlayfs として / が見えるだけになっている。また /dev や /dev/null は以下のように overlayfs 上にバインドマウントまたは tmpfs として作成したファイルシステムであるから、コンテナホストに影響はない。

ところで、v0.1.1 では元のルートファイルシステムを pivot_root で変更するディレクトリを mnt/ から dev/ に変更している。

理由はわからない。(/mnt は一時的なマウント用のディレクトリだからかな?) 作者のコメントを頼りにすると Since the simplest environment like busybox, there is no /mnt. という理由のためと考えられる。要するにポータビリティ。

6.root ファイルシステムの変更して実行

overlayfs 上の mnt/ をルートファイルシステムとすることで完全に隔離されたプロセス空間を生成することができる。ということで mnt/ をルートファイルシステムとしてプロセスを起動する。

ネットワーク名前空間だけは隔離していないので、コンテナホストと名前空間を共有している。ユーザー名前空間は同じになっているが、minc の起動時に --user tsuji などとすればユーザ名前空間も別のプロセスが生成される。

# ls -l /proc/$$/ns/ でコンテナとコンテナホストの名前空間の inode を確認しておく。

名前空間

inode(コンテナ)

inode(コンテナホスト)

ipc

4026532419

4026531839

mnt

4026532417

4026531840

net

4026531969

4026531969

pid

4026532420

4026531836

user

4026531837

4026531837

uts

4026532418

4026531838

シェルでお試し

羅列w

参考

Last updated