by fanxiushu 2026-02-07 转载或引用请注明原作者。
linux平台或者是macOS平台,要实现目录重定向,其实是挺简单的:
使用mount命令,挂载NFS网络文件系统,就可以把远程的NFS文件目录挂载到本地的某个目录下。
不需要做任何的代码开发就能完成这个功能。
那本文为何还专门提及,因为上面说的都是集成到系统中的NFS网络文件系统。
现成倒是现成的,可也是固定的。
比如,如果我们打算用自己的文件传输通信协议呢,
或者反过来,不是客户端主动连接NFS文件服务器,而是文件服务器反过去连接客户端呢?
因为最近我就在计划把xFsRedir程序做个改造,用于支持反向连接,
让文件服务器主动连接xFsRedir(当然也只能支持私有通信协议)
这种需求肯定有,一旦有这种需求,上面使用现成的mount挂载远程目录的就不再适用了。
经常接触各类系统平台,尤其是各种类型的嵌入式设备并且做代码开发的朋友,
通常都会面临一个代码维护问题。因为在你自己的电脑上写好代码,需要放到目标机器上去编译,调试。
比较笨的办法是把代码copy过去,编译,但是可能会出错,
找到哪里错误,接着在自己电脑上编辑修改,然后再copy到目标机器。。。如此反复,
(有人可能会问,为何不直接在目标机上编辑,这问题问得。。。)
当然你也可以使用git等代码同步工具,建一个git维护服务器,在目标机器装一个git,
在自己电脑也装git,每次修改完提交一下,到了目标机也更新一下。这么搞,其实比直接copy好不到哪里去。
到了这个时候,你就会发现,NFS这类网络文件系统就发挥他的优势了,
在嵌入式(通常是linux)设备mount挂载NFS网络文件系统。
NFS文件服务端就是我们的自己电脑编辑的代码所在目录,
这样我们在自己电脑上随时编辑代码,到了目标机,直接在这个被映射到目标机的目录上编译。
这就是最简洁的解决办法。
当然,我们还可以这样搞:在目标设备上开启文件服务器,比如FTP,NFS等等,
然后在我们的电脑上使用工具打开,当然也可以使用xFsRedir等软件直接映射到本地电脑目录中,
这样操作的坏处就是代码在目标机上,每次完成任务,都得copy到自己电脑上进行备份,免得丢失。
上面说的都是利用现成的东西搭建的,
现在我们再深入一点,自己搞这一套东西。不再使用现成的NFS文件系统。
很多年前,我是直接从linux最底层VFS驱动层去实现这个功能的,
因为当时也是刚开始做windows平台的xFsRedir软件,windows的文件过滤驱动很难搞,
但是linux驱动中VFS层相对来说就简单多了,在我的github上,很早就上传了VFS相关的实现代码。
https://github.com/fanxiushu/xfredir-linux-driver
里边有对应的VFS驱动,以及应用层调用驱动的完整代码,
当时是在2.6内核和3.10内核上都编译运行没问题,
只是挺可惜,VFS作为linux内核接口,却没有接口的觉悟。
因为每个kernel版本,VFS接口都变化极大。
到了4.xx, 5.xx,6.XX,几乎是每个版本都得修改,而且每次改动都挺大,真是搞得来非常头大。
因此linux平台,我们应该找一个接口更加稳定不变的,甚至不用编写驱动直接在应用层调用的接口。
那就是 fuse,fuse是 FileSystem In Userspace 的缩写,意思是用户空间的文件系统。
其实实现方式跟我发布到github上的
https://github.com/fanxiushu/xfredir-linux-driver
原理基本都一样,都是利用VFS驱动层,把内核中对文件的处理请求发送到应用层,然后应用层来处理具体的文件请求。
不过我当初实现VFS驱动的时候,并没参考fuse,甚至当时我做VFS的时候,都不知道有fuse的存在。
这也正好说明,在linux上实现用户空间的文件系统,核心部分其实没啥大的区别。
接着来讲 fuse,fuse会生成一个 /dev/fuse 设备,
文件传输等各种文件处理,全靠这个设备发送到应用层。
fuse的实现其实也是分成两部分,一是内核部分,跟linux内核合并到一起,具体是linux内核源码的 /fs/fuse 目录下,
大概是从内核2.6开始就一直存在在linux内核源码中,作为默认,fuse内核部分通常是直接编译进linux内核的。
当然作为嵌入式设备,如果没必要,会被从内核编译中直接裁剪掉。
fuse会生成 /dev/fuse 字符设备,用于作为内核和应用层的通信桥梁。
接着就是应用层接口部分,这部分主要目的就是跟 /dev/fuse 进行通信,
然后导出稳定的API接口,这样我们可以直接调用此API函数,从而实现我们自己的文件系统。
fuse的应用层接口,在github上也能找到源码:
https://github.com/libfuse/libfuse
具体的通信细节,这里不再赘述,反正无非就是各种文件请求传输来传输去的,
有兴趣的可以仔细研究libfuse应用层接口源码,尤其是跟/dev/fuse通信的接口代码。
可以看到,这个代码历史悠久 ,从 2.xx 版本一直发展到现在的 3.18.xx 版本。
为了能在较老的linux平台编译,我找了个折中的版本, libfuse-3.10,
当然,把 libfuse 编译成静态库,静态连接进我的程序中。
然后发现,集成libfuse3.10的程序,既能在 2.6 内核的 redhat5.5(很老的redhat版本)正常使用fuse,
同时也能正常运行在 ubuntu-25 这个最新的的linux系统中,内核版本是 6.17。
可见这个 /dev/fuse 设备兼容性挺强的,
再也不用担心VFS的兼容性问题,也不用每个内核版本都得修改VFS,改到怀疑人生了。
下面是集成fuse的程序的演示视频,当然也包括macOS系统,
不过MacOS系统中使用的不是fuse而是另外的办法,下面会具体提到。
演示linux和macOS自己实现的程序访问远程目录
演示视频中主要使用的是 xfs_redir 程序,可以看到 xfs_redir有两种办法来挂载远程目录:
一是主动连接到文件服务器,二是开启侦听端口,接收被动连接,由文件服务器主动连接xfs_redir。
当然,使用的都是我们自己定义的文件传输协议,这个通信协议其实就是 xFsRedir 软件的私有通信协议。
libfuse应用层对外提供的接口分两种:一种是更底层的接口,跟驱动更接近,保留inode节点信息。
另一种是在底层接口的基础上,再次封装,形成了上层接口,
这个上层接口完全屏蔽了inode节点信息,全部使用文件路径来代替。
我更喜欢libfuse的上层接口,使用起来更简单方便。
很早前做过VFS驱动,知道inode节点处理起来的麻烦,
因为到了更上层的文件传输网络通信部分,大部分都是以文件路径方式来进行文件的处理。
所以不得不做inode节点和Path路径的关联缓存,处理起来挺烦的。
有兴趣的同学可以去查阅我很早前发到github的xfredir-linux-driver 项目。
我们来看看libfuse提供的上层接口:
首先,我们必须搞清楚 fuse_operations 这数据结构:
它在libfuse的 fuse.h 头文件中定义,提供的全是一堆文件操作回调函数。
这也是我们实现自己的文件系统必须实现的核心任务。
如下伪代码:
cpp
struct fuse_operations* op = &ctx->op;
op->init = fuseInit;
op->destroy = fuseDestroy;
op->getattr = fuseGetattr;
op->mknod = fuseMknod;
op->mkdir = fuseMkdir;
op->unlink = fuseUnlink;
op->rmdir = fuseRmdir;
op->symlink = fuseSymlink;
op->rename = fuseRename;
op->link = fuseLink;
op->chmod = fuseChmod;
op->chown = fuseChown;
op->truncate = fuseTruncate;
op->open = fuseOpen;
op->read = fuseRead;
op->write = fuseWrite;
op->statfs = fuseStatfs;
op->flush = fuseFlush;
op->release = fuseRelease;
op->fsync = fuseFsync;
op->opendir = fuseOpendir;
op->readdir = fuseReaddir;
op->releasedir = fuseReleasedir;
op->access = fuseAccess;
op->create = fuseCreate;
op->utimens = fuseUtimens;
我实现了里边一半多的回调函数,当然有些没必要实现,也就不在这里列举出来了。
填充了数据结构,并且实现了里边的回调函数之后,如果你求更简单化,
直接在 main 函数中这么调用:
cpp
int main(int argc,char**argv)
{
return fuse_main(argc,argv, op, NULL);
}
就这么简简单单的就实现了fuse文件系统。
当然重头戏还是回调函数的实现,至于如何实现,因为回调函数过多,也不可能全部列举,
我们就以 getattr (获取文件或目录的属性,这是调用最频繁的回调函数之一)为例:
cpp
static int fuseGetattr (const char* path, struct stat* st, struct fuse_file_info *fi)
{
; path就是文件或目录的路径,
; st 对应的就是 stat 结构,这个跟C接口函数stat定义的结构体一致。
;至于如何响应此回调函数,其实应该挺好处理的,
;比如用我们自己网络通信协议,我们打包Path路径以及查询属性的命令到文件服务端。
;文件服务端接到这个命令,使用操作系统stat函数查询对应路径的文件,然后返回属性信息,
;整个过程都是在此函数中一问一答的阻塞方式进行通信,获得属性之后,此回调函数返回。
}
以同样的方式处理其他回调函数,
反正就是linux系统提供的 文件操作函数比如 open,read,write opendir, readdir 等等的回调版本。
上面所说的使用 fuse_main 未免过于简单,也不方便我们在同一个程序中挂载多个文件系统。
因此得拆开来进行处理,其实也挺简单:
我们首先使用 fuse_new 来创建 fuse 对象:
cpp
struct fuse* fuse = fuse_new(&args, op, sizeof(*op), ctx);
其中args是main函数第2个参数, op指向 fuse_operations 回调函数集合体,ctx是我们的私有数据指针。
接着调用fuse_mount函数挂载到某个本地目录,比如本地目录是 /mnt/fuse-dir:
cpp
fuse_mount(fuse, '/mnt/fuse-dir");
再接着就是调用 fuse_loop_mt 函数执行循环,直到在别的线程调用 fuse_exit 退出。
cpp
struct fuse_loop_config loop_config;
loop_config.clone_fd = 0;
loop_config.max_idle_threads = 10;
int res = fuse_loop_mt(ctx->fuse, &loop_config);// 一直阻塞,直到在别的线程调用 fuse_exit 退出
上面使用 fuse_loop_mt 而不是 fuse_loop, 其目的自然是为了使用它的多线程优势。
因为每个回调函数都是阻塞执行的,
使用 fuse_loop如果在调用某个回调函数的时候,其他回调函数就会发生阻塞,而多线程则不会发生这种事。
当然,要更灵活的解决阻塞问题,还是得使用 libfuse的底层接口函数来处理。
到最后,我们 使用
cpp
fuse_unmount(fuse);
fuse_destroy(fuse);
来卸载和销毁fuse。
这样,我们就可以使用 fuse_new等上面一套函数集合,在同一个程序中同时创建多个文件系统。
以上就是fuse的简单实现,说实话,比起windows使用过滤驱动实现目录重定向不知道要简单多少倍。
但其实也没有类比性,毕竟是完全不同的两种操作系统。
linux或者说类UNIX系统的文件系统,是以 '/' 为树根,整个文件系统像是一个倒挂的树。
任何其他文件系统都可以随意的挂载到以 '/' 为树根的任何子目录下,并且无缝的成为整颗倒挂树的某一个分叉。
所以我们不管是直接利用VFS在内核开发新文件系统,或者直接使用fuse开发的用户空间文件系统,
挂载到某个目录下,都能无缝的融合进整个倒挂树中去。
而 windows 是以盘符来区分不同文件系统,每个盘符都是并列的,没有上下之分,
比如 C,D,E盘,三个盘符都处于同一位置,他们没有上级 "/" 树根, CDE可以是相同的文件系统,也可以不是。
比如D是本地磁盘,E 是光盘,F是映射的网络邻居(SMB)等等。
而如果我们想在某个具体目录中,比如 D:\Path,挂载NFS或者其他网络文件系统,普通办法是没办法做到的。
只能是另外使用一个盘符,比如 G 盘符来挂载成 NFS 网络文件系统。
如果非要在 D:\Path 目录中NFS挂载网络文件系统呢?
那就是我的 xFsRedir 程序做法:
开发文件过滤驱动,拦截目录 D:\Path 的文件请求,并且转发成 NFS 网络文件系统请求。
整个过程繁琐而复杂,也很容易出问题。
有了linux下的fuse,我们再看看MacOS系统的情况,很可惜MacOS没有系统集成的fuse,macOS系统也没有VFS驱动层。
应该是很老的macOS有,但是后来的版本删除了VFS层。
macOS系统也没有另外的组件完成类似fuse的功能,因此我们得必须自己开发对应的驱动以及应用层接口。
github上有个项目 macFUSE(或者老名字叫 OSXFUSE)就是实现类似 fuse 功能的。
这个我目前还没去使用过,毕竟牵涉到驱动,要使用,我们还得额外安装他的驱动和应用层接口包。
有没有更简单,不用额外安装任何东西,
直接利用macOS操作系统本身提供的特性功能,简单的做一下封装或变换,就能提供类似fuse的功能呢?
答案是肯定的,正如我演示视频看到的,macOS系统中就单纯运行 xfs_redir 程序,没安装任何其他相关库。
其原理,说明白了的话,其实挺简单的,而且可以说是非常粗俗,不,是通俗: 使用代理。
是的,本地NFS代理。
代理这种办法也不是我发现的,而是在网上看到有人使用NFS代理来解决MacOS没有fuse的问题,
具体在哪看到的,已经忘记了。
macOS系统内部集成了NFS文件系统,他可以不用安装任何组件,就能顺利挂载NFS网络文件系统。
我们实现一个 NFS网络文件系统的服务端,在127.0.0.1本地侦听,
同时使用mount命令挂载到127.0.0.1的NFS文件服务端。
然后我们实现的NFS文件服务端把接收到的文件请求,做成类似fuse那样的接口,这样不就实现fuse功能了吗?
xfs_redir程序内部实现就包含了NFS服务端。
同样的道理,这种办法也可以使用到 linux 平台,
不过嘛,linux有现成的更好的 fuse,所以显得在linux下这种代理办法就没那么好使了。
这种办法当然也可以用在windows平台,不过嘛,咋说呢?
windows虽然也支持 NFS 网络文件的挂载(作为windows的可选组件,需要首先安装),
前面说了,windows的文件系统有别于 UNIX 的文件系统,
要挂载也就只能挂成一个新的盘符,
而且这个盘符还是非常明显的网络盘符标志,所以使用它,还不如使用 xFsRedir软件呢!
现在我们来看,如何实现我们自己的 NFS 文件服务器,
一种具有挑战性的做法就是完全实现NFS通信协议,
研究 NFS协议的RFC文档,RFC内容好像不是太多,如果有这个毅力的话,是能自己实现的。
还有就是从网络上找NFS的开源项目。
我找到两个:一个是 unfs3,地址:
https://github.com/unfs3/unfs3
另一个是 libnfs,地址:
https://github.com/sahlberg/libnfs
第一个unfs3因为跟本地文件系统绑的太紧,比较难拆分开,
拆分的目的是为了形成一个独立于本地文件系统或者说独立于操作系统的回调函数接口集合。
当然如果真想去拆分的话,还是能把unfs3的NFS通信代码和本地文件操作代码拆分出来,
不过我们有个更好的选择,那就是第二个 libnfs。
关于 sahlberg 提供的开源项目,我在 xFsRedir 中其实已经使用了他的三个项目:
libsmb2实现windows的 SMB协议, libiscsi实现 ISCSI网络磁盘,libnfs实现NFS客户端。
当时使用的libnfs是很早前,当时的libnfs源码中还没有 nfs server 功能。
新版本的libnfs提供了 nfs server, 并且是纯粹的网络通信部分,跟操作系统的文件系统不沾边。
这正好实现我们的 NFS 本地服务代理功能。
但是要正确使用libnfs的nfs server却并不容易,
好在有热心人帮忙 提供了 使用的example例子代码:
https://github.com/vitalif/libnfs-server-example
光有上面的example还不够,因为例子代码过于简单,我们还得结合 unfs3 源码,
找到每个NFS请求具体在干什么,然后实现真正的NFS文件处理功能。
反正我最后把他合并,整理成一组独立于操作系统的简洁的回调函数集合:
cpp
```cpp
struct nfsfile_callback_funcs
{
///基本的只读模式,必须添加
int (*stat)(struct nfs_client_t* c, const char* path, nfsfile_stat_t* st);
int (*readdir)(struct nfs_client_t* c, const char* path, nfsfile_readdir_t* item);///query item >0, no item =0, error <0
int (*stat_vfs)(struct nfs_client_t* c, const char* path, nfsfile_statvfs_t* st_vfs); //根据路径获取所在磁盘信息
int (*read)(struct nfs_client_t* c, const char* path, char*buffer, int64_t offset, int length);
////可读写文件系统必须
int (*write)(struct nfs_client_t* c, const char* path, char*buffer, int64_t offset, int length);
int (*set_attr)(struct nfs_client_t* c, const char* path, struct nfsfile_set_attr_t* set_attr);
int (*mkdir)(struct nfs_client_t* c, const char* path, int mode);
int (*remove)(struct nfs_client_t* c, const char* path);
int (*rmdir)(struct nfs_client_t* c, const char* path);
int (*rename)(struct nfs_client_t* c, const char* old_path, const char* new_path);
/// 扩展
int (*readlink)(struct nfs_client_t* c, const char* path, char*buf, int buflen);
int (*symlink)(struct nfs_client_t* c, const char* oldpath, const char* newpath);
int (*link)(struct nfs_client_t* c, const char* oldpath, const char* newpath);
int (*mknod)(struct nfs_client_t* c, const char* path, int mode, unsigned int devid);
};
有人可能对 本地 NFS代理,以及如何最终运行到我上面提供的简洁函数集合上,并且实现最终目的不太理解。
那我们现在梳理一下运行流程。
首先我们的程序运行 NFS Server,并且在 127.0.0.1 地址侦听,
然后 macOS系统(或者linux系统)使用 mount 命令,挂载类型为 nfs,挂载地址127.0.0.1, 然后挂载到某个本地目录。
比如 /mnt/nfs,
这个时候相当于MacOS系统是 NFS 的客户端,我们的程序是 NFS 服务端,
当操作系统 访问 /mnt/nfs 的时候,比如操作系统 要使用 stat 函数查询文件属性。
于是NFS 客户端把查询属性的命令转成 GETATTR 请求,并携带 GETATTR3args 参数,
我们在 NFS 服务端,收到 GETATTR 请求,并且从 GETATTR3args 获取到文件Path路径等参数。
然后调用我们的回调函数集合中的
int (stat)(struct nfs_client_t c, const char* path, nfsfile_stat_t* st);
在此回调函数中,我们与真正的文件服务器通信,根据path路径查询到文件属性,然后返回。
stat 回调函数返回之后,
我们在 NFS 服务端,根据返回 nfsfile_stat_t (一个类似 stat 的数据结构)的属性信息,
填充 NFS 网络通信的 GETATTR3res 应答参数,并且最终把GETATTR3res 回复给 NFS 客户端。
NFS 客户端接收到GETATTR3res 回答,然后返回给操作系统,接着 系统函数 stat 就获取到了 文件属性信息。
其他回调函数也是如此运行。
本文完。