VirtualBoxのファイルシステムを10倍速くする 〜 page cache編 〜
vboxsfを速くするために頑張る記事の2本目です。
前回は、findコマンドが遅いことを調べ、速くすることができました。
今回は、VirtualBoxのファイルシステムvboxsfと、VMWareのファイルシステムvmhgfsの違いをもっと調べていきます。
vboxsfとvmhgfsの速度を比較している記事としては、Comparing Filesystem Performance in Virtual Machinesが、わかりやすくまとまっていました。
この記事を見ると、
- sequential readで、vboxsfでは100MB/s、vmhgfsでは500MB/s
- random readで、vboxsfでは100MB/s、vmhgfsでは7GB/s
と、速度の差が大きいことを指摘され、さらには、
Because the deviation of the VirtualBox throughput is so small across various test cases, I theorize that there is a single hot path of code in the VirtualBox shared folder system limiting this. They’re clearly doing something wrong.
とか言われています。
そこで今回は、vboxsfとvmhgfsの実装を比較することで、vboxsfを遅くしている原因を見つけようと思いました。
検証した環境は、find編と同様です。
現象を確認する
とりあえず、記事に書かれているような差が本当に出るのか確認しました。
確認する方法は、記事で使われていたツールIOzoneを使ったベンチマークと、現象を追いやすいように自分で組んだ検証用プログラムの実行時間の2つです。
IOzoneの検証の条件は、記事と同様に以下のようにしました。
- 64KBのsequential read
- 64MBのrandom reead
またLinuxでは基本的に、read
/write
したファイルのデータはpage cacheに、stat
したファイルのメタデータはinode cacheに保存され、次読み込むときや書き込むときに利用します。
そのため、キャッシュを使わないdirect ioで計測する方が正しいと思いますが、vboxsf/vmhgfsではdirect ioをサポートしていないようだったので、page cache、directry entry の cacheを削除した上で、計測しました。
以下がIOzoneの結果です。
$ /home/vagrant/bin/iozone -i 0 -i 1 -i 2 -g 64k -a (各ファイルシステム以下のPath)

$ /home/vagrant/bin/iozone -i 0 -i 1 -i 2 -n 64m -g 64m -a (各ファイルシステム以下のPath)

確かに、記事で紹介されていたとおりに、vboxsfだけ異常に遅い結果になることがわかりました。
次に検証用プログラムの実行速度の結果です。
readし続ける/statし続ける検証用プログラムを組んで、その実行時間を比較しています。

こちらの場合、statの実行結果に大きな差が見られました。
またreadの結果は、1回目と2回目で大きな違いが見られました。
この結果から、vboxsfはpage cacheを利用してないのではないかと推測されます。
加えて、IOzoneにおける極端な速度の差は、page cacheが効いた状態で計測されているために生じているのでは無いかと考えられます。
コードを追う
次に、vboxsfが本当にpage cacheを利用していないのか、また、なぜstatが遅いのかをコードを追って確認していきます。
具体的には、システムコールの実装を追って呼ばれているファイルシステムの関数を調べ、その実装方法をvmghfsと比較するという流れになります。
本当にページキャッシュを使っていないのか?
まず、本当にpage cacheを利用していないのか確認します。
システムコールreadは、Linux kernel内に定義されています。
これが呼ばれると、以下のようにfile->f_op->read
, もしくはfile->f_op->read_iter
を呼ぶように実装されています。
// linux/fs/read_write.c
ssize_t __vfs_read(struct file *file, char __user *buf, size_t count,
loff_t *pos)
{
if (file->f_op->read)
return file->f_op->read(file, buf, count, pos); // <- ここ
else if (file->f_op->read_iter)
return new_sync_read(file, buf, count, pos); // <- この中で, file->f_op->read_iter を呼んでいる.
else
return -EINVAL;
}
file->f_op
は、ファイルシステム毎に定義さている関数なので、次にvboxsfの実装を確認します。
vboxsfのfile->f_op->read
をみてみると、vboxsf内の関数sf_reg_read
が指定されており、その中で直接ホスト側でreadを発生させる関数を呼んでいることが確認できます。
// VirtualBox-4.3.28/src/VBox/Additions/linux/sharedfolders/regops.c
struct file_operations sf_reg_fops =
{
.read = sf_reg_read, // <- これ
.open = sf_reg_open,
.write = sf_reg_write,
.release = sf_reg_release,
一方、vmhgfsのfile->f_op->read
をみてみると、new_sync_read
が指定されており、実質、file->f_op->read_iter
で指定されているHgfsFileRead
のみが呼ばれていることがわかります。
// vmware-tools-distrib/lib/modules/source/vmhgfs-only/file.c
struct file_operations HgfsFileFileOperations = {
.read = new_sync_read,
.write = new_sync_write,
.read_iter = HgfsFileRead, // <- これ
.write_iter = HgfsFileWrite,
また、HgfsFileReadの中を確認してみると、generic_file_read_iter
が呼ばれていました。
generic_file_read_iter
は、Linux kernel側で定義されている関数で、page cacheを利用してファイルの読み込みを行います。
つまり、一度読み込んだファイルはpage cacheに乗せられ、次に読み込むときはキャッシュを利用するように動作します。
以上のように、vmhgfsではpage cacheを利用しているが、vboxsfでは利用していないことがわかりました。
おそらくは、ホストとゲストの整合性を保つため、基本的にpage cacheを使わないという選択をしたのだと思います。
でもsendfileではページキャッシュ使ってる
しかし、VirtualBox の shared folder で sendfile(2) がバグってるやつを調べたでも紹介されているように、sendfileではページキャッシュを利用しているようです。
sendfileとは。
sendfile() は、あるファイルディスクリプターから別の ファイルディスクリプターへのデータのコピーを行う。 このコピーはカーネル内で行われるので、 sendfile() は、 read(2) と write(2) を組み合わせるよりも効率がよい。 read(2) や write(2) ではユーザー空間との間でデータの転送が必要となるからである。
実際にsendfileの実装を追ってみると、in->f_op->splice_read
で、ファイルシステム毎の関数を呼んでいることがわかります。
// linux-source-3.16/fs/splice.c
long do_splice_to(struct file *in, loff_t *ppos,
struct pipe_inode_info *pipe, size_t len,
unsigned int flags)
{
ssize_t (*splice_read)(struct file *, loff_t *,
struct pipe_inode_info *, size_t, unsigned int);
int ret;
if (unlikely(!(in->f_mode & FMODE_READ)))
return -EBADF;
ret = rw_verify_area(READ, in, ppos, len);
if (unlikely(ret < 0))
return ret;
if (in->f_op->splice_read)
splice_read = in->f_op->splice_read; // <- ここ
else
splice_read = default_file_splice_read;
return splice_read(in, ppos, pipe, len, flags);
}
そして、vboxsfでは、generic_file_splice_read
が指定されています。
generic_file_splice_read
は、generic_file_read_iter
と同様に、Linux kernel側で定義されている関数で、page cacheを利用した動作をします。
// VirtualBox-4.3.28/src/VBox/Additions/linux/sharedfolders/regops.c
struct file_operations sf_reg_fops =
{
.read = sf_reg_read,
.open = sf_reg_open,
.write = sf_reg_write,
.release = sf_reg_release,
.mmap = sf_reg_mmap,
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 0)
# if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 23)
.splice_read = generic_file_splice_read, // <- このへん
# else
.sendfile = generic_file_sendfile,
# endif
しかし、上の記事で紹介されているように、ホスト側でファイルが更新されたかどうかの確認は実装されていません。
sendfile自体が、page cacheを使うこと前提で実装されているようなので、使いたくないけどpage cacheを使ったということなのかもしれません。
でもそれなら、記事でやっているようにpage cacheを消したらいいじゃないのと思います。
なぜstatが遅いのか?
また、statが遅い原因もコードを追って調べてみます。
statを追うと、inode->i_op->getattr
を呼んでいることがわかります。
// linux-source-3.16/fs/stat.c
int vfs_getattr_nosec(struct path *path, struct kstat *stat)
{
struct inode *inode = path->dentry->d_inode;
if (inode->i_op->getattr)
return inode->i_op->getattr(path->mnt, path->dentry, stat); // <- ここ
generic_fillattr(inode, stat);
return 0;
}
vboxsfのinode->i_op->getattr
を確認してみると、sf_inode_revalidate
を呼んでおり、さらにその中ではsf_stat
を呼び、ホスト側にアクセスしているようです。
このsf_inode_revalidate
のコードで気になったのが、statを発行しないパスが準備されているものの、vboxsfのデフォルト設定ではそれを利用していない点です。
下記引用部のコードでは、前回statを実行した時のjiffies
と今の値を比較し、mountするときに設定できるinode cacheの生存期間ttl
を超えるまでは、ホスト側にアクセスしないとなっています。
しかし、vboxsfのデフォルトではsf_g->ttl=0
になっており、常にホスト側のstatを発生させるようになっていました。
// VirtualBox-4.3.28/src/VBox/Additions/linux/sharedfolders/utils.c
int sf_inode_revalidate(struct dentry *dentry)
{
...
if (!sf_i->force_restat)
{
if (jiffies - dentry->d_time < sf_g->ttl) // <- ここ
return 0;
}
err = sf_stat(__func__, sf_g, sf_i->path, &info, 1);
if (err)
return err;
dentry->d_time = jiffies;
sf_init_inode(sf_g, dentry->d_inode, &info);
一方、vmhgfsの実装を確認してみると、同じような実装ですが、age=0, ttl=0
の場合でもホストへのアクセスは発生しないことがわかります。
また、ttl=HZ
に設定されているようです。つまり、1秒以内の同じファイルに対するstatは、前回の結果を使いまわす形になっています。
// vmware-tools-distrib/lib/modules/source/vmhgfs-only/inode.c
...
si = HGFS_SB_TO_COMMON(dentry->d_sb);
if (!dentry->d_inode) {
LOG(4, (KERN_DEBUG "VMware hgfs: HgfsRevalidate: null input\n"));
return -EINVAL;
}
LOG(6, (KERN_DEBUG "VMware hgfs: HgfsRevalidate: name %s, "
"inum %lu\n", dentry->d_name.name, dentry->d_inode->i_ino));
age = jiffies - dentry->d_time;
if (age > si->ttl || iinfo->hostFileId == 0) { // <- このへん
// この中で、ホストへのアクセスを実行
}
以上のようにvboxsfは、ホストとゲストの整合性に対して、vmhgfsと比べて堅い方に倒しており、「page cacheは利用しない」、「dentry cacheは常にホストまで確認する」となっているようです。
vboxsfを修正する
たしかに、ホストとゲストの整合性を保つことは重要です。
しかし、ホストとの整合性がmsec単位で求められる状況でなければ、page cacheを上手く使って処理を速くすることもまた重要だと思います。
そのため、vboxsfでもpage cacheを使えるようにしてみました。
通常のread/writeでも、page cache使う
まず、page cacheを使うようにしました。
やり方はvmhgfsと同様に、file->f_op
のread/writeには、new_sync_read
/new_sync_write
を指定し、read_iter
/write_iter
を呼び出されるようにしました。
read_iter
/write_iter
では、generic_file_read_iter
/generic_file_write_iter
を呼ぶようにします。
[src/vboxguest-5.0.4/vboxsf/regops.c]
これで、page cacheを使えるようになりました。
古いキャッシュを削除する
このままでは、sendfileのようにホストでの更新が反映されなくなってしまうので、古いpage cacheをクリアする処理を追加する必要があります。
やり方は、inode情報を更新している関数(sf_inode_revalidate)で、以前取得したinode情報と新しく取得したinode情報を比較し、「sizeが異なる」もしくは、「timestampが異なる」場合、page cacheをクリアを実行することにしました。
[src/vboxguest-5.0.4/vboxsf/utils.c]
また、read_iter, write_iter, splice_read, llseekなど、ファイルの操作を実施する前に、sf_inode_revalidate
を呼ぶようにしてやります。
[src/vboxguest-5.0.4/vboxsf/regops.c]
これでホストの修正が正しく反映されるようになりました。
毎回statしない
しかし、まだ少し問題が残っています。
現在のvboxsfは、普通にmountした場合、ttl=0に設定されるため、statを実行すると、常にホストにアクセスすることになります。
そのため、このままでは、数msec中に何度もホストへのアクセスが発生してしまい、かなり遅くなってします。
vmhgfsではttl=250、つまり1秒以内に行われた同じファイルへのstatは、キャッシュを利用することにしています。
同じようにしようかとも思いましたが、1秒はちょっと長すぎるんじゃないかなと思い、1/HZ 以内であればキャッシュを利用する形に変更しました。
[src/vboxguest-5.0.4/vboxsf/utils.c]
結果
この修正を入れたiozoneの結果が以下です。
$ /home/vagrant/bin/iozone -i 0 -i 1 -i 2 -g 64k -a (各ファイルシステム以下のPath)

$ /home/vagrant/bin/iozone -i 0 -i 1 -i 2 -n 64m -g 64m -a (各ファイルシステム以下のPath)

今まで極端に遅かったvboxsfの結果が速くなっていることがわかります。(ただcache使ってるだけだけど)
IOzoneのコードを確認して見ると、write -> readの順番に計測を実行しており、writeの時に乗ったpage cacheを使ってreadが実行されているようです。
IOzoneではreadとwriteを分離するのが上手くできなかったので、page cacheに乗って居ない状態からのreadのベンチマークを行う際は、fioやBonnie++を使うほうが良さそうです。
検証用プログラムの実行時間の結果はこちらです。

readの1回目と2回目は、vmhgfsと同様に、page cacheを利用できていることがわかります。
statもinode cacheが使われており、早くなっていることがわかります。
ただこのstatの結果は、「1/HZ sec以内の同じファイルに対するstat」を、inode cacheを使って回避しているだけなので、大量の異なるファイルにstatをかけるアプリケーションに対して効果はありません。
ホストでの変更を気にしなくても良い場合は、mountするときのttlの値を少し長めに設定するという手もありますが、あまり需要はないかな。。
まとめ
vboxsfでもpage cacheを利用できるようにすることで、コンパイルやgrepなどを多少速くすることができました。
この修正は、VirtualBoxに取り込んでもらえないか打診中です。
kokukuma/vboxguestに、コードとインストール方法を乗せておきますので、使ってみたい人がいればこちらからどうぞ。