FUSE(The Filesystem in Userspace)介绍:
像ext4,xfs等本地文件系统都是在linux内核代码中,并且运行在内核态,如果要在内核中定制化开发一个文件系统,对程序员的能力要求是比较高的。fuse为用户态文件系统提供了与VFS交互的通道,fuse分为内核态fuse模块,用户态libfuse和fusemount工具。其中内核态fuse模块是其最核心的功能,主要是转发VFS的operations到用户态,实现export文件系统(NFS)接口;可以基于用户态的libfuse进行开发,如mcachefs等,也可以根据fuse的协议自己编写通信模块,如GlusterFS。
基于fuse开发的用户态文件系统一般只支持POSIX接口,在某些不能通过POSIX方式mount的时候,一般会采用上层再套一层NAS系统来提供对外服务。如下图:
在上述用法中,使用nfs客户端的程序在访问文件或者目录的时候可能会收到Stale file handle(errno: ESTALE)的报错,如GlusterFS就有这个问题,其原因我们下面逐步分析。
Stale file handle问题分析:
nfs客户端和服务端(如:nfsd,nfs-ganesha等)之间通过RPC协议进行通信,与其他文件系统以inode表示其中的一个文件或目录类似,nfs客户端和服务端都通过fh(file handle)来表示,且各自独立cache fh(问题所在),实现exportfs的文件系统都需要实例化struct export_operations结构中的操作 (详细参照内核代码:include/linux/exportfs.h)
/**
* struct export_operations - for nfsd to communicate with file systems
* @encode_fh: encode a file handle fragment from a dentry
* @fh_to_dentry: find the implied object and get a dentry for it
* @fh_to_parent: find the implied object's parent and get a dentry for it
* @get_name: find the name for a given inode in a given directory
* @get_parent: find the parent of a given directory
* @commit_metadata: commit metadata changes to stable storage
*/
struct export_operations {
int (*encode_fh)(struct inode *inode, __u32 *fh, int *max_len, struct inode *parent);
struct dentry * (*fh_to_dentry)(struct super_block *sb, struct fid *fid, int fh_len, int fh_type);
struct dentry * (*fh_to_parent)(struct super_block *sb, struct fid *fid, int fh_len, int fh_type);
int (*get_name)(struct dentry *parent, char *name, struct dentry *child);
struct dentry * (*get_parent)(struct dentry *child);
int (*commit_metadata)(struct inode *inode);RH_KABI_EXTEND(int (*get_uuid)(struct super_block *sb, u8 *buf, u32 *len, u64 *offset))
RH_KABI_EXTEND(int (*map_blocks)(struct inode *inode, loff_t offset, u64 len, struct iomap *iomap,
bool write, u32 *device_generation))
RH_KABI_EXTEND(int (*commit_blocks)(struct inode *inode, struct iomap *iomaps, int nr_iomaps, struct iattr *iattr))
};
下面我们看下fuse内核模块中这部分的实现:
static const struct export_operations fuse_export_operations = {
.fh_to_dentry = fuse_fh_to_dentry, // 通过fh解析出文件或目录的标识(如ext4中保存在磁盘上的inode号)
.fh_to_parent = fuse_fh_to_parent, // 通过fh解析出父目录的标识
.encode_fh = fuse_encode_fh, // 将文件或者目录的标识编码到fh中
.get_parent = fuse_get_parent, // 通过child找到parent
};
static struct dentry *fuse_fh_to_dentry(struct super_block *sb,
struct fid *fid, int fh_len, int fh_type)
{
struct fuse_inode_handle handle;if ((fh_type != 0x81 && fh_type != 0x82) || fh_len < 3)
return NULL;handle.nodeid = (u64) fid->raw[0] << 32;
handle.nodeid |= (u64) fid->raw[1];
handle.generation = fid->raw[2];
return fuse_get_dentry(sb, &handle);
}static struct dentry *fuse_get_dentry(struct super_block *sb,
struct fuse_inode_handle *handle)
{
struct fuse_conn *fc = get_fuse_conn_super(sb);
struct inode *inode;
struct dentry *entry;
int err = -ESTALE;if (handle->nodeid == 0)
goto out_err;// ilookup5是在inode cache中通过nodeid查找inode
inode = ilookup5(sb, handle->nodeid, fuse_inode_eq, &handle->nodeid);
if (!inode) { // 在inode cache中没有找到
struct fuse_entry_out outarg;
struct qstr name;if (!fc->export_support) // 用户态的libfuse或者自己实现的fuse通信模块都需要设置FUSE_EXPORT_SUPPORT,如果没有设置就直接报错了
goto out_err;name.len = 1;
name.name = "."; // 通过name为"." ,parent为自己的inode进行特殊的lookup来看文件是否存在
err = fuse_lookup_name(sb, handle->nodeid, &name, &outarg,
&inode);
if (err && err != -ENOENT)
goto out_err;
if (err || !inode) { // 返回ENOENT就
err = -ESTALE;
goto out_err;
}
err = -EIO;
if (get_node_id(inode) != handle->nodeid)
goto out_iput;
}
err = -ESTALE;
if (inode->i_generation != handle->generation)
goto out_iput;entry = d_obtain_alias(inode);
if (!IS_ERR(entry) && get_node_id(inode) != FUSE_ROOT_ID)
fuse_invalidate_entry_cache(entry);return entry;
out_iput:
iput(inode);
out_err:
return ERR_PTR(err);
}
从上面的代码中看到,fuse用户态的文件系统必须支持以name为"."和parent为自己的nodeid的lookup(其实在get_parent()函数中必须支持name为".."和parent为自己的nodeid的lookup找到parent)
然后我们看下libfuse里面的实现:
// libfuse在init的时候有设置FUSE_EXPORT_SUPPORT
static void fuse_lib_lookup(fuse_req_t req, fuse_ino_t parent,
const char *name)
{
struct fuse *f = req_fuse_prepare(req);
struct fuse_entry_param e;
char *path;
int err;
struct node *dot = NULL;if (name[0] == '.') {
int len = strlen(name);if (len == 1 || (name[1] == '.' && len == 2)) {
pthread_mutex_lock(&f->lock);
if (len == 1) {
if (f->conf.debug)
fprintf(stderr, "LOOKUP-DOT\n");
dot = get_node_nocheck(f, parent);
if (dot == NULL) {
pthread_mutex_unlock(&f->lock);
reply_entry(req, &e, -ESTALE);
return;
}
dot->refctr++;
} else {
if (f->conf.debug)
fprintf(stderr, "LOOKUP-DOTDOT\n");
parent = get_node(f, parent)->parent->nodeid;
}
pthread_mutex_unlock(&f->lock);
name = NULL;
}
}static struct node *get_node_nocheck(struct fuse *f, fuse_ino_t nodeid)
{
size_t hash = id_hash(f, nodeid);
struct node *node;for (node = f->id_table.array[hash]; node != NULL; node = node->id_next)
if (node->nodeid == nodeid)
return node;return NULL;
}
上面的代码中可以看出libfuse是在内存的cache中通过nodeid查找相应的node,但是如果此时cache中没有就会返回NULL,从而给内核fuse返回ESTALE错误码。可能会有疑问为什么cache中会没有呢?其原因还是因为nfs客户端和服务端的fh是分别cache的,如果nfsd的内存比较紧张,VFS的inode cache会释放某些不用的inode,从而通过fuse下发forget操作给用户态文件系统让其nlookup降为0,因为内核已经不用这个inode了,所以此时用户态文件系统就可以释放此inode相应的资源,比如libfuse的forget:
static void forget_node(struct fuse *f, fuse_ino_t nodeid, uint64_t nlookup)
{
struct node *node;
if (nodeid == FUSE_ROOT_ID)
return;
pthread_mutex_lock(&f->lock);
node = get_node(f, nodeid);while (node->nlookup == nlookup && node->treelock) {
struct lock_queue_element qe = {
.nodeid1 = nodeid,
};debug_path(f, "QUEUE PATH (forget)", nodeid, NULL, false);
queue_path(f, &qe);do {
pthread_cond_wait(&qe.cond, &f->lock);
} while (node->nlookup == nlookup && node->treelock);dequeue_path(f, &qe);
debug_path(f, "DEQUEUE_PATH (forget)", nodeid, NULL, false);
}assert(node->nlookup >= nlookup);
node->nlookup -= nlookup;
if (!node->nlookup) {
unref_node(f, node); // 引用计数-1,如果自己本身没有再引用就释放了。。
} else if (lru_enabled(f) && node->nlookup == 1) {set_forget_time(f, node);
}
pthread_mutex_unlock(&f->lock);
}
另外提一点,GlusterFS的社区版本在fuse_init的时候并没有设置FUSE_EXPORT_SUPPORT,所以内核fuse不会下发name为"."的lookup(虽然GlusterFS对此种lookup有实现),并且即使设置了FUSE_EXPORT_SUPPORT ,也会同样出现ESTALE的错误,原因是GlusterFS lookup返回的nodeid是客户端内存中的inode_t结构的一个实例,在forget后此内存就释放了,当再次用name为"."的lookup的时候并无法找到集群中的文件或者目录。