ILD

data struct design for fuse
作者:Yuan Jianpeng 邮箱:yuanjp89@163.com
发布时间:2024-12-11 站点:Inside Linux Development

    实现一个FUSE文件系统的时候,通常使用一个普通的文件系统作为backend filesystem。而fuse操作传入的参数是inode id。因此需要设计数据结构,将inode id映射到backend filesystem中的具体文件。同时要考虑支持hard link,支持move等文件操作。


fuse操作接口:

操作参数返回值说明
lookup
(ino, name)attr

查询目录下面的文件,返回文件的属性

ino是父目录的inode id

name,是文件名

forget(ino, count)void释放inode的引用计数
getattrinoattr读取属性
setattrinoattr设置属性
readlink
ino
读取符号链接的内容

mknod

mkdir

create

(ino, name)attr新建字符设备、目录、普通文件

unlink

rmdir

(ino, name)errno删除文件或目录
symlink
(ino, name, linkname)attr创建符号链接
rename

(ino old, name old,

ino new, name new)

errno移动文件或目录
link(ino, name, target ino)errno创建hard link

open

opendir

inohandle打开文件


从上述接口看出,只有两种操作:

1 涉及目录项的,包括创建、删除、查找文件或目录。都是父目录的inode id + 子文件(文件夹)名

2 涉及到具体文件或目录的,比如读取、设置属性,打开文件,都是inode id。


1 基础结构

    在文件系统里面,inode表示具体的文件,dentry表示目录项。因此我们就直接采用这个概念。

    设计两个结构体,inode和dentry。inode id存在inode结构体中。文件名存在dentry结构体中。

    inode表示fuse操作的对象,也是一个文件节点。dentry是这个inode在后备文件系统的目录。因此有了一个雏形:


struct inode {

    uint64_t ino;

};


struct dentry {

    char *name;

};


引入两个接口,新建inode和dentry:

struct inode *inode_new(uint64_t ino);

struct dentry *dentry_new(char *name);


2 inode索引

fuse传入的参数是inode id,因此为了根据inode id快速找到inode,我们需要为inode id建立哈希表。这样inode的结构体就变成这样:


struct hlist_head *hash_head;


struct inode {

    struct hlist_node hash;

    uint64_t ino;

};


引入一个接口,根据inode id查找inode

struct inode *inode_find(uint64_t ino);

3 inode引用dentry和dentry树

dentry存在的目的,是为了知道这个inode在backend filesystem中的路径。fuse不care这个dentry,它操作的对象是inode。

为了有效的组织dentry,dentry需要有个parent,组成一个树形结构。这样就可以算出一个dentry的路径。

同时inode也需要指向一个dentry,这样就把inode映射到后备文件系统中具体的文件了。这样结构体就变成:


struct inode {

    struct hlist_node hash;

    uint64_t ino;

    struct dentry *dentry;

};


struct dentry {

    char *name;

    struct dentry *parent;

};


引入一个接口,计算dentry的路径:

char *dentry_path(struct dentry *dentry)


这样getattr、setattr、readlink、open、opendir,这样操作inode的,就都能实现了。以getattr为例。


int getattr(uint64_t ino, struct stat *statbuf)

{

    // 1 根据inode id找到对应的inode

    struct inode *inode = inode_find(ino);


    // 2 根据inode的dentry可以计算出路径

    char *path = dentry_path(inode->dentry);


    // 3 在后备文件系统中统计属性即可

    return stat(back_path(path), statbuf)

}

4 lookup支持

lookup是fuse内核感知已有文件的接口,它查找一个目录的子文件的inode信息。

接口定义:int lookup(uint64_t ino, char *name, struct stat *statbuf);


lookup找到子文件,获得属性后,将添加子文件的inode和dentry。


找子文件是简单的。跟上面的getattr类似,计算出父目录的path。然后path后跟个name,就得到子文件的路径了。

在后备文件系统中读取子文件的属性。得到子文件的inode id:ino2


添加过程

struct inode *sub_ino = inode_new(ino2);

struct inode *sub_dentry = dentry_new(name);

sub_ino->dentry = sub_dentry;

sub_dentry->parent = inode->dentry;


现在的问题是,对于相同的文件,我们不能重复添加,对于inode是简单的,因为我们有inode id的哈希索引。

可以找出旧的,如果旧的不存在,才添加新的。

struct inode *sub_ino = inode_find(ino2)

if (!sub_Ino)

    sub_ino = inode_new(ino2);


对于dentry,情况变的复杂:


1 如果我们直接使用后备文件系统的inode id,且只有一个后备文件系统,且后备文件系统不会被fuse之外的地方修改。

那么问题就简化了。因为这种情况下,不考虑hard link,那么inode和dentry是一一对应的。

如果sub_ino找到了,那么对应的dentry就是我们要添加的dentry。dentry也可以直接不用添加了。可以确保dentry不会重复。


struct inode *sub_ino = inode_find(ino2)

if (!sub_Ino) {

    sub_ino = inode_new(ino2);

    struct inode *sub_dentry = dentry_new(name);

    sub_ino->dentry = sub_dentry;

    sub_dentry->parent = inode->dentry; 

}


2 inode id会变或者动态分配的场景

比如我们第一次lookup目录a下的文件b。此时b的inode id是3,然后在fuse之外,删除b,再创建一个新的b,此时inode id是4。

如果此时再次lookup。我们find 4发现是空白。但是 b 这个dentry已经存在了,且id为3的inode也存在了。我们会创建一个新的b。

这样就有两个b了。对于访问新的b,是没问题的。问题是访问inode 3,将访问的是新的文件,这也不符合逻辑。


对于动态分配的场景,比如b是个目录,如果是第一次lookup,就分配一个inode id,后续就复用那个inode id。


因此这两种需求,都要我们能查找旧的dentry。因此就需要建立一个dentry的哈希索引。

索引的key是(parent, name)。这唯一标识了一个dentry。


对于inode id变化的情况,我们希望dentry对应的旧inode不可以再访问,返回ENOENT。

因此又需要得到dentry的inode。因此dentry添加两个字段,一个支持hash索引,一个支持获得dentry的inode。

这样结构体变成:


struct inode {

    struct hlist_node hash;

    uint64_t ino;

    struct dentry *dentry;

};


struct dentry {

    struct hlist_node hash;

    char *name;

    struct dentry *parent;

    struct inode *inode;

};


引入新的接口

struct dentry *dentry_find(struct dentry *parent, char *name);


这样添加动作变成

struct inode *sub_ino = inode_find(ino2)

if (!sub_ino)

    sub_ino = inode_new(ino2);


struct dentry *sub_dentry = dentry_find(inode->dentry, name)

if (sub_dentry) {

    // 如果旧dentry的inode和现在的inode不同,将之前inode的dentry设置成NULL

    if (sub_dentry->inode != sub_ino)

        sub_dentry->inode->dentry = NULL;

}

else

    sub_dentry = dentry_new(inode->dentry, name)


sub_dentry->inode = sub_ino;

sub_ino->dentry = sub_dentry


5 新建文件

新建文件的操作和lookup是完全一样的,为新建的文件,添加inode和dentry。


6 删除文件(夹)

删除文件夹,没有啥特别的,不需要额外的数据字段了。以删除文件为例:


unlink(uint64_t ino, char *name)


1 找出父目录的inode和dentry

struct inode *inode = inode_find(ino)

struct dentry *parent = inode->dentry。


2 拼出路径,删除后备文件系统的文件

char *path = dentry_path(parent) + "/" + name


3 删除子文件对应的dentry。

注意此时子文件的dentry可能还没被lookup进来。

但是如果是fuse开启了default_permissions挂载选项,内核会先lookup儿子,来检查是否具有删除权限,此时子dentry是lookup了的。


删除dentry时,我们应当将其inode的dentry置为NULL,否则inode会访问已经释放的内存。因此dentry需要持有inode指针。


struct dentry *child = dentry_find(parent, name);

if (child) {

    child->inode->dentry = NULL;

    dentry_free(child);

}


注意,删除文件我们不释放inode,见下面的reference count。


7 移动文件(夹)

移动文件夹也没有啥特别的。inode没有变,只是把dentry的父亲修改一下,dentry名字变一下。


例如移动a目录下的b,到c目录下的d。

那么:

struct inode *a = inode_find(ino_a)

struct dentry *b = dentry_find(a->dentry, "b")

struct inode *c = inode_find(ino_c)

struct dentry *d = dentry_find(c->dentry, "d")


如果b和d已经提前lookup了。d可能存在,也可能不存在。

b->parent = c

b->name = "d"

// b的名字变了,hash的key发生了改变,需要重新哈希

rehash(b)

if (d) {

    d->inode->dentry = NULL;

    dentry_free(d);

}


8 硬链接支持

硬链接会有多个dentry指向同一个inode。删除一个dentry,仍然可以通过另外的dentry访问。因此需要修改下dentry,添加链表支持。


struct inode {

    struct hlist_node hash;

    uint64_t ino;

    struct dentry *dentry;

};


struct dentry {

    struct hlist_node hash;

    char *name;

    struct dentry *parent;

    struct inode *inode;

    struct dentry *next;

};


这样inode->dentry指向第一个硬链,next指向下一个硬链。移除一个dentry,就把dentry从单链中拿掉。

如果硬链很多,硬链有性能问题,此时可以用双向链表。具体的操作就不示例了。


9 reference count

根据fuse的设计,inode引用计数,完全由内核持有。内核不需要的时候,我们才释放inode。删除文件不释放inode。


fuse不关心dentry,dentry存在的唯一目的,就是为了inode找到后备文件系统中对应的文件。因此dentry的引用计数由inode持有。

由于dentry是棵树,儿子也持有父亲的引用计数。当dentry的引用计数到达0的时候,释放dentry。


添加引用计数后,结构体变成:

struct inode {

    struct hlist_node hash;

    unsigned refcnt;

    uint64_t ino;

    struct dentry *dentry;

};


struct dentry {

    struct hlist_node hash;

    unsigned refcnt;

    char *name;

    struct dentry *parent;

    struct inode *inode;

    struct dentry *next;

};


引申

dentry不持有inode指针的利弊?


在第4节lookup中,我们分析,如果本次lookup的inode发生了变化,那么之前的inode的dentry应该设置为NULL。这样确保之前的

inode无法再访问后备文件系统的文件。


但是如果lookup发生inode变化,内核是不是会确保不再访问此inode呢?如果是这样的话,那么dentry是没有必要持有inode的指针。


另外删除文件,需要dentry持有inode指针,这样可以将dentry的inode的dentry指针置空,否则,可能访问空指针。


Copyright © linuxdev.cc 2017-2024. Some Rights Reserved.