基于fuse的文件系统导出NFS的问题

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系统来提供对外服务。如下图:

基于fuse的文件系统导出NFS的问题

在上述用法中,使用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的时候并无法找到集群中的文件或者目录。