基于FUSE3.3.0编写自己的文件系统

前言

什么是FUSE

FUSE is a Linux kernel extension that allows for a user space program to provide the implementations for the various file-related syscalls

注意

WSL(Windows Subsystem for Linux)现在并不支持FUSE


关于文件系统

设计要求

From a user interface perspective, our file system will be a two level directory system, with the following restrictions/simplifications:

  1. The root directory “\” will not only contain other subdirectories, but also regular files
  2. The subdirectories will only contain regular files, and no subdirectories of their own
  3. All files will be full access (i.e., chmod 0666), with permissions to be mainly ignored
  4. Many file attributes such as creation and modification times will not be accurately
  5. Files cannot be truncated
  6. Directories are treated as files

The file system will keep data on “disk” via a linked allocation strategy, outlined below

FUSE 版本

  • FUSE 3.3.0

运行环境

  • Ubuntu 18.04

前期准备

安装 VMware

这里推荐 VMware Workstation 15 Player,非商业用途免费,想要高级功能也可以升级到Pro版(官网

创建 Ubuntu 18.04 虚拟机

请参考 https://blog.csdn.net/woainishifu/article/details/78967384


FUSE的安装

01. 可能所需的软件包安装

$ sudo apt-get install git gcc vim lrzsz openssh-server meson pkg-config make unity-tweak-tool libtool m4 autoconf pkg-config

02. 下载FUSE3.3

libfuse github地址 https://github.com/libfuse/libfuse

你也可以点这里下载,下载完成后解压

$ tar -xvf libfuse-fuse-3.3.0.tar.gz

03. 编译与安装

To build and install, we recommend to use Meson and Ninja. After extracting the libfuse tarball, create a (temporary) build directory and run Meson

在之前软件包安装的时候$ sudo apt-get install meson其实就已经装了好了meson和ninja,这里就可以直接使用啦

$ cd source/root
$ meson builddir
$ cd builddir
$ ninja
$ sudo ninja install

如果sudo ninja install出现问题

出现以下报错
update-rc.d: error: unable to read /etc/init.d/fuse3

解决办法

$ ls -l /etc/init.d/fuse3  #检查文件是否存在,未找到文件
$ ls -l /usr/local/etc/init.d/fuse3  #检查是否在这里,找到文件
$ sudo cp /usr/local/etc/init.d/fuse3 /etc/init.d/fuse3 #copy
$ sudo update-rc.d fuse3 start 34 S . start 41 0 6 .
$ sudo ninja install #重新执行出错的命令

04. 修改文件/etc/ld.so.conf,在最后加上一行

include /usr/local/lib/x86_64-linux-gnu

执行

$ ldconfig -v

测试装好的FUSE

官方的hello例子

$ cd builddir/example
$ mkdir testmount
$ ls -al testmount
$ ./hello testmount #将hello文件系统挂载到testmount下
$ ls -al testmount

You should see 3 entries: ., …, and hello. We just created this directory, and thus it was empty, so where did hello come from? Obviously the hello application we just ran could have created it, but what it actually did was lie to the operating system when the OS asked for the contents of that directory. So let’s see what happens when we try to display the contents of the file.

$ cat testmount/hello

You should get the familiar hello world quotation. If we cat a file that doesn’t really exist, how do we get meaningful output? The answer comes from the fact that the hello application also gets notified of the attempt to read and open the fictional file “hello” and thus can return the data as if it was really there.
Examine the contents of hello.c in your favorite text editor, and look at the implementations of readdir and read to see that it is just returning hard coded data back to the system.
The final thing we always need to do is to unmount the file system we just used when we are done or need to make changes to the program. Do so by:

$ fusermount -u testmount

进一步的了解

The hello application we ran in the above example is a particular FUSE file system provided as a sample to demonstrate a few of the main ideas behind FUSE. The first thing we did was to create an empty directory to serve as a mount point. A mount point is a location in the UNIX hierarchical file system where a new device or file system is located. As an analogy, in Windows, “My Computer” is the mount point for your hard disks and CD-ROMs, and if you insert a USB drive or MP3 player, it will show up there as well. In UNIX, we can have mount points at any location in the tree.

Running the hello application and passing it the location of where we want the new file system mounted initiates FUSE and tells the kernel that any file operations that occur under our now mounted directory will be handled via FUSE and the hello application. When we are done using this file system, we simply tell the OS that it no longer is mounted by issuing the above fusermount -u command. At that point the OS goes back to managing that directory by itself.


了解FUSE的傻瓜接口

要实现一个用户空间的文件系统,只需用自己的方法实现FUSE的“傻瓜”接口,即可写出一个自己的文件系统,而不需要了解操作系统的内核。这一部分最好阅读hello.cpassthrough.c的源码,以入门FUSE的编程规范

另外就是,以下8个调用保证了文件系统最基本的功能:

getattr  //获取文件/目录属性
readdir  //读取目录
mkdir    //创建目录
rmdir    //删除目录
mknod    //创建文件
write    //写文件
read     //读文件
unlink   //删除文件

设计u_fs文件系统

What You Need to Do

Your job is to create the u_fs file system as a FUSE application that provides the interface described in the first section.
The u_fs file system should be implemented using an image file, managed by the real file system in the directory that contains the u_fs application. The layout of file system will be follow. We will consider the disk to have 512 byte blocks.

super blockbitmap blockdata block
(1 block)(1280 block)(all the rest block)

Super Block

Super block must be the first block of the file system. It descripts the whole file system. Info containing in super block should be:

struct sb {
    long fs_size; //size of file system, in blocks
    long first_blk; //first block of root directory
    long bitmap; //size of bitmap, in blocks
};

Directories

Directories should be also treated as a file. Each directory contains a list of u_fs_directory_entry structures. There is no limit on how many directories we can have.

struct u_fs_file_directory {
    char fname[MAX_FILENAME + 1]; //filename (plus space for nul)
    char fext[MAX_EXTENSION + 1]; //extension (plus space for nul)
    size_t fsize; //file size
    long nStartBlock; //where the first block is on disk
    int flag; //indicate type of file. 0:for unused; 1:for file; 2:for directory
};

Each directory entry will contain an 8-character maximum directory name, and then have a list of files that are in the directory.
Each file entry in the directory has a filename in 8.3 format. We also need to record the total size of the file, and the location of the file’s first block on disk.

Files

Files will be stored in a virtual disk that is implemented as a single, pre-sized file called .disk with 512 byte blocks of the format:

struct u_fs_disk_block {
    size_t size; // how many bytes are being used in this block
    long nNextBlock; //The next disk block, if needed.
                     //This is the next pointer in the linked allocation list
    char data[MAX_DATA_IN_BLOCK]; //And all the rest of the space in the block...
                                  //can be used for actual data storage.
};

Disk Management

In order to manage the free or empty space, you will need to create some bitmap blocks on the disk that record whether a given block has been previously allocated or not. The total number of bitmap blocks should depends on the size of file system. You can do this however you like.

To create a 5MB disk image, execute the following:

$ dd bs=1K count=5K if=/dev/zero of=diskimg

This will create a file initialized to contain all zeros, named diskimg. You only need to do this once, or every time you want to completely destroy the disk.
You should also write a format program to init this file, i.e. write its super block and bitmap blocks data.

Syscalls

To be able to have a simple functioning file system, we need to handle a minimum set of operations on files and directories. The functions are listed here in the order that I suggest you implement them in.

The syscalls need to return success or failure. Success is indicated by 0 and appropriate errors by the negation of the error code, as listed on the corresponding function’s man page.

/** Description:
 * This function should look up the input path to determine if it is a directory or a file.
 * If it is a directory, return the appropriate permissions.
 * If it is a file, return the appropriate permissions as well as the actual size.
 * This size must be accurate since it is used to determine EOF and thus read may not be called.
 * Return values:
 * 0 on success, with a correctly set structure
 * -ENOENT if the file is not found
 */
static int u_fs_getattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi);

/** DESCIPTION:
 * This function should look up the input path,
 * ensuring that it is a directory, and then list the contents.
 * To list the contents, you need to use the filler() function.
 * For example: filler(buf, ".", NULL, 0);
 * adds the current directory to the listing generated by ls -a
 * In general, you will only need to change the second parameter...
 * to be the name of the file or directory you want to add to the listing.
 * Return values:
 * 0 on success
 * -ENOENT if the directory is not valid or found
 */
static int u_fs_readdir(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags);

/** DESCIPTION:
 * This function should add the new directory to the root level,
 * and should update the .directories file
 * Return values:
 * 0 on success
 * -ENAMETOOLONG if the name is beyond 8 chars
 * -EPERM if the directory is not under the root dir only
 * -EEXIST if the directory already exists
 */
static int u_fs_mkdir(const char *path, mode_t mode);

/** DESCIPTION:
 * Deletes an empty directory
 * Return values:
 * 0 read on success
 * -ENOTEMPTY if the directory is not empty
 * -ENOENT if the directory is not found
 * -ENOTDIR if the path is not a directory
 */
static int u_fs_rmdir(const char *path);

/** DESCIPTION:
 * This function should add a new file to a subdirectory,
 * and should update the .directories file appropriately
 * with the modified directory entry structure.
 * Return values:
 * 0 on success
 * -ENAMETOOLONG if the name is beyond 8.3 chars
 * -EPERM if the file is trying to be created in the root dir
 * -EEXIST if the file already exists
 */
static int u_fs_mknod(const char *path, mode_t mode, dev_t rdev);

/** DESCIPTION:
 * This function should read the data in the file...
 * denoted by path into buf, starting at offset.
 * Return values:
 * size read on success
 * -EISDIR if the path is a directory
 */
static int u_fs_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi);

/** DESCIPTION:
 * This function should write the data in buf into the file...
 * denoted by path, starting at offset.
 * Return values:
 * size on success
 * -EFBIG if the offset is beyond the file size (but handle appends)
 */
static int u_fs_write(const char *path, const char *buf, size_t size, off_t offset, struct fuse_file_info *fi);

/** DESCIPTION:
 * Delete a file
 * Return values:
 * 0 read on success
 * -EISDIR if the path is a directory
 * -ENOENT if the file is not found
 */
static int u_fs_unlink(const char *path);

正式开始着手编写u_fs文件系统

概述

如果你已经花了10分钟完全了解了FUSE和u_fs的设计要求,那么GJ,不过这里还是简单总结一下

  1. 需要实现的是两级文件系统,根目录下可以有文件与目录,子目录下只能放文件
  2. 文档内mknod的要求稍微与前面一条有冲突,请各位自行斟酌,取一即可
  3. 分割路径可以用sscanf(path, "/%[^/]/%[^.].%s", directory, filename, extension),但是要注意输入路径某些地方为空的情况,请自行测试
  4. 打开了文件,记得关闭文件;malloc之后记得free,小心操作指针,小心内存泄露

具体的结构根据下面几张图片来理解吧

diskimg_init
这里注意一下super_block的数据结构实际上只有24bytes的,但也要占用一个块,因为文件系统要求是这样,所以初始化文件的时候,位图起始块offset = 512bytes

在这里插入图片描述
超级块中有个frist_blk他应该指向根目录的起始块,一般认为是1281disk_blocku_fs_file_directory的关系是这样的:
dir_structfile_struct

接口实现

我的实现比较鸡肋,都是各种顺序查找,各位大佬看看就好。关于那八个接口的实现,先简单分个组(mkdirmknod)(rmdirunlink)(readwrite)(getattrreaddir)共四组,每组的实现都基本没有太大区别,这里先介绍一下我的mkdirrmdir的实现吧

mkdir

简单来说,就是往目录最后插item,我的设计要求就是目录从头读到尾中间是不能出现空item的,你可能会问,那删目录的时候怎么办?rmdir利用了一个简单的办法,“回填”(我是这么称呼的)

rmdir

希望你看懂了这张图,我也不多解释了,结合“回填”自己理解吧

想了想,别的其实也没有什么好讲的了,实现起来都大同小异,有不明白直接阅读代码吧,附上源码,仅供大家参考,切勿复制粘贴。

运行测试

待补充

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值