Upload
fumiyuki-yoshida
View
1.027
Download
3
Embed Size (px)
Citation preview
ちょっと Overlayfs の実装、読んでみました
自己紹介
名前: @akachochin
近況:お仕事は、組み込みソフト技術者です。
一時期カーネルを離れていましたが、 NetBSD/Linux カーネルの世界にまた戻ってきました。
趣味:趣味は名のとおり、赤提灯めぐり。場末の飲み屋、さいこーです。これだけで 20 分程度語れるけど、今は語りません( 笑 )
いきさつ
前回の「コンテナ型仮想化の情報交換会@東京」懇親会で「ちょっと喋ってみようかなあ」と勢いで言ったのが運のつき。
しっかり覚えられてました ( 笑 )
今日お話すること
腐ってもカーネルでご飯食べてる身。何か、カーネルのソース読むか。
じゃあ、 Overlayfs 読むか。 Union Filesystem の実装見たことないし。
使用上の注意です。
● 読んだソースは、 linux_4.4.0-21.37 (Ubuntu16.04) です。
自宅で主に使っているのが Ubuntu16.04 で、 ftrace した結果と突き合わせるといろいろはかどるからです。
● プレゼンの時間的な制約などから、細かいことは説明しきれません。なので、厳密さよりもわかりやすさを優先したり、話を端折ったりしている箇所が結構あります。ご了承のほどを。
Overlayfs って何?
Overlayfs って何?
Overlayfs って何?
この章を書くにあたり、「 LXC で学ぶコンテナ入門 -軽量仮想化環境を実現する技術第 18 回 Linux カーネルのコンテナ機能 [7]overlayfs 」という記事を大いに参考としました。
記事を書かれた加藤さん、大変感謝です。
Overlayfs って何?
Overlayfs は Union filesystem の一種です。
Union filesystem は複数のファイルシステムをひとつの場所でマウントし、仮想的にまとめてあたかもひとつのファイルシステムであるかのように見せる技術です。
Linux 3.18 のときに Linux カーネルにマージされました。
また、 Docker では 1.4.0 で overlayfs に対応しました。
Overlayfs って何?
図にすると、こんな感じ。
※https://docs.docker.com/engine/userguide/storagedriver/overlayfs-driver/ より図を引用
複数のファイルシステムのレイヤを重ねあわせてマージしたものを見せている雰囲気がつかめたでしょうか?
Overlayfs って何?
とはいっても、イメージしづらいので、 Docker のコンテナを例に説明します。
・ Docker の Image Layers はReadOnly なファイルシステム層を参照しています。
・ Docker がコンテナを作るとき、 Image の上に Writeableな層 ( コンテナ層 ) が追加されます。
※https://docs.docker.com/engine/userguide/storagedriver/imagesandcontainers/ より図を引用
Overlayfs って何?
コンテナ層を使って、 Docker はひとつのイメージを共有しつつ、それぞれ異なる複数のコンテナを作れるのです。
※https://docs.docker.com/engine/userguide/storagedriver/imagesandcontainers/ より図を引用
Overlayfs って何?
Overlayfs に戻ります。 Overlayfs視点では、各レイヤなどの名称は以下のとおりとなります。
※https://docs.docker.com/engine/userguide/storagedriver/overlayfs-driver/ より図を引用
Docker での呼び名 overlayfs での呼び名
下の Read Only な層 Image Layer lowerdir
上の writable な層 Container Layer upperdir
マージした結果 Container mount merged(Overlayfs から見たときに見えるもの )
Overlayfs って何?
マージされたファイルシステムを Overlayfs経由で見ると・・・
● ファイルシステム間で重複しないものはそのまま見える
● ファイルが重複→より上位のファイルシステム内のファイルが見える
● ディレクトリが重複→内容がマージされたディレクトリが見える
lower
upper A
overlay
a
B b
C
C d
d
A a C dB b
upper と lower のディレクトリを
マージ
Overlayfs って何?
lower から upperへコピーして・・・ (copy up)lower
upperA
overlay
B
A B
B
Write Write
upper側へ書き込み
overlayfs経由でファイルへの書き込みを行うと・・・
● upper側→ upper側のファイルが書き換わる
● lower側→ lower側からupper側にファイルがコピーされ、upper側が書き換わる
Overlayfs って何?
overlayfs経由でファイルの削除を行うと・・・
● upper側→ upper側のファイルが削除される
● lower側→ upper側に「削除」を示すファイルを生成、lower側ファイルを「覆う」。
lower
upper A
overlay
B
A B
削除 削除
「削除」を示すファイルが作成され、 lower のファイルが「見えなくなる」。 (whiteout)
Overlayfs って何?
ここまで見たことをまとめます
● overlayfs は、 lowerdir と upperdir をひとつのマウントポイントでマウントし、その中で仮想的にマージして見せます。
● 下に Read Only なファイルシステム層 (lowerdir) を積み重ねます。
● lowerdir の上に書き込み可能なファイルシステム層(upperdir) をひとつ置きます。
● ファイルシステムへの変更は upperdir で吸収します。
Overlayfs を使う準備
Overlayfs を使う準備
Overlayfs をマウントする際、以下のようなコマンドを実行します。
mount -t overlayfs -o \ lowerdir=lower,upperdir=upper,workdir=work \overlayfs mountpointdir
少し複雑ですので、ひとつずつ見ていきましょう( 「 \」は行変更を示します。念のため。 )
Overlayfs を使う準備
-t overlayfs
これは、マウントしたいファイルシステムが Overlayfs であることを指定しています。
Overlayfs の使い方
-o \lowerdir=lower,upperdir=upper,workdir=work
オプションをカンマ区切りで指定します。指定するオプションは以下のとおりです。オプション 概要
lowerdir lowerdir として重ねあわせるファイルシステムイメージ(ディレクトリ ) のパスを指定。複数指定可能。
upperdir upperdir として重ねあわせるファイルシステムイメージ(ディレクトリ ) のパスを指定。書き込み可能なローカルのファイルシステムの必要がある。
workdir overlayfs が使う作業ディレクトリ。 upperdir と同じファイルシステムの名前空間内に存在する必要あり。upperdir を指定する際は、セットで指定。
Overlayfs を使う準備
overlayfs mountpointdir
マウントする際はストレージデバイスを指定することが多いのですが、 overlayfs を使用するときに指定する「デバイス」は仮想的な「デバイス」という意味合いで overlayfs を指定します。
最後に仮想的にファイルシステム群をマージした結果を見せるためのマウントポイントを指定します。
Overlayfs の動き
Overlayfs の動き
今回は以下のディレクトリ構成で動きを見ます。
lowerdir
upperdirupfile updir
workdir
overlay
lowfile lowdir
注:今回は実験を簡単にするために、すべてのディレクトリ・ファイルは同一の名前空間内の ext4内にあります。
Overlayfs の動き - ls( ディレクトリを読む ) -
overlayfs のマウントをしたあとで、 ls コマンドを使い、 overlay の中を見ます。すると、 upperdir と lowerdir がマージされたものが見えます。
lowerdir
upperdirupfile updir
overlay
lowfile lowdir
upfile updir lowfile lowdir
Overlayfs の動き - ls( ディレクトリを読む ) -
2つのファイルシステムが「重ね合わせられ」ひとつのファイルシステムに見えることがわかります。
では、これをどうやって実現しているのでしょうか。ここで、カーネルのソースを読んでみたいと思います。
Overlayfs の動き - ls( ディレクトリを読む ) -
ls を行う際、ディレクトリの中身を読むために libc のreaddir() が呼ばれます。glibc の場合、 readdir() を経由して、 getdents というシステムコールが呼ばれます。
getdents は最終的に iterate_dir() という関数を呼び出します。この関数は 2 つの引数を受け取ります。
Overlayfs の動き - ls( ディレクトリを読む ) -
int iterate_dir(struct file *file, struct dir_context *ctx){ /* パラメータチェックなどは割愛 */ /* ディレクトリ内の最後のエントリに到達するまで */ if (!IS_DEADDIR(inode)) { /* 前処理は割愛 */ res = file->f_op->iterate(file, ctx); /* 後処理も割愛 */ } return res;}
ここで、 dir_context 構造体 (ctx) は以下 2点が役目です。 - ディレクトリの中をたどるごとに呼び出す処理を関数ポインタで渡す - 関数ポインタ経由で渡した処理が都度の結果を格納する
Overlayfs の動き - ls( ディレクトリを読む ) -
ここで、 file->f_op->iterate()経由で ovl_iterate() が呼ばれます。これは Overlayfs依存の関数です。最初に、マージの必要がないケースを処理します。static int ovl_iterate(struct file *file, struct dir_context *ctx){ /* 略 */ /* * 該当ディレクトリは重ねあわせたファイルシステム間で * 重複していなくて、マージの必要がないディレクトリである * この場合、そのディレクトリの実体をそのまま読みだす。 */ if (od->is_real) { /* 略 */ return iterate_dir(od->realfile, ctx); }
Overlayfs の動き - ls( ディレクトリを読む ) -
次に、 upper側と lower側で同名のディレクトリがあり、マージが必要なケースを処理します。該当ディレクトリに対するマージがこれまでにされておらず、結果が再利用できないケースの処理です。
/* 前ページからの続き */ /* マージが必要だが、マージ結果のキャッシュがない */ if (!od->cache) { struct ovl_dir_cache *cache; /* マージを行い、その結果を「キャッシュ」とする */ cache = ovl_cache_get(dentry); /* 略 */ }
Overlayfs の動き - ls( ディレクトリを読む ) -
最後に、得られた結果 (キャッシュ ) をたどり、ユーザから渡されたバッファにコピーします。コピー自体は ctx の関数ポインタで渡されたfilldir で実施します。
/* 前ページの続き */ /* 辿れるだけディレクトリの中身をたどる */ while (od->cursor != &od->cache->entries) { p = list_entry(od->cursor, struct ovl_cache_entry, l_node); /* 「消された」ことになっていない場合、 */ if (!p->is_whiteout) /* 呼び出し元から渡された ctx の中の関数ポインタを呼ぶ */ if (!dir_emit(ctx, p->name, p->len, p->ino, p->type)) break; /* 略 */
Overlayfs の動き - ls( ディレクトリを読む ) -
関数ポインタ (filldir) があるため、呼び出し構造がややわかりにくいです。そこで、ここまでの話を以下の概要図でまとめました。
ユーザ側ユーザ側 iterate_diriterate_dir filldirfilldir ovl_iterateovl_iterate 該当レイヤのiterate
該当レイヤのiterate
マージの必要がないケースの呼び出し
マージの必要があるケース
ディレクトリの中身をユーザ側バッファに
コピー
ディレクトリの中身をユーザ側バッファに
コピー
Overlayfs の動き - upper 側 file への write -
upper側にあるファイルを Overlayfs経由で書き換えると、 upper側のファイルが直接書き換わります。
では、これをどうやって実現しているのでしょうか。ここで、カーネルのソースを読んでみたいと思います。
Overlayfs の動き - upper 側 file への write -
upper側にあるファイルを書き込み可能で open() します。open() からシステムコールを経由して、 vfs_open() が呼び出され、関数ポインタ経由で ovl_d_select_inode() が呼ばれます。
int vfs_open(const struct path *path, struct file *file, const struct cred *cred){
/* 略 */ if (dentry->d_flags & DCACHE_OP_SELECT_INODE) { inode = dentry->d_op->d_select_inode(dentry, file->f_flags); if (IS_ERR(inode)) return PTR_ERR(inode); }
Overlayfs の動き - upper 側 file への write -
ovl_d_select_inode() の中で ovl_path_real() を呼び出します。これにより、実体 ( ここでは upper側のファイル ) のパス情報 (struct path) を取得します。
struct inode *ovl_d_select_inode(...引数省略 ...){
/* 略 */ type = ovl_path_real(dentry, &realpath); if (ovl_open_need_copy_up(...引数省略 ...)) {
/* 後述 */ }
/* 略 *//* upper側の inode を返すことで、呼び出し元で upper側のファイル
を open() する。これで以後のアクセスは upper側に行われる */ return d_backing_inode(realpath.dentry);}
Overlayfs の動き - lower 側 file への write -
lower側にあるファイルを overlayfs経由で書き換えると、 upper側に書き換え対象と同名のファイルが生成され、 upper側のファイルが書き換わります。
これにより、以後は upper側の「書き換えられた」ファイルが見え、 lower側ファイルは書き換わりません。
では、これをどうやって実現しているのでしょうか。ここで、カーネルのソースを読んでみたいと思います。
Overlayfs の動き - lower 側 file への write -
vfs_open() 自体の動きは、先に説明した upper側にあるファイルへの書き込みと同様です。
しかし、 d_select_inode経由で呼ばれる ovl_d_select_inodeの挙動が異なります。それは、 copy_upという処理が行われることです。
Overlayfs の動き - lower 側 file への write -
struct inode *ovl_d_select_inode(...引数略 ...){ /* 略 */ type = ovl_path_real(dentry, &realpath); if (ovl_open_need_copy_up(...引数略 ...)) { /* 略 */
/* ここで lower側にあるファイルを upper側にコピーします */ err = ovl_copy_up(dentry); /* 略 */ /* * ファイルを upper側にコピーした後でパス情報を返し、 * 書き込み処理が upper側ファイルに行われるようにする */ ovl_path_upper(dentry, &realpath);
Overlayfs の動き - lower 側 file への write -
ovl_copy_upでは以下の図のようなことを実施します。わざわざ workdir を中継する理由は、変更をアトミック ( 「完全失敗で何もない」か、「完全成功でファイルが upper側に生成される」かのどちらか ) にしたいためと推定します。 (煩雑さを避けるため、詳細については相当略しています。 )
lower
upper
手順 2.workdir内のファイルをリネームする
手順 1.一旦 workdirにコピー
workdir
upper と lower は異なるファイルシステム。そのため、コピー失敗の
リスクもある。そこで、 workdir を中継。そうすることで失敗時の
変更は workdir にとどまり、upper から残骸などが
見えない。
Overlayfs の動き - lower 側 file の削除 -
lower側にあるファイルを overlayfs経由で削除しても lower側のファイルは消えません。その代わりに、「 whiteout 」という処理が upper側に行われ、あたかも該当ファイルが消えたかのように見せかけます。
では、これをどうやって実現しているのでしょうか。ここで、カーネルのソースを読んでみたいと思います。
Overlayfs の動き - lower 側 file の削除 -
ファイルを削除する際、 unlinkat() システムコールを呼びます。この関数は最終的に vfs_unlink() を呼び出します。
vfs_unlink() では関数ポインタ経由で ovl_unlink() を呼び出します。
さらに ovl_unlink() では ovl_do_remove() を呼び出します。
Overlayfs の動き - lower 側 file の削除 -
Overlayfs を経由したファイル削除には、以下の 3パターンがあります。
lower
upper A
overlay
B
A B
C
削除 削除
C
削除
パターン1 パターン 2 パターン 3
C
Overlayfs の動き - lower 側 file の削除 -
static int ovl_do_remove(struct dentry *dentry, bool is_dir){ /* 略 ( 大切な処理も含むが、煩雑さを避けるため略 */ type = ovl_path_type(dentry); /* upper側にだけあるファイルは、 upper側ファイルを直接消す */ if (OVL_TYPE_PURE_UPPER(type)) { err = ovl_remove_upper(dentry, is_dir); /* lower側にも該当ファイルがある場合、 whiteout処理を実行 */ } else { /* 略 */ err = ovl_remove_and_whiteout(dentry, is_dir); /* 略 */ }
パターン 1.upper 側を直接削除
Overlayfs の動き - lower 側 file の削除 -
whiteout処理とは、「 upper側に特殊なファイルを作成し、あたかも lower側にあるファイルが消されたかのように見せかける」処理です。先のパターン 2,3 の場合、必要です。やり方は Overlayfs のバージョンによって異なります。
Overlayfs のバージョン
処理概要
V1 消そうとしているファイルと同名のシンボリックリンクを upper側に作り、拡張属性 "trusted.overlay.whiteout=y"を付与する。
v2 消そうとしているファイルと同名の charcter device file(major0:minor0) を upper側に作成する
Overlayfs の動き - lower 側 file の削除 -
static int ovl_remove_and_whiteout(struct dentry *dentry, bool is_dir){ /* 略 */ if (is_dir) { /* ディレクトリを消す場合の処理。今回は略 */ }
/* 略 */ /* ここで whiteout処理を実施 */ whiteout = ovl_whiteout(workdir, dentry); /* 略 */
upper = ovl_dentry_upper(dentry);
Overlayfs の動き - lower 側 file の削除 -
/* lower側にのみ消去対象のファイルがある場合 (パターン 2) */ if (!upper) { /* * upper側にディレクトリエントリを作成する。 * "lookup"なのだが、名前とディレクトリで検索して見つ *からない場合、 inode が空のディレクトリエントリが返る。 * ( ここでは新規に whiteout のためのファイルを生成したと * 考えれば良い ) */ upper = lookup_one_len(dentry->d_name.name, upperdir, dentry->d_name.len); /* 略 */ err = ovl_do_rename(wdir, whiteout, udir, upper, 0); /* 略 */
Overlayfs の動き - lower 側 file の削除 -
/* * 少なくとも upper側に消去対象のファイルがある場合 * (upper側と lower側の双方にあるケース。パターン 3.) * 単純に upper側のファイルを消すと、重ね合わせにより lower * 側のファイルが見えてしまい「消せていない」状態になる。 * よって、 upper側のファイルを whiteout のファイルに置き換える */ } else { /* 略 */ err = ovl_do_rename(wdir, whiteout, udir, upper, flags); /* 略 */ } /* 略 */
おわりに
ここまで駆け足で Overlayfs の概要と実装の概要をみてきました。しかし、細かいところについては時間の関係で説明できていません。ソースを読んだ箇所については、細かい話を近いうちに Qiita(http://qiita.com/akachochin) にまとめたいと思います。
ソースを自分で読みたい人のために
いくつかアドバイスです。
1.overlayfs内に現れる dentry 構造体がどこのレイヤの何のファイル /ディレクトリを指しているのか意識しましょう。
2.Linux のファイルシステム層について基本的な事項を学んでみましょう。日本語で入手できる資料では、「 Linux カーネル解読室」第 15 〜 17 章が最も助けになると思います。
3.ftrace を使って、カーネル内関数呼び出しを追いかけましょう。非常にはかどります。
ソースを自分で読みたい人のために
4.Linux ファイルシステム層のよく使われる関数 (特にlookup 系 ) に慣れましょう。そして、挙動がわからなければ、ソースを読んでみましょう。そうすることによってファイルシステムの実装に慣れてきます。
5. データ構造をメモりながら読みましょう。今回紹介しませんでしたが、 mount処理 ,lookup 系処理を読むと、全体感が捉えやすくなるでしょう。
参考文献・ Web ページ
Webページ
● 「 LXC で学ぶコンテナ入門 -軽量仮想化環境を実現する技術第 18 回 Linux カーネルのコンテナ機能 [7]overlayfs 」http://gihyo.jp/admin/serial/01/linux_containers/0018
● 「 Understand images, containers, and storage drivers 」https://docs.docker.com/engine/userguide/storagedriver/imagesandcontainers/
● そのファイル、安全に更新できていますか?(アトミックなファイル操作:前編)https://heartbeats.jp/hbblog/2013/10/atomic01.html
参考文献・ Web ページ
文献 (敬称略 )
● 「 Linux カーネル 2.6解読室」高橋浩和 /小田逸郎 /山幡為佐久 著
● 「プログラマのための Docker 教科書」阿佐志保 著
● 「 Understanding the Linux Kernel(3rd edition) 」DANIEL P.BOVET & MARCO CESATI 著
● 「 Linux Kernel Development(3rd Edition) 」Robert Love 著
ご静聴ありがとうございました。