在Linux中,我们可以使用FUSE来进行自定义用户态文件系统的实现。编译example中的示例是学习FUSE
的第一步,本文侧重于剖析FUSE
的client
端的源码。
Linux环境我们选择Ubuntu,方便我们查看一些Linux的相关手册。在准备接触FUSE
之前,我们可以先上网搜索一下,或者你也可以快速翻一下手册,执行man 8 fuse
,在手册里我们可以看到好几个名词概念,例如FUSE
、filesystem
、libfuse
、filesystem owner
、client
。
至此,我们正式在手册里看到了libfuse的身影,然后我们去github上拉取源码,地址为 https://github.com/libfuse/libfuse 。截止该文完成,该库在github上最新版本为3.14
。不过在查看Release Notes后,我们依然选择采用3.12
版本进行源码分析。
我们拉取代码至本地,git操作不再赘述。
接下来我们查看ReadMe文档,可以看到该工程依赖于meson
构建工具,而不是依赖于我们熟悉的CMake
,因此下一步,我们需要解读meson构建脚本,即各个meson.build
。
初识meson
,我们依旧选择最官方的文档。进入meson官网。如下图,在网页左侧的导航栏中,我们可以先读一下简介、教程与示例,并大致浏览一下一些meson里的约定内容。
此处介绍几个比较重要的函数方法,后续分析meson脚本用得上。
示例:
cc = meson.get_compiler('c')
获取当前机器的C编译器,可进行编译参数配置。
示例:
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()*定义了预构建的库的一些信息,包括编译参数、链接参数等。
示例:
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('-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_dirs = include_directories('include', 'lib', '.')
*include_directories()*用以生成上下文可用的include目录。
示例:
thread_dep = dependency('threads')
*dependency()*用以对外部库进行依赖。
在libfuse工程中,我们通过meson结合ninja构建工具可以得到最终的构建产物,分别是libfuse3.so
,fusermount3
,fuse相关头文件
。
现在我们出于将该工程移植到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中应该显式链接哪些需要的库。
在前文中可以看到,我们在编译时配置了链接依赖文件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_30
,fuse_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
的身影,但是@
的数量不同。简单理解的话,@@
就是当前默认使用的函数定义的意思,而@
是非默认的意思。至于如何在链接时使用指定版本的函数定义,希望读者可以去自行探究。
接下来我们在Linux环境中通过nm命令查看一下so库中的各个函数的symver信息。
nm -D /usr/local/lib/x86_64-linux-gnu/libfuse3.so
可以看到下面这些信息(仅节选部分):
我们看到fuse_parse_cmdline@@FUSE_3.12
的地址指向01e8d0
,而fuse_parse_cmdline_312@@FUSE_3.12
的地址也指向该位置。因此得以确定,我们在实现中调用fuse_parse_cmdline
函数时,实际的调用目标是fuse_parse_cmdline_312
函数。
在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)等等之类的限定介绍。
问题回到pthread_setcancelstate
与pthread_cancel
两个函数为什么被Google从NDK中移除,这个问题的原因之一是线程被标记结束后不一定会把自己拥有的资源释放掉,甚至不一定会结束,因此很可能造成内存泄露或死锁等问题,而这些问题在移动设备上更加突出。我们通过查看Linux上的pthread_cancel
的man page可以看到,该库函数内部实际使用了信号作为实现手段。然而在libfuse的实现中我们可以看到里面调用到了这两个函数,那么下一个问题就是,如何自定义实现这两个函数的行为。
通过查找与搜索,不难找到网上说的使用pthread_kill
函数来进行代替实现。
那么我们也对该方案进行了采纳,目的是先让我们的工程通过NDK环境的编译,并成功运行起来。以下是我们定义的pthread_setcancelstate
与pthread_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);
}
让我们重新回到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 ' ))
cfg.set('HAVE_ICONV',
cc.has_function('iconv', prefix: '#include ' ))
# 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的输出目录为
下):
add_executable(hello
hello.c)
target_link_libraries(
hello
fuse3)
为了后续的执行,我们将这些成果物adb push
到以下Android设备系统路径中去:
adb push \build\intermediates\cmake\debug\obj\armeabi-v7a\libfuse3.so /system/lib
adb push \build\intermediates\cmake\debug\obj\armeabi-v7a\hello /system/bin
现在源码有了,环境有了,产生成果物的脚本也有了,是时候分析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
的结构体进行入手,可以逐渐展开一张结构体的关联图(仅展示部分,读者可以自行绘制):
有了结构体关联图,我们在读源码的过程中,便可以像调试程序一样一边跟踪程序执行中各变量的内容,一边剖析程序的状态机模型。
在“初始化动作”阶段,我们主要关注我们的自定义函数实现去了哪里。这个不复杂,稍作代码跟踪,我们就可以看到最后我们实现的系列函数被copy
去了fuse_fs
的struct 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就是在工作线程均繁忙的时候创建新的工作线程去处理。
前文已经分析出了在事件循环中我们会读取请求数据,随后处理请求数据并回复。
在请求数据中,其实libufse中的实现很简单,就是从一个fd中读取预期缓冲大小的数据(实际并不会读取这么多数据):
res = read(ch ? ch->fd : se->fd, buf->mem, se->bufsize);
这里有一个问题,这个预期大小会是多少byte。其实在默认情况下,初次init时大小为
,后续为
,都与pagesize()
有关。具体的value1与value2是多少,留给读者去探索。
我们以在console中进入mountpoint
所在的父路径为例,例如mountpoint
为/sdcard/Test
,那么现在我们执行ls /sdcard
命令来触发自定义文件系统的相关函数的调用。可以在刚才执行/system/bin/hello /sdcard/Test
的console中看到有一些打印(这里假设我们自己加上去了一些用于查看关键信息的打印,非libfuse源码自带)。
我们将其转换为函数调用栈:
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
即可,这会触发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操作 |
到此为止,其实我们已经拆解了大部分libfuse的工作原理,首先我们可以确定其基于C/S架构,内核端的Fuse其实就是Server,它会将VFS给它的文件系统操作函数进行消息封装,转化为请求数据并发送至/dev/fuse
设备。因此整个工作模型就像下图:
至此,一个Linux
上的用户态文件系统的client
端的一套官方开源代码库就暂时剖析结束了,里面涉及到的参数解析、初始化、异常处理、同步处理的细节留给需要的读者去慢慢体会,这里不再展开。