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とで論理的に違うホストなので、キャッシュ管理に一貫性を持たせる仕組みがないといけないようです。( キャッシュ・コヒーレンシーとでも呼んだらよい?)
NFS や Samba
参考として、異なるホストでファイルを共有する NFS や Samba の実装を見ました。 NFS や Samba では 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