深入理解 FUSE:从 Hello World 到文件系统实现原理-01

深入理解 FUSE:从 Hello World 到文件系统实现原理-01

FUSE archlinuxcn wiki 用户空间中的文件系统 (Filesystem in Userspace) (FUSE) 是一种用于类 Unix 操作系统的机制,它使非特权用户可以创建自己的文件系统而无需编辑内核代码。这是通过在用户空间中运行文件系统代码来实现的,而 FUSE 内核模块仅提供了到实际内核接口的“桥梁”。

FUSE 是一个允许在用户空间实现文件系统的框架。这意味着你可以使用任何编程语言(虽然 C/C++ 是最常见的选择,因为 FUSE 库是用 C 编写的)来创建自己的文件系统,而无需深入内核编程的复杂性。这为各种有趣的应用场景打开了大门。

  • 将网络协议挂载为本地文件系统(如 SSHFS, NFS)。
  • 将数据库或云存储服务挂载为文件系统。(为了学习 JuiceFS 😢)

本文将从一个最简单的 FUSE 示例出发,深入剖析 FUSE 的工作原理、架构设计及其在现代操作系统中的应用,帮助读者不仅理解如何使用 FUSE,更能洞悉文件系统的本质。(这是第一篇,如果反响好就继续出)

fuse

结构设计

核心组件分析

创建了一个虚拟文件系统,其中只有一个名为 hello.txt 的只读文件,文件内容固定为"Hello World!\n"。这是一个教学用的最小 FUSE 实现,展示了 FUSE 的基本工作原理。

1.  文件系统结构

  • 根目录/
  • 一个文件/hello.txt,内容为"Hello World!\n"

2.  实现的文件系统操作

hello_getattr  函数

获取文件属性(类似于stat命令的功能):

  • 对于根目录/:设置为目录类型,权限 755
  • 对于/hello.txt:设置为普通文件,权限 444(只读),大小为字符串长度
  • 对于其他路径:返回"无此文件或目录"错误
hello_readdir  函数

读取目录内容(当用户执行ls命令时调用):

  • 只处理根目录/
  • 在目录中添加三项:.(当前目录),..(父目录)和hello.txt文件
  • 使用filler回调函数将目录项填充到 FUSE 提供的缓冲区中
hello_open  函数

打开文件操作:

  • 只允许打开/hello.txt
  • 只允许以只读模式打开(O_RDONLY),其他模式返回访问错误(EACCES)
hello_read  函数

读取文件内容:

  • 根据提供的偏移量(offset)和大小(size)读取hello_str字符串
  • 处理边界情况,确保不会读取超过字符串长度的内容
  • 返回实际读取的字节数

3.  操作函数表和主函数

fuse_operations结构体定义了文件系统支持的操作集合,本例中只实现了四个基本操作。

main函数调用fuse_main启动 FUSE 文件系统,传入命令行参数和操作函数表。

实践

  • 操作系统: Ubuntu 22.04
  • CC: gcc version 11.4.0 (Ubuntu 11.4.0-1ubuntu1~22.04)
  • CMAKE: cmake version 3.30.8

文件结构

1
2
3
CMakeLists.txt
hello
hello/main.c
1
sudo apt-get install libfuse-dev
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
cmake_minimum_required(VERSION 3.10)

project(fuse_lib_dev VERSION 1.0.0)

# 查找pkg-config工具
find_package(PkgConfig REQUIRED)

# 使用pkg-config查找FUSE库
pkg_check_modules(FUSE REQUIRED fuse)

# 添加可执行文件
add_executable(fuse_lib_dev
    hello/main.c
)

# 包含FUSE头文件目录
target_include_directories(fuse_lib_dev PRIVATE ${FUSE_INCLUDE_DIRS})

# 链接FUSE库
target_link_libraries(fuse_lib_dev ${FUSE_LIBRARIES})

# 添加FUSE相关编译选项
target_compile_options(fuse_lib_dev PRIVATE ${FUSE_CFLAGS_OTHER})
  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
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
#define FUSE_USE_VERSION 26

#include <errno.h>
#include <fcntl.h>
#include <fuse.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

// 文件内容
static const char* hello_str = "Hello World!\n";
static const char* hello_path = "/hello.txt";

/**
 *
 * @param path 识别用户当前请求操作的是哪个文件或目录
 * @param stbuf
 * @return
 */
static int hello_getattr(const char* path, struct stat* stbuf)
{
    int res = 0;


    memset(stbuf, 0, sizeof(struct stat));
    if (strcmp(path, "/") == 0)
    {
        stbuf->st_mode = S_IFDIR | 0755;
        stbuf->st_nlink = 2;
    }
    else if (strcmp(path, hello_path) == 0)
    {
        stbuf->st_mode = S_IFREG | 0444;
        stbuf->st_nlink = 1;
        stbuf->st_size = strlen(hello_str);
    }
    else
    {
        res = -ENOENT; // no such file or directory
    }

    return res;
}

/**
 * @brief
 * @param path 识别用户当前请求操作的是哪个文件或目录
 * @param buf FUSE内部分配的缓冲区,用于存储目录项信息
 * @param filler 用于向buf中添加目录项
 * @param offset 目录流中的偏移量, 用于大型目录的分页读取, 在简单实现中通常被忽略(如代码中的(void)offset)
 * @param fi 包含打开目录的信息, 可用于在多次调用间保持状态
 * @return
 */
static int hello_readdir(const char* path, void* buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info* fi)
{
    (void)offset;
    (void)fi;

    if (strcmp(path, "/") != 0)
        return -ENOENT;

    filler(buf, ".", NULL, 0); // 当前目录
    filler(buf, "..", NULL, 0); // 父目录
    filler(buf, hello_path + 1, NULL, 0); // hello.txt文件

    return 0;
}

/**
 * @brief
 * @param path 识别用户当前请求操作的是哪个文件或目录
 * @param fi
 * @return
 */
static int hello_open(const char* path, struct fuse_file_info* fi)
{
    if (strcmp(path, hello_path) != 0)
        return -ENOENT;

    if ((fi->flags & O_ACCMODE) != O_RDONLY)
        return -EACCES;

    return 0;
}

/**
 * @brief read the file
 * @param path 识别用户当前请求操作的是哪个文件或目录
 * @param buf
 * @param size
 * @param offset
 * @param fi
 * @return
 */
static int hello_read(const char* path, char* buf, size_t size, off_t offset, struct fuse_file_info* fi)
{
    (void)fi;

    if (strcmp(path, hello_path) != 0)
        return -ENOENT;

    const size_t len = strlen(hello_str);
    if (offset < len)
    {
        if (offset + size > len)
            size = len - offset;
        memcpy(buf, hello_str + offset, size);
    }
    else
    {
        size = 0;
    }

    return size;
}

/**
 * @brief
 */
static struct fuse_operations hello_oper = {
    .getattr = hello_getattr, //
    .readdir = hello_readdir,
    .open = hello_open,
    .read = hello_read,
};

int main(int argc, char* argv[])
{
    return fuse_main(argc, argv, &hello_oper, NULL);
}

编译后使用

 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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
yeisme@yeisme:~/code/fuse_lib_dev$ ./build/fuse_lib_dev --help
usage: ./build/fuse_lib_dev mountpoint [options]

general options:
    -o opt,[opt...]        mount options
    -h   --help            print help
    -V   --version         print version

FUSE options:
    -d   -o debug          enable debug output (implies -f)
    -f                     foreground operation
    -s                     disable multi-threaded operation

    -o allow_other         allow access to other users
    -o allow_root          allow access to root
    -o auto_unmount        auto unmount on process termination
    -o nonempty            allow mounts over non-empty file/dir
    -o default_permissions enable permission checking by kernel
    -o fsname=NAME         set filesystem name
    -o subtype=NAME        set filesystem type
    -o large_read          issue large read requests (2.4 only)
    -o max_read=N          set maximum size of read requests

    -o hard_remove         immediate removal (don't hide files)
    -o use_ino             let filesystem set inode numbers
    -o readdir_ino         try to fill in d_ino in readdir
    -o direct_io           use direct I/O
    -o kernel_cache        cache files in kernel
    -o [no]auto_cache      enable caching based on modification times (off)
    -o umask=M             set file permissions (octal)
    -o uid=N               set file owner
    -o gid=N               set file group
    -o entry_timeout=T     cache timeout for names (1.0s)
    -o negative_timeout=T  cache timeout for deleted names (0.0s)
    -o attr_timeout=T      cache timeout for attributes (1.0s)
    -o ac_attr_timeout=T   auto cache timeout for attributes (attr_timeout)
    -o noforget            never forget cached inodes
    -o remember=T          remember cached inodes for T seconds (0s)
    -o nopath              don't supply path if not necessary
    -o intr                allow requests to be interrupted
    -o intr_signal=NUM     signal to send on interrupt (10)
    -o modules=M1[:M2...]  names of modules to push onto filesystem stack

    -o max_write=N         set maximum size of write requests
    -o max_readahead=N     set maximum readahead
    -o max_background=N    set number of maximum background requests
    -o congestion_threshold=N  set kernel's congestion threshold
    -o async_read          perform reads asynchronously (default)
    -o sync_read           perform reads synchronously
    -o atomic_o_trunc      enable atomic open+truncate support
    -o big_writes          enable larger than 4kB writes
    -o no_remote_lock      disable remote file locking
    -o no_remote_flock     disable remote file locking (BSD)
    -o no_remote_posix_lock disable remove file locking (POSIX)
    -o [no_]splice_write   use splice to write to the fuse device
    -o [no_]splice_move    move data while splicing to the fuse device
    -o [no_]splice_read    use splice to read from the fuse device

Module options:

[iconv]
    -o from_code=CHARSET   original encoding of file names (default: UTF-8)
    -o to_code=CHARSET      new encoding of the file names (default: UTF-8)

[subdir]
    -o subdir=DIR           prepend this directory to all paths (mandatory)
    -o [no]rellinks         transform absolute symlinks to relative
cli
Licensed under CC BY-NC-SA 4.0
comments powered by Disqus