【Linux】分析Fuse中libfuse源码

news2024/11/26 19:40:11

在Linux中,我们可以使用FUSE来进行自定义用户态文件系统的实现。编译example中的示例是学习FUSE的第一步,本文侧重于剖析FUSEclient端的源码。

文章目录

    • (一) 下载libfuse源码,避免重复造轮子
    • (二) 解读与分析meson构建脚本,便于转化为CMakeLists脚本
      • 1. 理解meson基本语法
        • meson.get_compiler
        • project, library
        • configuration_data, configure_file
        • add_project_arguments
        • include_directories
        • dependency
      • 2. 拆解meson构建脚本
    • (三) 理解versionscript与symver,为解读源码做准备
      • 1. versionscript简介
      • 2. 使用nm命令查看函数的symver
    • (四) 结合NDK与CMake交叉编译,进入Android的fuse世界
      • 1. NDK中移除了pthread_setcancelstate与pthread_cancel
        • NDK为什么要移除上述两个函数
        • 如何进行类似的行为平替
      • 2. 移植Linux端生成的config.h并进行改写
    • (五) 从fuse_main()开始,一步一步分析libfuse组成
      • 1. libfuse的进程与线程结构
      • 2. 自定义文件操作接口的工作流水线
      • 3. /dev/fuse扮演的角色

(一) 下载libfuse源码,避免重复造轮子

Linux环境我们选择Ubuntu,方便我们查看一些Linux的相关手册。在准备接触FUSE之前,我们可以先上网搜索一下,或者你也可以快速翻一下手册,执行man 8 fuse,在手册里我们可以看到好几个名词概念,例如FUSEfilesystemlibfusefilesystem ownerclient

man fuse
至此,我们正式在手册里看到了libfuse的身影,然后我们去github上拉取源码,地址为 https://github.com/libfuse/libfuse 。截止该文完成,该库在github上最新版本为3.14。不过在查看Release Notes后,我们依然选择采用3.12版本进行源码分析。

libfuse_github_page
我们拉取代码至本地,git操作不再赘述。
接下来我们查看ReadMe文档,可以看到该工程依赖于meson构建工具,而不是依赖于我们熟悉的CMake,因此下一步,我们需要解读meson构建脚本,即各个meson.build

(二) 解读与分析meson构建脚本,便于转化为CMakeLists脚本

1. 理解meson基本语法

初识meson,我们依旧选择最官方的文档。进入meson官网。如下图,在网页左侧的导航栏中,我们可以先读一下简介、教程与示例,并大致浏览一下一些meson里的约定内容。

meson page
此处介绍几个比较重要的函数方法,后续分析meson脚本用得上。

meson.get_compiler

示例:

cc = meson.get_compiler('c')

获取当前机器的C编译器,可进行编译参数配置。

project, library

示例:

project('libfuse3', ['c'], version: '3.12.0',
        meson_version: '>= 0.42',
        default_options: [
            'buildtype=debugoptimized',
            'cpp_std=c++11',
            'warning_level=2',
        ])
        
libfuse = library('fuse3', libfuse_sources, version:meson.project_version(), soversion: '3',                           include_directories: include_dirs, dependencies: deps, 
                  install: true, link_depends: 'fuse_versionscript', 
                  c_args: [ '-DFUSE_USE_VERSION=312',
                            '-DFUSERMOUNT_DIR="@0@"'.format(fusermount_path) ],
                  link_args: ['-Wl,--version-script,' + meson.current_source_dir()
                              + '/fuse_versionscript' ])

*project()*定义了该工程的一些基本信息,*library()*定义了预构建的库的一些信息,包括编译参数、链接参数等。

configuration_data, configure_file

示例:

cfg = configuration_data()
cfg.set_quoted('PACKAGE_VERSION', meson.project_version())
configure_file(output: 'config.h',
               configuration : cfg)

*configuration_data()创建一个可配置对象,用以配置一些宏定义,最后使用configure_file()*输出为config.h,我们的libfuse中需要其发挥全局定义的作用。如下为构建自动生成的内容。

// config.h
#define PACKAGE_VERSION "3.12.0"

add_project_arguments

示例:

add_project_arguments('-D_REENTRANT', '-DHAVE_CONFIG_H', '-Wno-sign-compare',
                      '-Wstrict-prototypes', '-Wmissing-declarations', '-Wwrite-strings',
                      '-fno-strict-aliasing', language: 'c')
add_project_arguments('-D_REENTRANT', '-DHAVE_CONFIG_H', '-D_GNU_SOURCE',
                     '-Wno-sign-compare', '-Wmissing-declarations',
                     '-Wwrite-strings', '-fno-strict-aliasing', language: 'cpp')

*add_project_arguments()*用以配置工程对不同编程语言的参数。

include_directories

示例:

include_dirs = include_directories('include', 'lib', '.')

*include_directories()*用以生成上下文可用的include目录。

dependency

示例:

thread_dep = dependency('threads')

*dependency()*用以对外部库进行依赖。

2. 拆解meson构建脚本

在libfuse工程中,我们通过meson结合ninja构建工具可以得到最终的构建产物,分别是libfuse3.sofusermount3fuse相关头文件
现在我们出于将该工程移植到Android项目中的目的,未来利用NDK搭配CMake来进行编译,于是需要拆解meson构建脚本,我们通过查阅meson官网上的文档,将多个meson脚本进行翻译。
乍一看,可能会觉得有点头疼,毕竟这里也有好一些构建脚本,静下心来,读懂它后就可以比较轻松地翻译为CMakeLists.txt文件了。
此处不进行通篇翻译,过于冗余,仅结合前文介绍几个片段作为参考示例。

libfuse = library('fuse3', libfuse_sources, version: meson.project_version(),
                  soversion: '3', include_directories: include_dirs,
                  dependencies: deps, install: true,
                  link_depends: 'fuse_versionscript',
                  c_args: [ '-DFUSE_USE_VERSION=312',
                            '-DFUSERMOUNT_DIR="@0@"'.format(fusermount_path) ],
                  link_args: ['-Wl,--version-script,' + meson.current_source_dir()
                              + '/fuse_versionscript' ])

pkg = import('pkgconfig')
pkg.generate(libraries: [ libfuse, '-lpthread' ],
             libraries_private: '-ldl',
             version: meson.project_version(),
             name: 'fuse3',
             description: 'Filesystem in Userspace',
             subdirs: 'fuse3')

正如前文所说,*library()*是用来创建目标产物库的,这段代码的意思是配置了库的相关头文件,源文件,链接依赖文件,C编译器参数,链接参数,我们可以等价地将其翻译为如下的CMake脚本语句。

add_library(fuse3

        SHARED

        fuse.c
        fuse_i.h
        fuse_loop.c
        fuse_loop_mt.c
        fuse_lowlevel.c
        fuse_misc.h
        fuse_opt.c
        fuse_signals.c
        buffer.c
        cuse_lowlevel.c
        helper.c
        modules/subdir.c
        mount_util.c
        fuse_log.c
        mount.c
        modules/iconv.c)
        
set_target_properties(fuse3
        PROPERTIES LINK_DEPENDS ${CMAKE_SOURCE_DIR}/fuse_versionscript
        LINK_FLAGS -Wl,--version-script,${CMAKE_SOURCE_DIR}/fuse_versionscript)

target_link_libraries(fuse3

        log
        dl)

如果你对于为什么我们没有显式链接libpthread库,那么可以参考这篇官网文档,可以帮助你了解在Android NDK中应该显式链接哪些需要的库。

(三) 理解versionscript与symver,为解读源码做准备

1. versionscript简介

在前文中可以看到,我们在编译时配置了链接依赖文件fuse_versionscript,同时也配置了--version-script参数。你如果知道这个知识点,那么可以跳过第三节,直接阅读下一节。否则的话,可以在此稍作停留,让我们补一补versionscript的知识。
versionscript是一个在链接器中的概念,意在给各个函数方法加上版本信息,这样做的用处就是可以指定某个函数方法的不同版本的实现。在源代码中,我们可以这样声明一个函数原型,跟普通的函数声明没有区别,然后我们可以进行对该函数的实现。
例如,在libfuse中有这样一个函数原型:

int fuse_parse_cmdline(struct fuse_args *args,
                       struct fuse_cmdline_opts *opts);

这时候你直接想在源码中进行定义的跳转时会发现,这失败了。我们的IDE并没有找到这个函数的定义,但是这又是可以被编译的,为什么呢?如果你直接查看汇编代码,那么你可以找到答案。不过我相信聪明的你会有正确的直觉,这是不是跟前面说的fuse_versionscript文件有关系。
我们打开这个文件,你可以看到(这里我们节选了一部分,只有3.12版本新增的这部分):

FUSE_3.12 {
	global:
		fuse_session_loop_mt;
		fuse_session_loop_mt_312;
		fuse_loop_mt;
		fuse_loop_mt_32;
		fuse_loop_mt_312;
		fuse_loop_cfg_create;
		fuse_loop_cfg_destroy;
		fuse_loop_cfg_set_idle_threads;
		fuse_loop_cfg_set_max_threads;
		fuse_loop_cfg_set_clone_fd;
		fuse_loop_cfg_convert;
		fuse_parse_cmdline;
		fuse_parse_cmdline_30;
		fuse_parse_cmdline_312;
} FUSE_3.4;

我们可以看到以fuse_parse_cmdline开头的函数还有fuse_parse_cmdline_30fuse_parse_cmdline_312这两个,似乎有一定的眉目了。我们看一下这个函数的声明与定义。

int fuse_parse_cmdline_312(struct fuse_args *args,
                           struct fuse_cmdline_opts *opts);

FUSE_SYMVER("fuse_parse_cmdline_312", "fuse_parse_cmdline@@FUSE_3.12")
int fuse_parse_cmdline_312(struct fuse_args *args,
                           struct fuse_cmdline_opts *opts) { /* ... */ }
                           
int fuse_parse_cmdline_30(struct fuse_args *args,
                          struct fuse_cmdline_opts *opts);

FUSE_SYMVER("fuse_parse_cmdline_30", "fuse_parse_cmdline@FUSE_3.0")
int fuse_parse_cmdline_30(struct fuse_args *args,
                          struct fuse_cmdline_opts *opts) { /* ... */ }

我们看到这两个函数定义与声明之间都有一个宏:

#define FUSE_SYMVER(sym1, sym2) __asm__("\t.symver " sym1 "," sym2);

我们可以看到调用了汇编函数,并指定了.symver,这其实就是符号版本。同时我们注意到,这两个宏调用中都有fuse_parse_cmdline的身影,但是@的数量不同。简单理解的话,@@就是当前默认使用的函数定义的意思,而@是非默认的意思。至于如何在链接时使用指定版本的函数定义,希望读者可以去自行探究。

2. 使用nm命令查看函数的symver

接下来我们在Linux环境中通过nm命令查看一下so库中的各个函数的symver信息。

nm -D /usr/local/lib/x86_64-linux-gnu/libfuse3.so

可以看到下面这些信息(仅节选部分):
nm -D libfuse3.so
我们看到fuse_parse_cmdline@@FUSE_3.12的地址指向01e8d0,而fuse_parse_cmdline_312@@FUSE_3.12的地址也指向该位置。因此得以确定,我们在实现中调用fuse_parse_cmdline函数时,实际的调用目标是fuse_parse_cmdline_312函数。

(四) 结合NDK与CMake交叉编译,进入Android的fuse世界

1. NDK中移除了pthread_setcancelstate与pthread_cancel

在NDK中,我们经常可以看到有很多的库函数受限于Android的API版本,这意味着高版本受限的API必须满足工程的minSdkVersion ≥ 受限API版本的情况下才可以被编译,否则编译器会拒绝编译。比如NDK21.4.7075529的pthread.h中定义了以下一些版本受限的函数(仅节选):

#if __ANDROID_API__ >= __ANDROID_API_N__
int pthread_barrierattr_init(pthread_barrierattr_t* __attr) __INTRODUCED_IN(24);
int pthread_barrierattr_destroy(pthread_barrierattr_t* __attr) __INTRODUCED_IN(24);
int pthread_barrierattr_getpshared(const pthread_barrierattr_t* __attr, int* __shared) __INTRODUCED_IN(24);
int pthread_barrierattr_setpshared(pthread_barrierattr_t* __attr, int __shared) __INTRODUCED_IN(24);
#endif

#if __ANDROID_API__ >= 26
int pthread_getname_np(pthread_t __pthread, char* __buf, size_t __n) __INTRODUCED_IN(26);
#endif /* __ANDROID_API__ >= 26 */

#if __ANDROID_API__ >= 28
int pthread_setschedprio(pthread_t __pthread, int __priority) __INTRODUCED_IN(28);
#endif /* __ANDROID_API__ >= 28 */

我们可以看到有的是API24(7.0),API26(8.0)等等之类的限定介绍。

NDK为什么要移除上述两个函数

问题回到pthread_setcancelstatepthread_cancel两个函数为什么被Google从NDK中移除,这个问题的原因之一是线程被标记结束后不一定会把自己拥有的资源释放掉,甚至不一定会结束,因此很可能造成内存泄露或死锁等问题,而这些问题在移动设备上更加突出。我们通过查看Linux上的pthread_cancel的man page可以看到,该库函数内部实际使用了信号作为实现手段。然而在libfuse的实现中我们可以看到里面调用到了这两个函数,那么下一个问题就是,如何自定义实现这两个函数的行为。

如何进行类似的行为平替

通过查找与搜索,不难找到网上说的使用pthread_kill函数来进行代替实现。
那么我们也对该方案进行了采纳,目的是先让我们的工程通过NDK环境的编译,并成功运行起来。以下是我们定义的pthread_setcancelstatepthread_cancel的行为。

#define SIG_CANCEL_SIGNAL SIGUSR1
#define PTHREAD_CANCEL_ENABLE 1
#define PTHREAD_CANCEL_DISABLE 0

static int pthread_setcancelstate(int state, int *oldstate) {
    sigset_t new, old;
    int ret;
    sigemptyset(&new);
    sigaddset(&new, SIG_CANCEL_SIGNAL);

    ret = pthread_sigmask(state == PTHREAD_CANCEL_ENABLE ? SIG_BLOCK : SIG_UNBLOCK, &new, &old);
    if (oldstate != NULL) {
        *oldstate = sigismember(&old, SIG_CANCEL_SIGNAL) == 0 ? PTHREAD_CANCEL_DISABLE
                                                              : PTHREAD_CANCEL_ENABLE;
    }

    return ret;
}

static inline int pthread_cancel(pthread_t thread) {
    return pthread_kill(thread, SIG_CANCEL_SIGNAL);
}

2. 移植Linux端生成的config.h并进行改写

让我们重新回到meson.build构建脚本,我们看一看原来它在Ubuntu-Linux操作系统上会生成什么产物,再看看它在Android-Linux操作系统上应该生成什么。我们通过分析构建脚本,可以看到里面有相当一部分用于测试当前机器环境是否具有相应的目标函数能力的代码:

# Test for presence of some functions
test_funcs = [ 'fork', 'fstatat', 'openat', 'readlinkat', 'pipe2',
               'splice', 'vmsplice', 'posix_fallocate', 'fdatasync',
               'utimensat', 'copy_file_range', 'fallocate' ]
foreach func : test_funcs
    cfg.set('HAVE_' + func.to_upper(),
        cc.has_function(func, prefix: include_default, args: args_default))
endforeach
cfg.set('HAVE_SETXATTR', 
        cc.has_function('setxattr', prefix: '#include <sys/xattr.h>'))
cfg.set('HAVE_ICONV', 
        cc.has_function('iconv', prefix: '#include <iconv.h>'))

# Test if structs have specific member
cfg.set('HAVE_STRUCT_STAT_ST_ATIM',
         cc.has_member('struct stat', 'st_atim',
                       prefix: include_default,
                       args: args_default))
cfg.set('HAVE_STRUCT_STAT_ST_ATIMESPEC',
         cc.has_member('struct stat', 'st_atimespec',
                       prefix: include_default,
                       args: args_default))

这些测试内容最终会反映到config.h文件中去,同理,我们可以在NDK中进行手动测试并写出一致的config.h文件(仅部分):

#define HAVE_SPLICE

#define HAVE_STRUCT_STAT_ST_ATIM

#undef HAVE_STRUCT_STAT_ST_ATIMESPEC

#define HAVE_UTIMENSAT

#define HAVE_VMSPLICE

至此我们已经准备好了大部分NDK环境下的编译准备,其余可能还涉及一些内部的常量定义,读者可自行根据报错来修复,此处不再赘述。
我们通过将hello程序引入进来,并在CMakeLists.txt中进行简要配置即可在assemble后生成Android环境下的可执行文件(AS的输出目录为 <module>/build/intermediates/cmake/<debug/release>/obj/<abi>/下):

add_executable(hello

        hello.c)
        
target_link_libraries(
        hello

        fuse3)

为了后续的执行,我们将这些成果物adb push到以下Android设备系统路径中去:

adb push <your path>\build\intermediates\cmake\debug\obj\armeabi-v7a\libfuse3.so /system/lib

adb push <your path>\build\intermediates\cmake\debug\obj\armeabi-v7a\hello /system/bin

(五) 从fuse_main()开始,一步一步分析libfuse组成

1. libfuse的进程与线程结构

现在源码有了,环境有了,产生成果物的脚本也有了,是时候分析libfuse的工作原理了。首先,我们来选择在哪个环境下分析源码,是在Ubuntu中呢,还是在Android中呢。其次,我们选择如何分析源码,是通过断点调试呢,还是通过打印日志信息呢,或者是打印方法调用栈呢。这些你都可以进行选择,不过有一些遗憾的是,在Android NDK中没有你可能需要的execinfo.h,因此如果想打印方法调用栈,需要另寻他法,此处不赘述,读者自行去查。

在libfuse官方推荐的经典示例hello程序中,我们可以在其源码中看到其主要调用了fuse_main()函数,查看对应的源码,我们来到了helper.c文件下的fuse_main_real()函数,而这,就是整个libfuse的主要入口。

int fuse_main_real(int argc, char *argv[], const struct fuse_operations *op,
                   size_t op_size, void *user_data) {
    struct fuse_args args = FUSE_ARGS_INIT(argc, argv);
    struct fuse *fuse;
    struct fuse_cmdline_opts opts;
    struct fuse_loop_config *loop_config = NULL;

    if (fuse_parse_cmdline_312(&args, &opts) != 0) {	// 1 parse args
        return 1;
    }

    fuse = fuse_new_31(&args, op, op_size, user_data);	// 2 create fuse, fuse_session

    if (fuse_mount(fuse, opts.mountpoint) != 0) {	// 3 mount path to fs
		// ......
    }

    if (fuse_daemonize(opts.foreground) != 0) {	// 4 default: fork & exit current process
		// ......
    }

    struct fuse_session *se = fuse_get_session(fuse);
    if (fuse_set_signal_handlers(se) != 0) {	// 5 set signal handlers
		// ......
    }

    loop_config = fuse_loop_cfg_create();
	// ......
    res = fuse_loop_mt_312(fuse, loop_config);	// 6 loop to process

    fuse_remove_signal_handlers(se);	// 7 remove signal handlers
out3:
    fuse_unmount(fuse);	// 8 use fusermount3
out2:
    fuse_destroy(fuse); // 9 release fuse resource
out1:
    fuse_loop_cfg_destroy(loop_config);
    free(opts.mountpoint);
    fuse_opt_free_args(&args);
    return res;
}

在上述代码段中,我们已经将其中重要的步骤做了1234各种标记,里面最关键的是步骤6,前5个步骤可以我们将其归为“初始化动作”,步骤6归为“循环处理工作”,后几个步骤归为“资源释放动作”。
默认情况下,hello程序会在步骤4中进行fork()操作,这会开启一个新的子进程,并退出主进程。你可能会发现你在hello.c的很多函数实现里加了printf()打印却没有任何信息显示,其实这就是在步骤4中进行了fd的重定义,将stdin(0),stdout(1),stderr(2)与/dev/null特殊设备相关联,如果你希望接下来能看到打印,那么将下面这段话注释掉即可:

nullfd = open("/dev/null", O_RDWR, 0);
if (nullfd != -1) {
    (void) dup2(nullfd, 0);
    (void) dup2(nullfd, 1);
    (void) dup2(nullfd, 2);
    if (nullfd > 2) {
        close(nullfd);
    }
}

我们对fuse的结构体进行入手,可以逐渐展开一张结构体的关联图(仅展示部分,读者可以自行绘制):

libfuse struct
有了结构体关联图,我们在读源码的过程中,便可以像调试程序一样一边跟踪程序执行中各变量的内容,一边剖析程序的状态机模型。
在“初始化动作”阶段,我们主要关注我们的自定义函数实现去了哪里。这个不复杂,稍作代码跟踪,我们就可以看到最后我们实现的系列函数被copy去了fuse_fsstruct fuse_operations op字段中。

    memcpy(&fs->op, op, op_size);

另一个在“初始化动作”阶段要注意的是,我们的一系列mount动作,最终会打开/dev/fuse设备,并通过该设备进行读写(消息转发)。

fuse_mount
--->fuse_session_mount
	--->fuse_kern_mount
		--->fuse_mount_sys
			--->1. open("/dev/fuse", ...)
			--->2. mount

现在我们重点来看步骤6,源码中这里是写了fuse_loop_mt(),不过为了方便跳转,结合前文介绍的symver,我们可以直接将其改为fuse_loop_mt_312()函数调用,没有影响,更方便在IDE中跳转。
fuse_loop_mt_312函数中,我们可以其实际关键的调用了fuse_session_loop_mt_312,我们可以展示该函数最近的调用栈:

fuse_loop_mt_312
--->fuse_session_loop_mt_312
	--->fuse_loop_start_thread
		--->fuse_start_thread
			--->pthread_create

结合源码,可以看到fuse_loop_start_thread这里创建了一个用于执行fuse_do_work()的线程。在fuse_do_work()我们终于见到了久违的while循环体。

while (!fuse_session_exited(mt->se)) {
    // ... 
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
    res = fuse_session_receive_buf_int(mt->se, &w->fbuf, w->ch); // 1 read request data from /dev/fuse
    pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
    // ...
    pthread_mutex_lock(&mt->lock);
    if (mt->numavail == 0 && mt->numworker < mt->max_threads) {
        fuse_loop_start_thread(mt);	// 2 create work thread when too busy
    }
    pthread_mutex_unlock(&mt->lock);
    // ...
    fuse_session_process_buf_int(mt->se, &w->fbuf, w->ch);	// 3 handle request data & response
}

工作循环内的主要调用还是比较好理解的,步骤1接收数据,步骤3处理数据并回复。步骤2就是在工作线程均繁忙的时候创建新的工作线程去处理。

2. 自定义文件操作接口的工作流水线

前文已经分析出了在事件循环中我们会读取请求数据,随后处理请求数据并回复。
在请求数据中,其实libufse中的实现很简单,就是从一个fd中读取预期缓冲大小的数据(实际并不会读取这么多数据):

    res = read(ch ? ch->fd : se->fd, buf->mem, se->bufsize);

这里有一个问题,这个预期大小会是多少byte。其实在默认情况下,初次init时大小为<value1>,后续为<value2>,都与pagesize()有关。具体的value1与value2是多少,留给读者去探索。
我们以在console中进入mountpoint所在的父路径为例,例如mountpoint/sdcard/Test,那么现在我们执行ls /sdcard命令来触发自定义文件系统的相关函数的调用。可以在刚才执行/system/bin/hello /sdcard/Test的console中看到有一些打印(这里假设我们自己加上去了一些用于查看关键信息的打印,非libfuse源码自带)。
process req

我们将其转换为函数调用栈:

fuse_session_process_buf_int
--->do_getattr // fuse_ll_ops[opcode].func
	--->fuse_lib_getattr // session->op.getattr
		--->fuse_fs_getattr
			--->hello_getattr	// fuse_fs->op.getattr

我们可以在几段代码中找到很多答案(结合前面的结构体关联图分析更直观):

enum fuse_opcode {	// protocol definition.
    FUSE_LOOKUP = 1,
    FUSE_FORGET = 2,  /* no reply */
    FUSE_GETATTR = 3,
    FUSE_SETATTR = 4,
    // ......
}

static struct {
    void (*func)(fuse_req_t, fuse_ino_t, const void *);

    const char *name;
} fuse_ll_ops[] = {
        [FUSE_LOOKUP]       = {do_lookup, "LOOKUP"},
        [FUSE_FORGET]       = {do_forget, "FORGET"},
        [FUSE_GETATTR]       = {do_getattr, "GETATTR"},	// FUSE_GETATTR = 3
        [FUSE_SETATTR]       = {do_setattr, "SETATTR"},
        // ......
}

static struct fuse_lowlevel_ops fuse_path_ops = {
        .init = fuse_lib_init,
        .destroy = fuse_lib_destroy,
        .lookup = fuse_lib_lookup,
        .forget = fuse_lib_forget,
        .forget_multi = fuse_lib_forget_multi,
        .getattr = fuse_lib_getattr,
        .setattr = fuse_lib_setattr,
        // ......
}

至此,我们便完全走通了libfuse的工作循环。我们的这些自定义文件系统涉及到的函数正是通过上述这些调用途径被执行。
数据处理完后,下一步就是回复响应。这一步的函数调用链为:

fuse_session_process_buf_int
--->do_getattr // fuse_ll_ops[opcode].func
	--->fuse_lib_getattr // session->op.getattr
		--->1. fuse_fs_getattr // has done.
		--->2. fuse_reply_attr
			--->send_reply_ok
				--->send_reply
					--->send_reply_iov
						--->fuse_send_reply_iov_nofree
							--->fuse_send_msg
								--->writev

我们可以看到,最终也是调用熟悉的writev函数完成写操作。其实整个libfuse中,最迷的是很多地方的void*指针,不过这也是C语言的通病了,耦合降低的同时带来的却是可读性极差。如果还需要读懂来龙去脉,那还需要研读Linux源码中的FUSE模块,虽然这本质上就是一套数据交换协议,所以搞清楚了实质以后也没有什么神奇的事情。
若我们需要卸载mountpoint,则需要采用fusermount3工具,执行fusermount3 -u <mountpoint>即可,这会触发hello子进程的退出。
下表是在剖析libfuse源码中,罗列出其涉及到的一些库函数及系统调用(仅展示部分),有些可能是你常见的,有些可能是你不常见的,读者可以借此机会重温一下这些函数是否都熟悉:

函数名函数功能
pipe创建管道
realpath获取真实路径
_exit不会执行on_exit和atexit中注册的清理函数
setsid创建一个session并设置进程组id
dup2重定向oldfd至newfd
mount挂载fs节点
umount2卸载fs节点
poll等待事件
chdir切换工作目录
pthread相关多线程与同步机制
signal相关信号机制
process相关多进程
semaphore相关信号量
dl相关动态加载库
io相关I/O操作

3. /dev/fuse扮演的角色

到此为止,其实我们已经拆解了大部分libfuse的工作原理,首先我们可以确定其基于C/S架构,内核端的Fuse其实就是Server,它会将VFS给它的文件系统操作函数进行消息封装,转化为请求数据并发送至/dev/fuse设备。因此整个工作模型就像下图:
work model
至此,一个Linux上的用户态文件系统的client端的一套官方开源代码库就暂时剖析结束了,里面涉及到的参数解析、初始化、异常处理、同步处理的细节留给需要的读者去慢慢体会,这里不再展开。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/621167.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

什么是WePY?

WePY&#xff08;微信小程序开发框架&#xff09;是一个基于组件化开发思想的微信小程序开发框架。它类似于Vue.js框架&#xff0c;通过封装小程序原生的API&#xff0c;提供了更加简洁、高效的开发方式。 WePY的主要特点包括&#xff1a; 组件化开发&#xff1a;WePY将页面拆…

分布式锁原理与实战二:公平锁和可重入锁的原理

在单体的应用开发场景中&#xff0c;涉及并发同步的时候&#xff0c;大家往往采用synchronized 或者 Lock 的方式来解决多线程间的同步问题。但在分布式集群工作的开发场景中&#xff0c;那么就需要一种更加高级的锁机制&#xff0c;来处理种跨JVM 进程之间的数据同步问题&…

论证有效性写作模板

析错口诀&#xff1a; 1.概念不明确&#xff0c;我就说它概念模糊&#xff0c;并做不利它的解释。【有概念模糊之嫌&#xff0c;A是理解1&#xff1f;还是理解2&#xff1f;】 2.概念有变换&#xff0c;我就说它混淆概念&#xff0c;并指出混淆的环节。&#xff08;概念推概念&…

华为OD机试真题 Java 实现【数列描述】【2023 B卷 100分】,附详细解题思路

一、题目描述 有一个数列a[N] (N60)&#xff0c;从a[0]开始&#xff0c;每一项都是一个数字。数列中a[n1]都是a[n]的描述。其中a[0]1。 规则如下&#xff1a; a[0]:1 a[1]:11(含义&#xff1a;其前一项a[0]1是1个1&#xff0c;即“11”。表示a[0]从左到右&#xff0c;连续出…

【数据结构】图的定义、存储

对王道数据结构选择题做错和不清楚的题的简单纠错 图的定义 一个有n个顶点和n条边的无向图一定是有环的 一个无向图有n个顶点和n-1条边&#xff0c;可以使它连通单没有环&#xff0c;若再加一条边&#xff0c;则会形成环 若图中顶点数为n&#xff0c;则它的生成树有n-1条边&am…

网际互联及OSI七层模型:

网际互联及OSI七层模型&#xff1a; 物理层、数据链路层、网络层、传输层、会话层、表示层、应用层 物理层 作用&#xff1a;定义一些电器&#xff0c;机械&#xff0c;过程和规范&#xff0c;如集线器&#xff1b; PDU(协议数据单元)&#xff1a;bit/比特 设备&#xff…

分布式锁原理与实战三:ZooKeeper分布式锁的原理

目录 ZooKeeper分布式锁的原理 ZooKeeper的每一个节点&#xff0c;都是一个天然的顺序发号器。 ZooKeeper节点的递增有序性&#xff0c;可以确保锁的公平 ZooKeeper的节点监听机制&#xff0c;可以保障占有锁的传递有序而且高效 ZooKeeper的节点监听机制&#xff0c;能避免羊群…

STM32开发——定时器led、PWM呼吸灯

目录 1.定时器控制LED亮灭 2.PWM呼吸灯——定时器 1.定时器控制LED亮灭 定时器分类&#xff1a; 基本定时器&#xff08;TIM6~TIM7&#xff09; 通用定时器&#xff08;TIM2~TIM5&#xff09; 高级定时器&#xff08;TIM1和TIM8&#xff09; 通用定时器介绍&#xff1a; 16 …

jenkins接口自动化测试,allure报告怎么清楚上一个项目的用例报告?

持续集成(CI Continuous integration/CD Continuous Deployment)的含义其实是在研发过程中团队开发成员持续性的将他们的工作集成到一个完整流程中&#xff0c;通常每个成员每天至少集成一次&#xff0c;每次集成都通过自动化的构建&#xff08;包括编译&#xff0c;发布&#…

【数据分享】1929-2022年全球站点的逐月平均降水量(Shp\Excel\12000个站点)

气象数据是在各项研究中都经常使用的数据&#xff0c;气象指标包括气温、风速、降水、湿度等指标&#xff0c;说到常用的降水数据&#xff0c;最详细的降水数据是具体到气象监测站点的降水数据&#xff01; 之前我们分享过1929-2022年全球气象站点的逐月平均气温、逐月最高气温…

【Windows】解决无线网卡TL-WN823N免驱版无法上网的问题

【Windows】解决无线网卡TL-WN823N免驱版无法上网的问题 1、背景2、解决办法3、查看适配器 1、背景 下午去前台领了一个"300M无线USB网卡"。 插到台式机上发现不能跳出WLAN网络信号。 我观察到每次插入无线上网卡&#xff0c;在window设置–>设备–>其他设…

员工资料导入

人事管理项目-员工资料导入 后端接口实现前端实现 既然有员工资料导出需求&#xff0c;当然也就有导入需求。对前端而言&#xff0c;员工资料导入就是文件上传&#xff0c;对后端而言&#xff0c;则是获取上传的文件进行解析&#xff0c;并把解析出来的数据保存到数据库中。 后…

Springboot实验二(用配置文件的方式整合Mybatis)仅供参考!

&#xff08;1)articleList.html 效果如下: <!DOCTYPE html> <html lang"en" xmlns:th"http://www.thymeleaf.org/"> <head> <meta charset"UTF-8"> <title>article 列表</title> </head> <body…

数论与组合数学 期末总结(未完

自然数的基本性质 数学归纳法(Principle of Mathematical Induction) n n 0 nn_{0} nn0​时成立&#xff0c;且 n k nk nk成立 ⇒ n k 1 \Rightarrow nk1 ⇒nk1成立&#xff0c;则定理对 n ≥ n 0 n\ge n_{0} n≥n0​成立良序定理(Well Ordering Principle) 每个非空集合…

网络编程 lesson7 广播组播和本地通信

目录 广播 什么是广播&#xff1f;&#xff08;了解&#xff09; 广播发送流程 广播接收流程 组播 什么是组播&#xff1f; 组播地址 组播结构体 组播发送流程 组播接收 本地套接字通信 什么是本地套接字通信&#xff1f; 特性 核心代码 程序实例 服务端 客户…

SpringCloud源码解析-gatewayopenFeign

SpringCloud高级应用-源码解析 1. gateway 源码解析1.1 自动装配1.2 核心装配1.2.1 GatewayClassPathWarningAutoConfiguration1.2.2 GatewayAutoConfiguration1.2.3 GatewayLoadBalancerClientAutoConfiguration1.2.4 GatewayRedisAutoConfiguration 1.3 Gateway 工作机制1.3.…

分布式任务调度XXL-JOB

XXL-JOB 分布式任务调度平台特点 职责分离&#xff0c;任务调度&#xff0c;任务执行解耦 执行一致性&#xff0c;任务执行不会多次重复执行 丰富的路由策略&#xff08;指定那个执行实例执行&#xff09; 阻塞处理 &#xff08;触发的任务&#xff0c;上一次没有执行完&am…

Android Studio类ChatGpt的免费AI编程助手

ChatGpt大火&#xff0c;带动了AI工具的发展&#xff0c;介绍两款免费的AI编程助手&#xff0c;一款用于输入关键字自动输出代码&#xff0c;一款则是自动补全提示&#xff0e; 可支持大部分代码编辑器&#xff0c;这里主要介绍Android Studio上安装使用&#xff0e; Bito 支…

来阿里面试,问我未来3-5年规划,我给领导画个大饼...

在面试的过程中是不是经常被面试官问未来几年的职业规划?你会答吗&#xff1f;是不是经常脑袋里一片空白&#xff0c;未来规划&#xff1f;我只是想赚更多的钱啊&#xff0c;哈哈哈&#xff0c;今天我来教大家&#xff0c;如何给面试官画一个大饼&#xff0c;让他吃的不亦乐乎…

c++day4 ——homework

1.思维导图 2. 整理类中特殊成员函数&#xff1a;构造函数&#xff0c;析构函数&#xff0c;拷贝构造函数&#xff0c;拷贝赋值函数的使用和实现 特殊成员函数的使用和实现&#xff1a; ① 构造函数 功能&#xff1a;当使用类实例化对象时&#xff0c;给类对象初始化空间以及初…