基于Fuse的最简单的文件系统

从今天开始,我打算挖一个比较大的坑。我计划在“数据存储张”创建一个合集,介绍如何从零开始开发一个文件系统。整个开发工作基于Ubuntu 20.04,当然其它Linux版本也没有问题。对于文件系统的开发,我们通常的理解是文件系统是一个内核模块。但是实际情况是文件系统并不一定是内核模块,比如EMC的UFS64就是一个用户态的文件系统。同时,很多开源的分布式文件系统也都是用户态的。

鉴于内核态开发门槛较高,所以我暂时不打算基于Linux内核来开发,而是借助FUSE来开发一个用户态的文件系统。通过本合集的实践,希望大家能够对本人拙著《文件系统技术内幕》中的内容能够有更加深刻的理解。其实无论是内核态的文件系统也好,用户态的文件系统也罢,其原理是相同的。

虽然暂时不打算基于内核开发,但基于用户态的内容实现完成后,如果大家对Linux内核文件系统的实现感兴趣,我也会继续基于内核实现一些功能,让大家对内核文件系统有一个比较全面的理解。

好了,言归正传,我们回到本文的正题,继续我们的文件系统之旅。在具体实现文件系统之前,我觉得有必要介绍一些基本概念,这样大家对理解后续内容会有所帮助。

什么是文件系统

首先我们介绍一下什么是文件系统。文件系统是一个将硬盘的线性地址转换为层级结构的软件系统,其核心是给用户呈现层级结构的目录树(如下图所示)。在文件系统中有两个非常重要的概念,一个是文件,另外一个是目录(或者文件夹)。其中目录是一个容器,可以存储文件或者目录(称为子目录)。文件是存储数据的实体,我们的数据都是以文件的形态进行存储的。文件有很多种类,比如视频文件、音频文件、Word文档和文本文件等等。


什么是文件系统

文件系统的API

在普通用户角度,文件系统提供了一个层级结构的文件组织方式。从程序开发的角度,文件系统提供一套API来访问文件和目录。比如文件的打开、关闭、读取数据和写入数据等,目录的打开、读取(遍历)和关闭等等,具体如下所示是这些API的一个子集。对于上述API,在内核态都有对应的API来实现具体的功能。理解这些API很重要,因为FUSE正是在内核态对这些API的请求进行了截获,然后转发到用户态来处理的。

功能描述 Linux API
打开文件 open
向文件写数据 write
从文件读数据 read
关闭文件 close
移动文件指针位置 lseek
获取文件属性 stat
遍历目录 readdir

什么是FUSE

FUSE实现了一个在用户态开发文件系统的框架,有了这个框架,我们可以在用户态开发文件系统的逻辑,而不用关心Linux内核的相关内容。从而大大降低了开发文件系统的门槛。

FUSE本身包含一个用户态的库和一个内核模块。内核态模块与VFS及其他文件系统的关系如图所示,可以理解FUSE为一个内核态的文件系统。内核态的文件系统,并不会将数据持久化,其功能是将文件系统访问请求转发到用户态。

用户态库提供了一套API,同时提供了一套接口规范,这套规范实际上是一组函数集合。基于FUSE开发文件系统就是实现FUSE定义的函数集合的某些或者全部函数。然后调用FUSE用户态库的API将实现的函数注册到内核模块中。在下图的示例中,ceph_fuse就是基于FUSE开发的一个用户态的文件系统,用于实现对CephFS的访问的。如果我们基于FUSE开发自己的文件系统,也是位于这个位置的。

image.png

前文已述,内核态的模块基于VFS实现了一个文件系统,可以与Ext4、NFS或者CephFS主机端的文件系统对比理解。但不同的是,当有用户请求达到该文件系统时,该文件系统不是访问磁盘或者是通过网络发送请求,而是调用用户态注册的回调函数。

如果大家对前文NFS的流程,或者CephFS的流程熟悉的话,理解FUSE工作原理就非常简单了。与NFS类比,FUSE将NFS中通过网络转化请求换成了通过函数调用(严格来说并非简单的函数调用,因为涉及内核态到用户态的转换)来转化请求。

实现一个不是文件系统的文件系统

今天我们将开发一个最简单的文件系统,严格来说都不是一个文件系统。这里我们只是模拟一个文件系统的层级结构。在这个文件系统中包含一个目录和一个文件,目录的名称为“dir”,文件的名称为“helloworld”。同时,按照Linux的惯例,这里还包含当前目录“.”和上一级目录“..”。该文件系统的目录结构如下图所示。

我们实现的文件系统

虽然这里是一个层级结构,但是这个文件系统并不能像其他文件系统那样在目录中创建文件,也不能删除文件,更不能读取文件的内容。为什么?原因很简单,因为我们并没有实现上述功能。

接下来我们来看一下这个文件系统的具体实现。所有文件都在一个名称为helloworld的目录当中,该目录中的文件与作用如下图所示。其中3个文件(Fuse.cpp、Fuse.h和Fuse-impl.h)是FUSE的C++封装,这里我们借用了James的实现,感谢他这方面的工作。另外3个C++源代码文件是本文件系统的实现,其中helloworld.cpp是主函数的源文件,其实没有太多内容,helloworldFS.cpp/h是文件系统的实现。CMakeLists.txt是cmake的源文件,用于管理整个工程。

本工程代码结构

如下是文件系统的具体实现,可以看到这里只实现了2个函数,分别是getattr和readdir。其中readdir用于读取目录项,getattr用于获取文件或者目录的详细属性。这两个函数正式命令行工具ls会调用到的命令。

/* Description: HelloWorld filesystem class implementation
 *      This class implements the core API that is related to the directory and file.
 *      In this class, we implement a very simple file system. 
 */

#include "helloworldFS.h"

#include <iostream>
#include <string>

// include in one .cpp file
#include "Fuse-impl.h"

using namespace std;

static const string root_path = "/";
static const string hello_str = "Data Storage Zhang!\n";
static const string hello_path = "/helloworld";
static const string dir_path = "/dir";

int HelloWorldFS::getattr(const char *path, struct stat *stbuf, struct fuse_file_info *)
{
    int res = 0;

    memset(stbuf, 0, sizeof(struct stat));
    if (path == root_path) {  //根目录的属性
        stbuf->st_mode = S_IFDIR | 0755;
        stbuf->st_nlink = 2;
    } else if (path == hello_path) { //helloworld文件的属性
        stbuf->st_mode = S_IFREG | 0444;
        stbuf->st_nlink = 1;
        stbuf->st_size = hello_str.length();
    } else if (path == dir_path) { //dir目录的属性
        stbuf->st_mode = S_IFDIR | 0755;
        stbuf->st_nlink = 2;
    } else
        res = -ENOENT;

    return res;
}

int HelloWorldFS::readdir(const char *path, void *buf, fuse_fill_dir_t filler,
                           off_t, struct fuse_file_info *, enum fuse_readdir_flags)
{
    if (path != root_path)
        return -ENOENT;

    filler(buf, ".", NULL, 0, FUSE_FILL_DIR_PLUS); 
    filler(buf, "..", NULL, 0, FUSE_FILL_DIR_PLUS);
    filler(buf, hello_path.c_str() + 1, NULL, 0, FUSE_FILL_DIR_PLUS);
    filler(buf, dir_path.c_str() + 1, NULL, 0, FUSE_FILL_DIR_PLUS);

    return 0;
}

头文件的实现更加简单,如下是头文件的源代码。在头文件中只是定义了一个文件系统类HelloWorldFS,并定义了上述两个函数。

// Hello filesystem class definition
#ifndef __HELLOFS_H_
#define __HELLOFS_H_

#include "Fuse.h"
#include "Fuse-impl.h"

class HelloWorldFS : public Fusepp::Fuse<HelloWorldFS>
{
public:
  HelloFS() {}
  ~HelloFS() {}

  static int getattr (const char *, struct stat *, struct fuse_file_info *);
  static int readdir(const char *path, void *buf,
                     fuse_fill_dir_t filler,
                     off_t offset, struct fuse_file_info *fi,
                     enum fuse_readdir_flags);
};

#endif

如下是主函数所在源文件的内容,实现也是比较简单的。这里实例化了HelloWorldFS类,并调用run函数。这个函数完成了文件系统挂载,函数注册等任务。具体涉及FUSE实现的内容,我们后面再详细介绍这方面的内容。

// See  FUSE:  example/hello.c

#include "helloworldFS.h"

int main(int argc, char *argv[])
{

  HelloWorldFS fs;

  int status = fs.run(argc, argv);

  return status;
}

如下是cmake的工程文件,用于生成Makefile文件。关于cmake的概念及使用方法可以阅读本号之前的文章,本文不在赘述相关内容。

cmake_minimum_required(VERSION 3.16)
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --std=c++14 -D_FILE_OFFSET_BITS=64 -D__coverage -I/usr/include/fuse3 -lfuse3 -lpthread")
project(CMakeSunny
    VERSION 1.0
    DESCRIPTION "A CMake Tutorial"
    LANGUAGES CXX)

add_executable(helloworld
    helloworld.cpp
    helloworldFS.cpp
    Fuse.cpp)

target_link_libraries(helloworld -lfuse3)

具备上述源文件后就可以编译源文件了。首先需要在根目录创建一个名称为build子目录。然后切换到该目录执行“cmake ..”命令。成功后会生成一个Makefile。最后,我们执行一下make命令就会编译出一个可执行程序helloworld。执行如下命令就可以将我们实现的“文件系统”挂载到/mnt/test(需要提前创建)目录了。

./helloworld /mnt/test

然后我们可以通过cd命令切换到该目录,并通过ls命令查看其中的内容。这时就可以看到我们在文章一开始看到的内容。


我们实现的文件系统

如前文所述,本文我们实现了一个假的文件系统,这个文件系统既不能创建新文件,也不能读取文件的数据,更不能向已有文件写入数据。但是通过本文,相信大家对文件系统的概念和FUSE的用法有了一个基本的了解。后面我们将以当前实现的文件系统为基础,实现一个基于内存的,可读写的文件系统。

注: 本文配套的源代码可以在github的SunnyZhang-IT/fs-from-zero库中找到。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容