在用fuse实现的一个聚合文件系统测试中,概率性出现stat或者open文件的时候,返回ESTALE错误。
该文件系统实现共享,多个用户可以访问同一个目录,并且带有不同的权限。
复现条件是:
两个用户同时访问共享文件夹,并且执行mv操作
挂载聚合文件系统:
# modprobe fuse
# mkdir /mnt/merge
# cfs /mnt/merge /mnt/vda/pa0 /mnt/vda/pb0
复现脚本:
/mnt/fs_stress /mnt/merge/user0/myshare/user1/filemgr/test >/dev/null &
/mnt/fs_stress /mnt/merge/user0/myshare/user2/filemgr/test >/dev/null &
mv /mnt/merge/user0/data/abd/* /mnt/merge/user0/data/abc
fs_stress就是遍历文件夹,访问属性,读取文件内容,在后台跑两个。
定位过程
给vfs加日志,一步步定位到是:
fs/stat.c
vfs_fstatat() -> -ESTALE
vfs_statx() -> -ESTALE
fs/namei.c
filename_lookup() -> -ESTALE
path_lookupat() -> -ESTALE
link_path_walk() -> ESTALE
walk_component() -> ESTALE
lookup_slow() -> ESTALE
inode->i_op->lookup() -> STALE
fs/fuse/dir.c
上面的lookup函数,是fuse提供的
fuse_lookup()
fs/dcache.c
d_splice_alias() -> ESTALE
__d_unalias() -> ESTALE
__d_unalias()就是最终的根因了。
/*
* This helper attempts to cope with remotely renamed directories
*
* It assumes that the caller is already holding
* dentry->d_parent->d_inode->i_mutex, and rename_lock
*
* Note: If ever the locking in lock_rename() changes, then please
* remember to update this too...
*/
static int __d_unalias(struct inode *inode,
struct dentry *dentry, struct dentry *alias)
{
struct mutex *m1 = NULL;
struct rw_semaphore *m2 = NULL;
int ret = -ESTALE;
/* If alias and dentry share a parent, then no extra locks required */
if (alias->d_parent == dentry->d_parent)
goto out_unalias;
/* See lock_rename() */
if (!mutex_trylock(&dentry->d_sb->s_vfs_rename_mutex))
goto out_err;
m1 = &dentry->d_sb->s_vfs_rename_mutex;
if (!inode_trylock_shared(alias->d_parent->d_inode))
goto out_err;
m2 = &alias->d_parent->d_inode->i_rwsem;
out_unalias:
__d_move(alias, dentry, false);
ret = 0;
out_err:
if (m2)
up_read(m2);
if (m1)
mutex_unlock(m1);
return ret;
}这个函数出错就是返回ESTALE,出错的原因很明朗了,因为只有再执行mv操作的时候,才会复现问题。
所以就是拿取rename锁失败了。
mutex_trylock(&dentry->d_sb->s_vfs_rename_mutex)
这个锁是sb的一个全局锁。
为什么执行__d_unalias()也很明朗了
因为两个用户并发访问同一个共享文件夹,fuse返回的文件夹的inode id相同。
# stat merge//user0/myshare/user2/filemgr/test
Device: 10h/16d Inode: 650241000000051281 Links: 2
# stat merge/user0/myshare/user1/filemgr/test
Device: 10h/16d Inode: 650241000000051281 Links: 2
但是路径不同,导致并发访问的时候,需要和旧的解绑,解绑过程中,由于拿取锁失败,放回了ESTALE。
d_splice_alias()中,如果是文件夹,会尝试和旧的解绑,因为Linux不支持文件夹的硬链,所以文件夹的inode,只能关联到一个dentry。
if (S_ISDIR(inode->i_mode)) {
struct dentry *new = __d_find_any_alias(inode);
if (unlikely(new))
解决办法:
共享的文件夹,为每个用户分配不同的inode id。