VirtualBox の shared folder で sendfile(2) がバグってるやつを調べた

Vagrant というか VirtualBox の話でゲストOS が Linux の話です。

sendfile(2) のバグ

Nginx や Apache で sendfile(2) サポートを有効にしていると VirtualBox の shared folder ( /vagrant ) のファイルを ホストOS側からで更新しても反映されないバグが知られています。Vagrant のドキュメント にも以下のワークアラウンドで回避しろとあります

There is a VirtualBox bug related to sendfile which can result in corrupted or non-updating files. You should deactivate sendfile in any web servers you may be running.

In Nginx:

sendfile off;
In Apache:

EnableSendfile Off

vboxsf

VirtualBox の shared folder は vboxsf という Linuxカーネルモジュールで実装されたファイルシステムです ( ホストOSとゲストOSとを繋ぐ部分は違うんだろうけど)。 このファイルシステムの実装に不備があります

いろいろ調べてみたところ

  • vboxsf は sendfile(2) を generic_file_splice_read ( カーネルのバージョンによっては generic_file_sendfile ) で実装している
  • generic_file_splice_read は、読み取り側デスクリプタのデータを一旦ページキャッシュに載せてから、書き込み側デスクリプタに書き出す
  • ページキャッシュを扱う際は、ファイルの内容を書き換える際にページキャッシュも適宜 破棄/書き換え すべき

ところが

  • generic_file_splice_read はファイルが書き換えられているかどうかを確認しない
  • ホストOSでファイルが更新されていても generic_file_splice_read は知らず知らず 古いページキャッシュを返してしまう

という問題のようです。

ホストOSとゲストOSとで論理的に違うホストなので、キャッシュ管理に一貫性を持たせる仕組みがないといけないようです。( キャッシュ・コヒーレンシーとでも呼んだらよい?)

NFSSamba

参考として、異なるホストでファイルを共有する NFSSamba の実装を見ました。 NFSSamba では sendfile(2) 呼び出し時に inode の情報を見て 適宜ページキャッシュを破棄する仕組みをいれています。

static ssize_t
nfs_file_splice_read(struct file *filp, loff_t *ppos,
		     struct pipe_inode_info *pipe, size_t count,
		     unsigned int flags)
{
	struct dentry *dentry = filp->f_path.dentry;
	struct inode *inode = dentry->d_inode;
	ssize_t res;

	dprintk("NFS: splice_read(%s/%s, %lu@%Lu)\n",
		dentry->d_parent->d_name.name, dentry->d_name.name,
		(unsigned long) count, (unsigned long long) *ppos);
        // これ
	res = nfs_revalidate_mapping(inode, filp->f_mapping);
	if (!res)
		res = generic_file_splice_read(filp, ppos, pipe, count, flags);
	return res;
}
static ssize_t
smb_file_splice_read(struct file *file, loff_t *ppos,
		     struct pipe_inode_info *pipe, size_t count,
		     unsigned int flags)
{
	struct dentry *dentry = file->f_path.dentry;
	ssize_t status;

	VERBOSE("file %s/%s, pos=%Ld, count=%lu\n",
		DENTRY_PATH(dentry), *ppos, count);

        // これ
	status = smb_revalidate_inode(dentry);
	if (status) {
		PARANOIA("%s/%s validation failed, error=%Zd\n",
			 DENTRY_PATH(dentry), status);
		goto out;
	}
	status = generic_file_splice_read(file, ppos, pipe, count, flags);
out:
	return status;
}

vboxfs でも同様の仕組みが必要そうですね

試作パッチ

sendfile(2) の呼び出しの際に invalidate_mapping_pages でページキャッシュを破棄するようにしたら、とりあえず、不具合は直りました。 CentOS6.5 で試しています

--- /opt/VBoxGuestAdditions-4.3.4/src/vboxguest-4.3.4/vboxsf/regops.c.org	2014-03-20 05:11:51.261751419 +0000
+++ /opt/VBoxGuestAdditions-4.3.4/src/vboxguest-4.3.4/vboxsf/regops.c	2014-03-13 14:47:44.159223601 +0000
@@ -561,6 +561,14 @@
     return 0;
 }
 
+ssize_t sf_reg_splice_read(struct file *in, loff_t *ppos,
+                                 struct pipe_inode_info *pipe, size_t len,
+                                 unsigned int flags)
+{
+    invalidate_mapping_pages(in->f_mapping, 0, -1);
+    return generic_file_splice_read(in, ppos, pipe, len, flags);
+}
+
 struct file_operations sf_reg_fops =
 {
     .read        = sf_reg_read,
@@ -570,7 +578,7 @@
     .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,
+    .splice_read = sf_reg_splice_read,
 # else
     .sendfile    = generic_file_sendfile,
 # endif

パッチを適用後、下記の手順で vboxfs をリビルド・リロードできます

$ sudo umount /vagrant/
$ sudo rmmod vboxsf
$ sudo /etc/init.d/vboxadd setup
$ sudo modprobe vboxsf
$ sudo mount -t vboxsf -o uid=`id -u vagrant`,gid=`id -g vagrant` v-root /vagrant

しかしこれでは sendfile(2) を呼び出す度にページキャッシュを破棄するので全然効率がよくないですね ... ホストOSで更新があった時だけ破棄するとよさげですが、後一歩実装方法がよく分からず

とりあえず不具合の原因が分かったところで満足してしまっています < イマココ

P.S

半年くらい前に調べて塩漬けにしていたエントリなのでした :q