MyFS

源码地址

源码

Fuse介绍

Fuse,全称为Filesystem in Userspace,即运行在用户空间上的文件系统。Fuse file-system deamon指的就是基于Fuse library开发的文件系统驱动,它将我们的目录挂载到/dev/fuse。之后,我们就可以通过目录挂载点的方式来访问这个文件系统。
说人话就是我们也许只需要一个c语言文件和一个h类函数文件,或者甚至只需要一个c语言文件就能完成一个自己设计的文件系统。

方案论证

使用FUSE创建自定义文件系统的过程包括以下步骤:

  1. 定义文件系统的特性和行为,如文件和目录的命名规则、权限管理、数据存储方式等。
  2. 编写文件系统驱动程序,实现FUSE提供的API,处理文件系统请求并执行相应的操作。
  3. 编译和运行文件系统驱动程序,并使用FUSE提供的工具将其挂载到指定的目录上,使其成为可访问的文件系统。
  4. 用户可以通过标准的文件操作命令(如lsmkdirecho等)操作自定义文件系统。

环境配置

  1. 安装ninja以及meson,从github下载libfuseclone的时候卡住了有可能是因为文件数过多导致的?
    1
    2
    3
    sudo apt-get install python3 python3-pip ninja-build  
    pip3 install --user meson
    git clone https://github.com/libfuse/libfuse.git
    如果在wsl环境下clone一直卡住,有可能是
  2. libfuse下新建文件夹build,切换到build文件夹,运行meson
    meson ..命令和ninja命令组合起来会生成一堆相关的构建产物
    1
    2
    3
    mkdir build; cd build  
    sudo apt install meson
    meson ..
  3. 接着我们使用ninja安装libfuse
    1
    2
    3
    4
    5
    6
    7
    8
    9
    ninja
    ```
    1. 进入**`build`下面的**`example`文件夹,
    ``` bash
    mkdir mountTest **创建临时文件夹用来测试**
    ls -al moutTest **展示文件夹的相关详细信息**
    ./hello mountTest **将hello文件系统安装到mountTest**
    ls -al mountTest
    cat mountTest/hello **输出Hello World!**
  4. 经过上面的测试,可以初步了解Fuse,同时可以参照hello.c来编写自己的文件系统

中间可能遇到的问题

  1. 问题如下 解决方案: sudo apt install libfuse3-dev

开工

相关结构

  1. 文件系统布局如下:
  2. 超级块的结构如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    struct SuperBlock {
    long fs_size; //文件系统的大小,以块为单位 8MB/512B=16384块

    long first_blk; //数据区的第一块块号为 518(从0开始),根目录也放在此
    long data_size; //数据区大小,最大以块为单位 16384-1-1-4-512=15818块
    long first_inode; //inode区起始块号为 6
    long inode_area_size; //inode区大小,以块为单位 512块
    long first_blk_of_inodebitmap; //inode位图区起始块号为 1
    long inode_bitmap_size; // inode位图区大小,以块为单位 1块
    long first_blk_of_databitmap; //数据块位图起始块号为 2
    long databitmap_size; //数据块位图大小,以块为单位 4块
    };
    需要注意的是:由于c中有字节对齐的特性,所以需要通过sizeof才能正确得到类型大小
  3. 目录项结构如下:
    目录项结构图:
    1
    2
    3
    4
    5
    6
    7
    // 目录项结构 根目录具有固定inode号:0
    struct DirTuple{ // 16字节
    char f_name[8]; // 文件名 最多为8字节
    char f_ext[3]; // 文件拓展名,最多为3字节
    unsigned short i_num; // inode号最多为2字节
    char spare[1]; // 备用:1字节
    };
  4. inode的结构如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // inode结构 由于c中有字节对齐的特性,所以需要通过sizeof才能正确得到类型大小
    struct Inode { // 64字节
    short int st_mode; // 前四位用作文件类型,后九位表示用户,组,其他的权限,中间三位表示特殊属性(suid,sgid,sticky)
    short int st_ino; // i-node号,2字节
    char st_nlink; // 连接数,1字节
    uid_t st_uid; // 拥有者的用户 ID ,4字节
    gid_t st_gid; // 拥有者的组 ID,4字节
    off_t st_size; // 文件大小,4字节
    struct timespec st_atim; // 16个字节time of last access
    short int addr [7]; // 磁盘地址,14字节 0-3直接地址,4一次间址,5二次间址,6三次间址
    char spare[9]; // 备用:9字节
    };

  5. 数据块的结构如下:
    1
    2
    3
    4
    // 数据块结构,大小为 512 bytes,占用1块磁盘块
    struct DataBlock {
    char data[BLOCK_SIZE];
    };

中间发生了什么?

  1. 介绍一下文件系统中查找某一文件的过程
    不能弄混的是目录文件和普通文件的数据块存的内容是完全不一样的,目录文件只存文件名与inode号的映射,普通文件只存文件内容
  2. 新建文件或目录所发生的事
  • 先确定用户对于欲新增文件的目录是否具有w与x的权限,若有的话才能新增
  • 根据inode位图找到没有使用的inode号,并将新文件的相关属性写入inode后修改inode位图
  • 根据数据块位图找到没有使用的数据块号,并将实际的数据写入数据块中后修改数据块位图,并更新inode信息中的arr数组

实现步骤

对磁盘进行初始化

  1. 初始化超级块:
    主要是填写文件系统的相关信息,first_blk用于表示数据块的块号(从0开始)为518,fisrt_inode用于表示第一个inode所在的块号为6,inode_area_size表示inode区有512块,结合inode类型字节大小为64,可以得知文件系统最多支持4096个inode即最多4096个文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 1.超级块初始化
    struct SuperBlock *super_blk = malloc(sizeof(struct SuperBlock));
    {
    super_blk->fs_size = 16384;
    super_blk->first_blk = 518;
    super_blk->data_size = 15818;
    super_blk->first_inode = 6;
    super_blk->inode_area_size = 512;
    super_blk->first_blk_of_inodebitmap = 1;
    super_blk->inode_bitmap_size = 1;
    super_blk->first_blk_of_databitmap = 2;
    super_blk->databitmap_size = 4;
    }

    fwrite(super_blk, sizeof(struct SuperBlock), 1, fp);
    printf("initiate superblock success!\n");
  2. 初始化inode位图
    首先通过fseek函数将fp移动到正确的位置上,然后由于文件系统中默认有根目录,占用第一个数据块与第一个inode,所以需要用1<<32表示第一个inode被占用,在此需要极其注意的是要使用unsigned int来赋值,读取位图也需要使用unsigned int,不然会遇到溢出错误,导致inode分配出错。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 2.初始化Inode位图
    if (fseek(fp, BLOCK_SIZE * 1, SEEK_SET) != 0) // 将指针移动到文件的第二块的起始位置
    fprintf(stderr, "bitmap fseek failed!\n");

    // 目前只有inum为0的inode被分配给根目录,inode位图占一个块,512字节= 128*4B
    unsigned int tmp_1 = 1<<31;
    fwrite(&tmp_1,sizeof(tmp_1),1,fp); // 写入位图的前32位,从左往右(下标从0开始)
    int tmp_arr_1[127];
    memset(tmp_arr_1,0,sizeof(tmp_arr_1)); fwrite(tmp_arr_1,sizeof(tmp_arr_1),1,fp); // 写入inode位图剩下的位,并全部初始为0

    printf("initiate Inode map success!\n");
  3. 初始化数据块位图:
    与初始化inode位图相似,仍然需要注意使用unsigned int类型
    1
    2
    3
    4
    5
    6
    // 3.初始化数据块位图
    // 目前只有数据区的第一个数据块被分配给根目录了
    unsigned int tmp_2 = 1<<31;
    fwrite(&tmp_2,sizeof(tmp_2),1,fp); // 写入数据块位图的前32位
    int tmp_arr_2[127 + 128 * 3];
    memset(tmp_arr_2,0,sizeof(tmp_arr_2)); fwrite(tmp_arr_2,sizeof(tmp_arr_2),1,fp); // 写入剩下的位
  4. 初始化inode区:
    初始化inode区所要做的事情主要就是写入根目录所占inode的信息,即第一个inode的信息,需要注意的是由于C语言不支持构造函数,只能显式初始化addr数组。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 4. 初始化inode区 大小:512块
    struct Inode inode_table[4096];
    for(int i = 0; i < 4096; i++)
    for(int j = 0; j < 7; j++)
    inode_table[i].addr[j] = -1;
    inode_table[0].st_mode = __S_IFDIR | 0755; // 此inode的st_mode说明为一个文件夹(目录)
    //printf("root dir's mode is %d\n",inode_table[0].st_mode); // 输出16877
    inode_table[0].st_ino = 0;
    inode_table[0].st_nlink = 1; // 根目录没有父文件夹,只有一个指向自身的目录项
    inode_table[0].st_uid = 579;
    inode_table[0].st_gid = 768288;
    inode_table[0].st_size = 16; // 刚开始只有一个指向自身的目录项,故占16字节
    clock_gettime(CLOCK_REALTIME, &inode_table[0].st_atim); // 获取从UTC时间1970年1月1日零时开始经过的秒数
    inode_table[0].addr[0] = 0; // 根目录的数据块号为0
    fwrite(inode_table,sizeof(inode_table),1,fp);
    printf("initiate inode success!\n");
  5. 初始化数据块区:
    初始化数据块区所要做的事情主要就是写入根目录,第一个数据块用作根目录,应只含有默认目录项,由于根目录没有父目录,所以我在这里只写入了”.”的目录项,并没有写入”..”的目录项。同时需要注意的是,在使用memcpy函数时,对于自定义结构的数组,最好不用使用+偏移量的方式,而应该使用&arr[index]的形式,前者的方式在偏移量较大时极其容易出错导致segment fault。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 5. 初始化数据块区大小:15818块
    // 先单独的把根目录写入
    struct DataBlock *tmp_db_1 = malloc(sizeof(struct DataBlock));
    struct DirTuple *tmp_dtuple = malloc(sizeof(struct DirTuple));
    strncpy(tmp_dtuple->f_name, ".", 8);
    memset(tmp_dtuple->f_ext, 0, sizeof(tmp_dtuple->f_ext));
    tmp_dtuple->i_num = 0;
    memset(tmp_dtuple->spare, 0, sizeof(tmp_dtuple->spare));
    memcpy(&tmp_db_1->data[0], tmp_dtuple, sizeof(struct DirTuple));

    fwrite(tmp_db_1, sizeof(struct DataBlock), 1, fp);
    free(tmp_db_1); free(tmp_dtuple);

    // 再初始化其他的数据块
    struct DataBlock *tmp_db_2 = malloc(sizeof(struct DataBlock));
    fwrite(tmp_db_2, sizeof(struct DataBlock), 15817, fp);
    free(tmp_db_2);
    fclose(fp);

    printf("initiate data block succes!\n");

实现文件系统操作

  1. .init调用:
    将超级块读入,并且考虑到只有4096个inode,所以使用全局变量inodes将所有的inode存入内存,方便读写修改,这样可以减少文件读写的操作。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    cfg->kernel_cache = 1; // 启用内核缓存,提高文件系统性能

    /* 处理与超级块有关的信息 */
    k_super_block = malloc(sizeof(struct SuperBlock));
    fread(k_super_block,sizeof(struct SuperBlock),1,fp);

    /* inode区的inode全部读入进inodes */
    if (fseek(fp, BLOCK_SIZE * 6, SEEK_SET) != 0) // 将指针移动到inode区的起始位置
    fprintf(stderr, "inodes fseek failed! (func: bugeater_init)\n");
    inodes = malloc(sizeof(struct Inode) * 4096);
    fread(inodes,sizeof(struct Inode),4096,fp);

    printf("bugeater_init() called successfully!\n");
    return NULL;
  2. .getattr调用:
    主要通过GetSingleDirTuple函数,根据path来获得目录项,然后将读取到的信息通过stbuf传递给FUSE。在本文件系统中支持多级目录结构。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    struct DirTuple *dir_tuple = malloc(sizeof(struct DirTuple));
    if(GetSingleDirTuple(path, dir_tuple) == 0)
    {
    // printf("dir_tuple->i_num is %d\n",dir_tuple->i_num);
    // printf("mode within path is %d\n",inodes[dir_tuple->i_num].st_mode);
    if(inodes[dir_tuple->i_num].st_mode & __S_IFDIR) // 是个目录
    {
    /* 目录必须给x权限,不然打不开 */
    stbuf->st_mode = __S_IFDIR | 0755; // 755表示owner拥有rwx权限,而group和其他只有rx权限
    stbuf->st_size = inodes[dir_tuple->i_num].st_size;
    stbuf->st_nlink = inodes[dir_tuple->i_num].st_nlink;
    stbuf->st_atime = inodes[dir_tuple->i_num].st_atim.tv_sec;
    printf("stbuf->st_atime is %ld\n", stbuf->st_atime);
    printf("it's a DIR\n");
    }
    else if(inodes[dir_tuple->i_num].st_mode & __S_IFREG) // 是个普通文件
    {
    stbuf->st_mode = __S_IFREG | 0666;
    stbuf->st_size = inodes[dir_tuple->i_num].st_size;
    stbuf->st_nlink = inodes[dir_tuple->i_num].st_nlink;
    stbuf->st_atime = inodes[dir_tuple->i_num].st_atim.tv_sec;
    printf("stbuf->st_atime is %ld\n", stbuf->st_atime);
    printf("it's a REG\n");
    }
    else
    {
    fprintf(stderr, "file type not recognized! (func: bugeater_getattr)\n");
    }
    }
    else
    {
    res = -ENOENT;
    printf("No such REG or DIR\n");
    }
    free(dir_tuple);
  3. .mknod和.mkdir调用:
    这两个调用都是通过同一函数create_file实现的。
  • create_file的函数参数为pathis_dirpath用于表示所要创建的对象的绝对路径,is_dir用于表示是否为目录文件。
  • 首先会调用GetMultiDirTuples来获取此目录下的所有目录项,然后检查要创建的文件是否已经存在,并且检查命名是否符合规范
  • 然后会根据是否为目录文件来分别调用DistributeIno函数,参数分别表示创建的文件的大小以及是否为目录文件,并且在DistributeIno函数中也会调用DistributeBlockNo函数用于分配数据块。
    1
    2
    3
    4
    5
    6
    // 可以确认regular或dir均不在父文件夹下
    int target_ino;
    if(is_dir)
    target_ino = DistributeIno(DEFAULT_DIR_SIZE, is_dir); // 分配inode号
    else
    target_ino = DistributeIno(0L, is_dir); // 分配inode号
  • 然后需要记住的是如果为目录文件,需要为其添加两个默认目录项。
  • 最后在父目录中添加目录项用于表示新建的文件,需要注意的是在父目录中添加目录项时,考虑需不需要为父目录新增数据块的问题。
    1
    2
    3
    4
    if(AddToParentDir(parent_ino, target, target_ino) != 0) // 添加一条记录到父目录中
    {
    fprintf(stderr, "something wrong when adding target to parent dir! (func: create_file)");
    }
  1. .unlink调用和 .rmdir调用:
    这两个调用都是通过同一函数remove_file实现的,在我实现的文件系统中采用了递归删除文件夹的方式,可以删除非空的文件夹。
  • remove_file的函数参数为parent_pathtargetis_dirparent_path用于表示所要删除的对象的父目录的绝对路径,target表示所要删除的对象的名字,is_dir用于表示是否为目录文件。
  • 首先会调用GetMultiDirTuples来获取此目录下的所有目录项,然后检查要创建的文件是否不存在。
  • 若为文件夹则需要进行递归删除,若为普通文件则调用DelSign函数来删除target在文件系统的所有痕迹即可,下图展示了为目录文件的情况:其中,new_parent_path表示当前文件夹的绝对路径,new_target表示当前文件夹下面的文件名字,new_is_dir对应该文件是否为目录文件。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    if(is_dir) // 文件夹的话需要递归删除
    {
    // printf("check4\n");
    int new_parent_path_len = strlen(parent_path) + strlen(target) + 2;
    char *new_parent_path = malloc(sizeof(char) * new_parent_path_len);
    strcpy(new_parent_path, parent_path);
    strcat(new_parent_path, target);
    strcat(new_parent_path, "/");
    // printf("new_parent_path is %s\n", new_parent_path);

    ssize_t new_count = inodes[target_ino].st_size / sizeof(struct DirTuple); // 表示target文件夹有多少个目录项
    struct DirTuple *new_tuples = malloc(sizeof(struct DirTuple) * new_count);
    new_tuples = GetMultiDirTuples(target_ino);

    // printf("递归删除target文件夹下的file\n");
    // 递归删除target文件夹下的file
    for(ssize_t new_i = 2; new_i < new_count; new_i++)
    {
    char new_target[16];
    strcpy(new_target, new_tuples[new_i].f_name);
    if (strlen(new_tuples[new_i].f_ext) != 0)
    {
    strcat(new_target, ".");
    strcat(new_target, new_tuples[new_i].f_ext);
    }
    bool new_is_dir = false;
    if(inodes[new_i].st_mode & __S_IFDIR) new_is_dir = true;
    if(remove_file(new_parent_path, new_target, new_is_dir) == RDNOEXISTS)
    {
    fprintf(stderr, "the new_target not found in new_parent_dir! (func: remove_file)");
    }
    }
    // printf("开始删target这个文件夹\n");

    // 开始删target这个文件夹
    DelSign(parent_ino, i, target_ino);
    // printf("check7\n");

    }
    else // 普通文件就不需要递归删除
    {
    DelSign(parent_ino, i, target_ino);
    }
  1. .write调用:
    主要通过write_file函数来实现
  2. .read调用:
    主要通过read_file函数来实现

结果分析

我在本次测试中加入了一些日常使用不到的极限测试,通过使用在source文件下的mkdir_1000.sh与echo_4000BYTES.sh可以分别测试创建1000个文件夹与写入4000字节的文件的正确性。接下来介绍如何进行测试:

  1. 依次使用以下命令
    1
    2
    3
    4
    5
    dd bs=8K count=1k if=/dev/zero of=diskimg
    make
    ./disk_init
    mkdir mountDir
    ./MyFS -f mountDir
  2. 新开终端进入到挂载目录下进行测试
  3. 下面主要测试多级目录,ls,mkdir,echo和cat命令
  4. 下面主要测试rmdir,unlink命令,可以得出本文件系统中的rmdir可以递归删除文件夹,而不一定需要时空文件夹才能删除。
  5. 下面进行我自己设置的极限测试,根据下图可以得出本文件系统可以进行较大量的文件读写与创建。

一些教训

  1. 之前不知道从哪看的,使用了access的API,每次cd到一个目录直接报错bash: cd: mountDir/: Numerical result out of range,最大的问题是它报错的不是权限相关,而是说溢出了???只需不定义access函数即可解决此问题
  2. 一个好的习惯:字符串数组建议都初始化一下,博主被一个因未初始化导致的乱码问题折磨了一天
  3. 对C语言中的细节有了更深的认知,比如关于C语言中的字节对齐特性,一开始我并不知道此特性,导致我编写完disk_init函数后,发现通过偏移字节获取我写入diskimg的信息是错误的,让我花了整整一个下午才发现了这个错误;还有关于strncpy函数,它的特性是当src字符串的长度大于n时,dest并不会在末尾添加’\0’,这就导致我在使用字符串的时候,有的时候会出现乱码导致无法匹配的问题;还有memcpy函数,如果是自定义的数据类型,最好不要使用+offset的形式来进行偏移,而应该通过&arr[index]的形式来偏移,前者在偏移量较大时会出现错误,此问题同样让我花了整整一天时间才钻研出来。

参考

[1] 知乎文章
[2] [鸟哥的Linux私房菜]
[3] CSDN博客


MyFS
http://bugeater.space/2023/11/30/MyFS/
Author
BugEater
Posted on
November 30, 2023
Licensed under