FUSE
什么是FUSE
Filesystem in Userspace顾名思义,即在用户空间的文件系统。
为什么要强调用户空间呢?接触过Linux内核的同学大概会知道,文件系统一般是实现在内核里面的,比如,Ext4、Fat32、NTFS(Kernel原生版)等常见的文件系统,其代码都在内核中,而FUSE特殊之处就是,其文件系统的核心逻辑是在用户空间实现的。为什么FUSE会存在
事物的存在的原因之一是其优势大于劣势,下面是它的优劣描述。
优势
- 文件系统的改动不用更新内核 FUSE的核心逻辑在用户空间,所以修改文件系统的行为绝大部分修改会在用户空间。这在很多场合是一件很方便的事情。
- 很容易实现自己的文件系统 理论上它可以实现任何天马行空的文件系统,只要一个开发者实现了基本的文件操作。而这个所谓的文件操作也是自己定义的,甚至可以这个操作可能只是一句打印而已,或者是一件超级复杂的事情,只要这个操作符合开发者的要求,他就完成了一个符合开发者需求的文件系统(也许本质上并不是文件系统了,这种情况是有现实例子的)。
劣势
- 效率较低 这是显而易见的,就针对块设备的文件系统而言,用户层肯定不如内核实现的效率高,毕竟用户态/内核态切换的开销是少不了的。这也是符合一般软件规律的,越高层次的软件易用性越高,效率越低。
FUSE实现原理
下面这张图体现了FUSE工作的基本套路,是根据WIki里的画的,这张图感觉更符合我看到的代码的状况。
图中体现了FUSE的2个关键部分(绿色方框),分别是Kernel中的那个FUSE(这里简称kernel FUSE)和user space中的那个fuse_user
程序。其中kernel FUSE是负责把从用户层过来的文件系统操作请求传递给fuse_user
程序的,而这个fuse_user
程序实现了前面所说的文件系统的核心逻辑。
/tmp
目录已经属于某个FUSE分区了,为了达到这种状况,前面还需要有一个mount的过程。 图中1号折线过程
- 用户敲
ls -l /tmp/file_on_fuse_fs
+回车 这时ls
会调用一些系统调用(例如stat(2))。 - kernel FUSE接收用户请求 文件相关的系统调用会进入VFS处理,然后VFS会根据这个分区的文件系统,找到对应文件系统的实现接口,这里当然是找到kernel FUSE。
- kernel FUSE会把收到的操作请求按照FUSE定义的通信协议发送给
fuse_user
程序 那么问题来了,kernel FUSE凭什么把消息给fuse_user,却没给别人呢? 如果看得懂,请体会如下两段代码
// kernel/fs/fuse/dev.cconst struct file_operations fuse_dev_operations = { .owner = THIS_MODULE, .open = fuse_dev_open, .llseek = no_llseek, .read_iter = fuse_dev_read, .splice_read = fuse_dev_splice_read, .write_iter = fuse_dev_write, .splice_write = fuse_dev_splice_write, .poll = fuse_dev_poll, .release = fuse_dev_release, .fasync = fuse_dev_fasync, .unlocked_ioctl = fuse_dev_ioctl, .compat_ioctl = fuse_dev_ioctl,};EXPORT_SYMBOL_GPL(fuse_dev_operations);static struct miscdevice fuse_miscdevice = { .minor = FUSE_MINOR, .name = "fuse", .fops = &fuse_dev_operations,};
// fuse_userint fd = open("/dev/fuse", ...);read(fd, ...);write(fd, ...);
1.第一段代码说明,FUSE会创建一个名为fuse的混杂设备文件(简称fuse设备文件);
2.第二段代码说明,fuse_user可以用过读写fuse设备文件来与kernel FUSE通信,也就是说,fuse_user可以通过read函数主动读取了kernel FUSE的请求。至此1号折线走完。图中2号曲线过程
fuse_user
收到kernel FUSE发来的请求 这个收发请求的机制在文末的参考资料中有提及,感兴趣的同学可以研究一下。fuse_user
处理这个请求 这个“处理”完全是开发者自己定义的,只要符合开发者的要求就是合适的处理方式,不过本文讨论的是针对块设备的货真价实的文件系统,所以这个“处理”必须能够读写块设备上面的内容。那么一个很简单的问题来了,如果不使用fwrite(3)
或write(2)
这种方式,怎么写入文件呢?
答案要回到事物的本源,文件是个抽象概念,它本质上只是块设备(例如磁盘、优盘或SD卡)上字节的有序排列而已,所以只要能写入块设备,写入文件当然就可以实现。
那么如何读写块设备呢?请想象插入一个优盘,然后体会下面代码。
int fd = open("/dev/block/sda");//或者sda1pwrite(fd, buf, count, offset);
解释一下上面代码。当我们插入一个优盘到linux系统时,常见的情况是系统会自动生成/dev/block/sda
和/dev/block/sda1
两个块设备文件,所以第一句open就是在获取块设备的fd(file descriptor)
,然后再用pwrite访问这个fd,将buf的内容向offset位置写count个字节。其中offset是写入位置,即从块设备的哪个字节开始写。虽然这里也是用了write一类的函数,但是write的对象不同哦。
图中3号折线过程
fuse_user
将处理结果返回给kernel FUSE- 继续顺着1号折线的来路,原路返回处理结果至此,3号折线走完。
说完了代码,下面我们用2个实际使用的case(Android和NTFS-3G)进行说明。
FUSE的实现代码
如前面所讲,FUSE分为2部分,分别在user、kernel spcae中,在kernel space中的部分由kernel官方维护,user space中的部分(仅是个框架,不包括开发者的实现)有一个开源库叫,NTFS-3G就是基于这个FUSE框架的实现,另一个我接触到实现是Android 8.0的中SD卡的文件系统的实现,它没有用libfuse,完全是谷歌自己写的一个实现。
NTFS-3G与FUSE
关于NTFS-3G
是一个叫Szabolcs Szakacsits的开发者2006年创建的项目,后来他创建了一个公司叫,从事很多NTFS文件系统相关的业务,NTFS-3G这个开源项目,也由这个公司维护至今,它就是一份典型的FUSE文件系统实现。
代码导读
根据上面所说的原理,这个文件系统中必然存在着块设备和fuse设备的open/close/read/write
。下面着重描述3个重要动作,分别是打开块设备、打开fuse设备和处理kernel FUSE请求,啥也不说了,都在代码里了,撸!。
ntfs-3g.cmain——打开块设备{ ... //打开块设备,opts.device就是块设备的名字,例如"/dev/block/sda1" //这里就是前面代码中的open来获得fd的动作 err = ntfs_open(opts.device); ==> ctx->vol = ntfs_mount(device, flags); { dev = ntfs_device_alloc(name, 0, &ntfs_device_default_io_ops, NULL); { //埋下伏笔(1)!!! //注册了设备文件操作函数 //dev->d_ops->open = ntfs_device_unix_io_open //dev->d_ops->write = ntfs_device_unix_io_write dev->d_ops = dops; dev->d_private = priv_data; } ... vol = ntfs_device_mount(dev, flags); ==> vol = ntfs_volume_startup(dev, flags); { if ((dev->d_ops->open)(dev, ...)) //为什么会call 到这呢,请看前面的伏笔(1)!!! ==> ntfs_device_unix_io_open(struct ntfs_device *dev, int flags) //注意了!注意了!open块设备了啊! //例如,dev->d_name = "/dev/block/sda1" ==> *(int*)dev->d_private = open(dev->d_name, flags); //埋下伏笔(7)!!! //注册了设备文件操作函数 vol->dev = dev; } } ...}
从上述代码中可以看到,块设备确实被打开了。
ntfs-3g.cmain——打开fuse设备{ //前面已经open了块设备 ... fh = mount_fuse(parsed_options); { ctx->fc = try_fuse_mount(parsed_options); ==> fc = fuse_mount(opts.mnt_point, &margs); { fd = fuse_kern_mount(mountpoint, args); ==> res = fusermount(0, 0, 0, mnt_opts ? mnt_opts : "", mountpoint); ==> res = mount_fuse(mnt, opts); ==> fd = open_fuse_device(&dev); ==> fd = try_open(FUSE_DEV_NEW, devp); //注意了!注意了!open fuse设备了啊! //例如,dev = "/dev/fuse" ==> fd = open(dev, O_RDWR); ... ==> ch = fuse_kern_chan_new(fd); { struct fuse_chan_ops op = { //埋下伏笔(2.0)!!! //注册了设备文件操作函数 //op.receive = fuse_kern_chan_receive //op.send = fuse_kern_chan_send .receive = fuse_kern_chan_receive, .send = fuse_kern_chan_send, ... }; ... return fuse_chan_new(&op, fd, bufsize, NULL); ==> return fuse_chan_new_common(op, fd, bufsize, data); { //埋下伏笔(2.1)!!! //伏笔(2.0)的op给了ch //ch->op->receive = fuse_kern_chan_receive //ch->op->send = fuse_kern_chan_send ch->op = *op; //埋下伏笔(3)!!! //打开fuse设备的fd给了ch ch->fd = fd; } } } //埋下伏笔(4.0)!!! //ntfs_3g_ops.write = ntfs_fuse_write fh = fuse_new(ctx->fc, &args , &ntfs_3g_ops, sizeof(ntfs_3g_ops), NULL); //埋下伏笔(5.0)!!! //llop = fuse_path_ops //fuse_path_ops.write = fuse_lib_write ==> f->se = fuse_lowlevel_new(args, &llop, sizeof(llop), f); { struct fuse_session_ops sop = { //埋下伏笔(6.0)!!! //sop.process = fuse_ll_process .process = fuse_ll_process, ... }; ... //埋下伏笔(4.1)!!! //f->fs->op = ntfs_3g_ops //f->fs->op.write = ntfs_fuse_write fs = fuse_fs_new(op, op_size, user_data); ==> memcpy(&fs->op, op, op_size); f->fs = fs; ... //埋下伏笔(5.1)!!! //op = llop = fuse_path_ops //f->op->write = fuse_lib_write memcpy(&f->op, op, op_size); ... se = fuse_session_new(&sop, f); { //埋下伏笔(6.1)!!! //sop给了se se->op = *op; se->data = data; } } } ...}
从上述代码中可以看到,fuse混杂设备确实被打开了。
ntfs-3g.cmain——处理kernel FUSE请求{ //前面已经open了块设备和fuse设备 ... fuse_loop(fh); ==> return fuse_session_loop(f->se); { //这个循环中不停地响应着kernel FUSE的请求 while (!fuse_session_exited(se)) { struct fuse_chan *tmpch = ch; res = fuse_chan_recv(&tmpch, buf, bufsize); ==> return ch->op.receive(chp, buf, size); //为啥call到这?请看伏笔(2.x) ==> fuse_kern_chan_receive { //fuse_chan_fd(ch)是什么?请看伏笔(3) res = read(fuse_chan_fd(ch), buf, size); } ... fuse_session_process(se, buf, res, tmpch); ==> se->op.process(se->data, buf, len, ch); //为啥call到这?请看伏笔(6.x) ==> fuse_ll_process /********** static struct { void (*func)(fuse_req_t, fuse_ino_t, const void *); const char *name; } fuse_ll_ops[] = { ... [FUSE_WRITE] = { do_write, "WRITE" }, ... }; ***********/ //假设我们在进行写(FUSE_WRITE)操作 ==> fuse_ll_ops[in->opcode].func(req, in->nodeid, inarg); ==> do_write ==> req->f->op.write(req, nodeid, PARAM(arg), arg->size, arg->offset, &fi); //为啥call到这?请看伏笔(5.x) ==> fuse_lib_write { ... res = fuse_fs_write(f->fs, path, buf, size, off, fi); ==> return fs->op.write(path, buf, size, off, fi); //为啥call到这?请看伏笔(4.x) ==> ntfs_fuse_write ==> s64 ret = ntfs_attr_pwrite(na, offset, size, buf + total); //vol->dev是什么?请看伏笔(7) ==> written = ntfs_pwrite(vol->dev, ...); ==> written = dops->pwrite(dev, ...); //为啥call到这?请看伏笔(1) ==> ntfs_device_unix_io_pwrite //注意了!注意了!对块设备文件pwrite了啊! //DEV_FD(dev)是什么?请看前面打开块设备的地方 ==> return pwrite(DEV_FD(dev), buf, count, offset); ... fuse_reply_write(req, res); ==> return send_reply_ok(req, &arg, sizeof(arg)); ==> return send_reply(req, 0, arg, argsize); ==> return send_reply_iov(req, error, iov, count); ==> res = fuse_chan_send(req->ch, iov, count); ==> return ch->op.send(ch, iov, count); ==> fuse_kern_chan_send //注意了!注意了!对fuse设备文件writev了啊! //虽然和write不同,但也是向fuse设备文件的fd写东西 //fuse_chan_fd(ch)是什么?请看伏笔(3) ==> ssize_t res = writev(fuse_chan_fd(ch), iov, count); ... } } } //收尾工作 ...}
光练不说傻把式,还得说一下。
从上述代码中可以看到,“写”的用户请求是用ntfs_fuse_write函数处理的,struct fuse_operations ntfs_3g_ops
就是开发者要实现的文件系统核心逻辑,这些文件操作在打开fuse设备过程(mount_fuse函数)中被绑定到那些核心数据结构中。在最后处理文件系统请求时调用,最终以直接访问块设备的方式实现了“处理”。然后,以写入fuse设备文件的方式将“处理”结果发给kernel FUSE。 Android与FUSE
Android代码到哪看
谈到Android,由于众所周知的原因,首先要说怎么在中国大陆访问它的代码,这事靠百度可以解决,如果只是看看,用这些网站在线看就好了。
Android 8.0的FUSE
Android里面用的并不是NTFS-3G所使用的libfuse,因为我接触到的是AN8(Android 8.0)的代码,所以就基于它来再次领略一下FUSE文件系统的实现。代码在,下面有3个代码文件
AN8/system/core/sdcard├── Android.mk├── fuse.cpp├── fuse.h└── sdcard.cpp // main函数在这里!!!
sdcard.cpp是对SD卡文件系统处理的代码。谷歌搞了个sdcardfs
文件系统,当系统支持sdcardfs
,并且用户要求使用时,就会优先用这个文件系统挂载SD卡,否则就用FUSE挂载。也就是说,对于AN8来说,FUSE是sdcardfs
的备胎,下面代码反映了这绿油油的事实。
int main(int argc, char **argv) { //各种准备工作 ... if (should_use_sdcardfs()) { //如果应该用sdcardfs,就运行sdcardfs run_sdcardfs(...); } else { //否则,就运行FUSE run(...); } return 1;}
下面创建了3个start_handler的线程,看得出来它们之间有些区别。为什么是这3个?我也不知道,那就是AN8的实现问题了,不是本文重点。
static void run(...) { //准备工作 ... //埋下伏笔(1) //这些dest_path后面会用到 snprintf(fuse_default.dest_path, PATH_MAX, "/mnt/runtime/default/%s", label); snprintf(fuse_read.dest_path, PATH_MAX, "/mnt/runtime/read/%s", label); snprintf(fuse_write.dest_path, PATH_MAX, "/mnt/runtime/write/%s", label); handler_default.fuse = &fuse_default; handler_read.fuse = &fuse_read; handler_write.fuse = &fuse_write; ... if (fuse_setup(&fuse_default, AID_SDCARD_RW, 0006) || fuse_setup(&fuse_read, AID_EVERYBODY, ...) || fuse_setup(&fuse_write, AID_EVERYBODY, full_write ? 0007 : 0027)) ==> fuse_setup { //注意了!注意了!打开fuse设备文件了啊! //埋下伏笔(2) //注意这个fd,后面会用到 fuse->fd = TEMP_FAILURE_RETRY(open("/dev/fuse", O_RDWR | O_CLOEXEC)); //欲知fuse->dest_path是什么,请看伏笔(1) mount("/dev/fuse", fuse->dest_path,...) } ... if (pthread_create(&thread_default, NULL, start_handler, &handler_default) || pthread_create(&thread_read, NULL, start_handler, &handler_read) || pthread_create(&thread_write, NULL, start_handler, &handler_write)) { LOG(FATAL) << "failed to pthread_create"; } // 一些不会退出的loop ...}
在上面代码中,打开了fuse设备文件,同时创建了3个FUSE用户线程来处理kernel FUSE的请求。
start_handler ==> handle_fuse_requests{ for (;;) { //欲知fuse->fd是什么,请看伏笔(2) read(fuse->fd,...); ... //埋下伏笔(3) //data是kernel FUSE发来的请求 int res = handle_fuse_request(fuse, handler, hdr, data, data_len); //以一个顺利的写请求为例 ==> return handle_write(fuse, handler, hdr, req, buffer); { struct handle *h = static_cast(id_to_ptr(req->fh)); ... //注意了!注意了!写块设备文件了啊! //欲知h->fd是什么,它源自伏笔(3)提到的data,所以它来自kernel FUSE //它是怎么来的呢?此处设下一个悬念(1) res = TEMP_FAILURE_RETRY(pwrite64(h->fd, buffer, req->size, req->offset)); ... fuse_reply(fuse, hdr->unique, &out, sizeof(out)); //注意了!注意了!写fuse设备文件了啊! //欲知fuse->fd是什么,请看伏笔(2) ==> ssize_t ret = TEMP_FAILURE_RETRY(writev(fuse->fd, vec, 2)); ... } ... }}
在上面代码中,读取了kernel FUSE的请求,然后处理,即写入了块设备文件,最后发回结果给kernel FUSE。
这段代码中有一个悬念,后文会揭露。Kernel FUSE
代码在哪
- Kernel代码、
- FUSE路径 Kernel fuse代码在目录
代码导读
我对这里没有多少研究,怕误人子弟,所以不展开了,仅仅围绕前面代码中的悬念(1)进行说明。前面的悬念(1)在于那个来自kernel FUSE的fd是在哪里赋值的。下面先从读fuse设备文件说起,因为这里就是获取kernel FUSE请求的现场。
读写fuse设备文件
前面的FUSE文件系统实现中,它们与kernel FUSE沟通都是通过读写fuse设备文件实现的,而这个设备文件的读写操作就定义在struct file_operations fuse_dev_operations
中。
//dev.cconst struct file_operations fuse_dev_operations = { ... .read_iter = fuse_dev_read, .splice_read = fuse_dev_splice_read, .write_iter = fuse_dev_write, .splice_write = fuse_dev_splice_write, ...};
我看了半天才想到,上面这部分代码对我们揭开悬念没有帮助,因为这里只是把请求从某处读出来给用户层而已,所以它并不生产请求,只是请求的搬运工。回顾一下原理,直接给kernel FUSE创建请求的是VFS,所以应该从对接VFS的那部分kernel FUSE的代码寻找线索。另外一个重要线索就是在AN8的代码中,处理写请求时用到了struct fuse_write_in
这个结构体,悬念处的fd值就是源于fuse_write_in.fh
,关键是它定义在kernel的头文件里哟,你懂的。
神秘的fh
在kernel里搜struct fuse_write_in
就很容易发现,fuse_write_in.fh
的赋值在fuse_write_fill
里面。下面的代码描述的是每个FUSE文件系统中的文件的“写”过程,从这个过程中可以观察到,这个fh
就在file->private_data
中,file->private_data
的实际类型是struct fuse_file*
。
static const struct file_operations fuse_file_operations = { ... .write_iter = fuse_file_write_iter, ==> written_buffered = fuse_perform_write(file, mapping, from, pos); ==> num_written = fuse_send_write_pages(...); ==> res = fuse_send_write(req, &io, pos, count, NULL); { struct fuse_file *ff = file->private_data; ... fuse_write_fill(req, ff, pos, count); { struct fuse_write_in *inarg = &req->misc.write.in; ... //inarg->fh = file->private_data->fh inarg->fh = ff->fh; } } ...};void fuse_init_file_inode(struct inode *inode){ inode->i_fop = &fuse_file_operations; inode->i_data.a_ops = &fuse_file_aops;}
神秘的private_data——一切还在用户层
暮然回首,那fd还在用户层,请看代码(AN8中)。
static int handle_open(struct fuse* fuse, struct fuse_handler* handler, const struct fuse_in_header* hdr, const struct fuse_open_in* req){ ... node = lookup_node_and_path_by_id_locked(fuse, hdr->nodeid, path, sizeof(path)); ... //注意了!注意了!打开块设备文件了啊! //悬念(1)被揭露了 h->fd = TEMP_FAILURE_RETRY(open(path, req->flags)); out.fh = ptr_to_id(h); fuse_reply(fuse, hdr->unique, &out, sizeof(out)); ...}
上面这段代码是AN8的FUSE实现打开文件的函数,这就是AN8 FUSE打开块设备文件的现场了。为什么是open呢?此处与kernel中的file->private_data有什么关系呢?请看下面代码。
int fuse_do_open(struct fuse_conn *fc, u64 nodeid, struct file *file, bool isdir){ ... //这里就通向了用户层的open,用户层那个fh就通过outarg传了回来 err = fuse_send_open(fc, nodeid, file, opcode, &outarg); ff->fh = outarg.fh; ff->open_flags = outarg.open_flags; ... //file->private_data->fh = outarg.fh //outarg就是悬念(1)被揭露现场的那个out file->private_data = fuse_file_get(ff); ==> return ff; ...}
与NTFS-3G相比,AN8利用每个文件的struct file.private_data来传递块设备文件的fd,并在open文件的时候打开块设备。