AbydOS开发日记 (8) - 文件系统框架

7/3/2024 OSC++

好久不见!自从上一篇博客,上板成功,到现在已经过去了 3 个月。这几个月,主要是忙活一些学业上的东西,4月跟着老师探索了一点 IC design with LLM 的内容,5月忙活各种大作业、小组作业,6月忙活备考。终于考完试了,开完香槟就开始填坑啦!

# 虚拟文件系统

由于 Unix 的 “文件即一切” 思想实在很好,所以我们采用这样的思路。 虚拟文件系统,即 Virtual FileSystem, VFS, 是整个文件系统的抽象,向上层提供统一的访问接口。考虑简单的设计,VFS 只需实现真实文件系统的挂载、卸载,以及各种操作的代理即可。

# 基础定义

我们定义一个基类,作为基础文件系统,所有真实的文件系统都需要遵守之:

class BasicFS
{
  public:
    virtual int open(const char *path, int flags, int mode) = 0;
    virtual int close(int fd) = 0;
    virtual int read(int fd, void *buf, size_t count) = 0;
    virtual int write(int fd, const void *buf, size_t count) = 0;
    virtual int lseek(int fd, off_t offset, int whence) = 0;
    virtual int fstat(int fd, struct stat *buf) = 0;
    // virtual int stat(const char *path, struct stat *buf) = 0;

    // virtual int opendir(const char *path) = 0;
    // virtual int readdir(int fd, struct dirent *dirp) = 0;
    // virtual int closedir(int fd) = 0;

    // virtual int mkdir(const char *path, mode_t mode) = 0;
    // virtual int rmdir(const char *path) = 0;

    virtual ~BasicFS() = default;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 初始框架

针对 VFS 的初步功能,我们需要维护三张表,如下:

  1. 文件系统驱动程序表,存放文件系统驱动程序的挂载、卸载函数
  2. 虚拟挂载点表,将挂载点路径映射到对应的文件系统驱动对象
  3. 全局文件描述符表,将全局 fd 号映射到文件系统驱动对象及内部 fd 号

这里的设计类似于 DriverManager,但是不同的是,一个文件系统驱动可以拥有多个对象(在不同的挂载点上),而一个对象可以持有多个fd,是一个 1Ⓜ️n 的关系,而 DriverManager 是一个 1:m 的关系,即一个驱动对应多个设备。故而,引入工厂模式,通过专用函数进行构造和销毁,减轻 FS 内部管理的负担。同样的,我们本来可以委派一个 fd 给真实的 FS,但是这样并没有多大用处,只是转换的层级下放到了真实的 FS 中。

我们定义这样的类:

constexpr int FILE_STDIN = 0;
constexpr int FILE_STDOUT = 1;
constexpr int FILE_STDERR = 2;

class VirtualFS
{
  public:
    using newInstanceFunc_t = std::function<std::pair<int, BasicFS *>(const char *devicePath)>;
    using deleteInstanceFunc_t = std::function<int(BasicFS *)>;

    VirtualFS();
    ~VirtualFS();

    static int registerFS(const std::string &fs_name, newInstanceFunc_t nf, deleteInstanceFunc_t df);
    static int unregisterFS(const std::string &fs_name);

    static int open(const char *path, int flags, int mode);
    static int close(int fd);
    static int read(int fd, void *buf, int count);
    static int write(int fd, const void *buf, int count);
    static int lseek(int fd, off_t offset, int whence);
    static int fstat(int fd, struct stat *buf);

    static int mount(const char *path, const char *devicePath, int flags, int mode, const char *fs_name = nullptr);
    static int umount(const char *path);

  private:
    static std::map<std::string, std::pair<BasicFS *, deleteInstanceFunc_t>> _fs_map;
    static std::vector<std::tuple<std::string, newInstanceFunc_t, deleteInstanceFunc_t>>
        _fs_factories; // fs-name, new-instance-func, delete-instance-func
    static lock_t _fs_lock;
    static std::map<int, std::pair<BasicFS *, int>> _global_fd_map;
    static int _global_fd_counter;

};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

Note

遵守 POSIX 的规范,我们预留头三个文件号给 STDIO。 所有操作都需要加锁,未来可以考虑细化锁的粒度。

# 文件系统驱动的注册与反注册

类似于设备驱动的,利用 global constructor 进行注册,要求我们的列表先于文件系统驱动初始化,详细请参阅设备驱动框架的描述。 在 registerFS() 中,我们只需做参数校验并加入 _fs_factories;而反注册,只需找出来名称对应的项删除即可。 需要注意的是,反注册一个 FS 并不会将其对应的对象销毁,而只能避免安装新的对象,虽然不符合一些原则,但是我们的系统目前没有动态加载和卸载内核模块的办法,先做到这里也无可厚非。

# 文件系统的挂载与卸载

对于挂载,我们遍历所有的安装函数,传入一个挂载设备字符串,驱动程序根据字符串寻找设备对象并进行操作。随后,如果挂载成功,返回一个 0 错误码;若不是支持的文件系统,则返回不支持;若挂载过程出现了错误,则返回错误码。当不支持时,遍历继续,否则终止。

    static int mount(const char *path, const char *devicePath, int flags, int mode, const char *fs_name = nullptr)
    {
        _fs_lock.lock();
        for (auto &fs : _fs_factories)
        {
            if ((fs_name && std::get<0>(fs) == fs_name) || !fs_name)
            {
                auto [ret, fs_instance] = std::get<1>(fs)(devicePath);
                if (ret == K_OK)
                {
                    _fs_map.insert(std::make_pair(path, std::make_pair(fs_instance, std::get<2>(fs))));
                    _fs_lock.unlock();
                    return 0;
                }
                if (ret != K_ENOTSUPP) // Something other than not supported happened
                {
                    _fs_lock.unlock();
                    return ret;
                }
            }
        }
        _fs_lock.unlock();
        return K_ENOTSUPP; // No FS could be mounted
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

对于卸载,简单的就是找到挂载点对应的对象,进行解除安装。已打开的文件暂时不受影响,以后完善。

    static int umount(const char *path) // After unmounting, the FS instance is deleted
    {
        _fs_lock.lock();
        auto it = _fs_map.find(path);
        if (it != _fs_map.end())
        {
            it->second.second(it->second.first);
            _fs_map.erase(it);
            _fs_lock.unlock();
            return 0;
        }
        _fs_lock.unlock();
        return K_ENOTSUPP;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 操作代理

主要涉及 open() close() read() write() lseek() 等。

只要维护全局文件描述符表并对应调用真实文件系统服务程序即可。此处以 open() 为例:

    static int open(const char *path, int flags, int mode)
    {
        // printf("Open %s", path);
        _fs_lock.lock();
        BasicFS *fs = nullptr;
        size_t max_len = 0;
        for (auto x : _fs_map)
        {
            if (std::string_view(path).find(x.first) == 0)
            {
                if (max_len < x.first.size())
                {
                    max_len = x.first.size();
                    fs = x.second.first;
                }
            }
        }
        if (fs)
        {
            int ret = fs->open(path+max_len, flags, mode);
            if (ret >= 0)
            {
                auto fd = _global_fd_counter++;
                _global_fd_map.insert(std::make_pair(fd, std::make_pair(fs, ret)));
                _fs_lock.unlock();
                return fd;
            }

            _fs_lock.unlock();
            return ret;
        }
        // printf("Failed!\n");
        _fs_lock.unlock();
        return K_ENOENT;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

各文件的锁以及权限等在真实文件系统驱动中维护,因其多样性。

# 最简单的文件系统实现:CPIO

CPIO 是 Linux 上用于同步文件的一种格式,完整地记录了文件在文件系统中的各个状态。其归档被用于 initrd,读取的代码总共也没有 500 行,也是只读的。要实现它,我们先引入 libcpio,然后适配我们的文件系统框架即可。

详细实现

#include <cstdint>
#include <cstring>
#include <stdexcept>

#include "libcpio/libcpio.h"
#include "k_vfs.h"
#include "k_defs.h"

class CPIOFS : public BasicFS
{
  public:
    CPIOFS(void *archive, unsigned long len) : _archive_len(len)
    {
        printf("%p, %lu\n", archive, len);
        if (cpio_info(archive, len, &_info))
        {
            throw std::runtime_error("cpio_info failed");
        }
        else
        {
            printf("[CPIOFS] cpio_info: %d files, %d max_path_size\n", _info.file_count, _info.max_path_sz);
        }

        _archive = new uint8_t[len];
        memcpy(_archive, archive, len);
    }
    ~CPIOFS()
    {
        if (_archive)
            delete[] _archive;
        _archive = nullptr;
    }

    int open(const char *path, int flags, int mode) override
    {
        unsigned long size = 0;
        const void *file = cpio_get_file(_archive, _archive_len, path, &size);
        if (!file)
        {
            return K_ENOENT;
        }
        for (int i = 0; i < MAX_FILES; i++)
        {
            if (!_fcb[i].used)
            {
                _fcb[i].used = true;
                _fcb[i].file = (void *)file;
                _fcb[i].size = size;
                _fcb[i].lpos = 0;
                return i;
            }
        }
        return K_ENOMEM;
    }
    int close(int fd) override
    {
        if (fd >= MAX_FILES || fd < 0)
            return K_ENOENT;
        if (!_fcb[fd].used)
            return K_ENOENT;
        _fcb[fd].used = false;
        _fcb[fd].file = nullptr;
        _fcb[fd].size = 0;
        _fcb[fd].lpos = 0;
        return K_OK;
    }

    int read(int fd, void *buf, size_t count) override
    {
        if (fd >= MAX_FILES || fd < 0)
            return K_ENOENT;
        if (!_fcb[fd].used)
            return K_ENOENT;
        if (_fcb[fd].lpos + count > _fcb[fd].size)
            count = _fcb[fd].size - _fcb[fd].lpos;
        memcpy(buf, (uint8_t *)_fcb[fd].file + _fcb[fd].lpos, count);
        _fcb[fd].lpos += count;
        return count;
    }

    int write(int fd, const void *buf, size_t count) override
    {
        return -1; // No write should be made to a CPIO archive
    }
    int lseek(int fd, off_t offset, int whence) override
    {
        if (fd >= MAX_FILES || fd < 0)
            return K_ENOENT;
        if (!_fcb[fd].used)
            return K_ENOENT;
        if (whence == SEEK_SET)
        {
            if (offset < 0)
                return K_EINVAL;
            if (offset > (long long)_fcb[fd].size)
                return K_EINVAL;
            _fcb[fd].lpos = offset;
        }
        else if (whence == SEEK_CUR)
        {
            if (_fcb[fd].lpos + offset < 0)
                return K_EINVAL;
            if (_fcb[fd].lpos + offset > _fcb[fd].size)
                return K_EINVAL;
            _fcb[fd].lpos += offset;
        }
        else if (whence == SEEK_END)
        {
            if (offset > 0)
                return K_EINVAL;
            if (offset < -(long long)_fcb[fd].size)
                return K_EINVAL;
            _fcb[fd].lpos = _fcb[fd].size + offset;
        }
        else
        {
            return K_EINVAL;
        }
        return K_OK;
    }
    int fstat(int fd, struct stat *buf) override
    {
        return -1;
    }

  private:
    static constexpr int MAX_FILES = 1024;

    uint8_t *_archive = NULL;
    size_t _archive_len = 0;
    struct cpio_info _info;

    struct fcb_t
    {
        bool used = false;
        void *file = nullptr;
        unsigned long size = 0;
        unsigned long lpos = 0;
    };

    std::array<fcb_t, MAX_FILES> _fcb;
};

// Register the filesystem
FS_INSTALL_FUNC(K_PR_FS_BEGIN) static void fs_register()
{
    VirtualFS::registerFS(
        "cpiofs",
        [](const char *devicePath) -> std::pair<int, BasicFS *> {
            try
            {
                size_t addr = 0, len = 0;
                if (sscanf(devicePath, "%lu,%lu", &addr, &len) != 2 && sscanf(devicePath, "%lx,%lx", &addr, &len) != 2)
                {
                    throw std::runtime_error("invalid devicePath specified");
                }
                auto ret = new CPIOFS((void *)addr, len);
                return {0, ret};
            }
            catch (std::exception &e)
            {
                printf("[CPIO] Failed to create CPIOFS: %s\n", e.what());
                return {-1, nullptr};
            }
            catch (...)
            {
                printf("[CPIO] Failed to create CPIOFS: unknown exception\n");
                return {-2, nullptr};
            }
        },
        [](BasicFS *fs) -> int {
            delete fs;
            return 0;
        });
    printf("FS CPIO installed\n");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177

我们限定从内存中读取归档,需要一个起始地址和长度,从安装设备路径字符串获得。

# LibC 的适配

我们想要比较容易在 OS 层进行使用文件系统,则需要进行 LibC 的适配。其实只需要将相关函数重写到 VFS 处理即可:

    // Hook with libc
    int _write(int fd, char *buf, int size)
    {
        return VirtualFS::write(fd, buf, size);
    }

    int _read(int fd, char* buf, int size){
        return VirtualFS::read(fd, buf, size);
    }

    int _close(int fd)
    {
        return VirtualFS::close(fd);
    }

    int _open(const char *path, int flags, int mode)
    {
        return VirtualFS::open(path, flags, mode);
    }

    int _lseek(int fd, off_t offset, int whence)
    {
        return VirtualFS::lseek(fd, offset, whence);
    }

    int _fstat(int fd, struct stat *buf)
    {
        return VirtualFS::fstat(fd, buf);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

值得一提的是,之前我们直接重写 _write() 重定向 stdout 到串口,改动之后只需在 VFS 加入一个判断并写在那里即可。

# 测试

我们首先生成一个 cpio 归档,如下将 2.txt 加入:

echo 2.txt | cpio -o -H newc > 1.cpio
1

注意cpio 也有多种格式,这里使用 newc 变体。

随后我们修改 sysroot 解析 initrd 参数,

    prop = fdt_get_property(fdt, rc, "linux,initrd-start", &len); // Just compatible with Linux
    if (prop)
    {
        size_t t = 0, e = 0;
        if (len == 4)
        {
            t = fdt32_to_cpu(*((fdt32_t *)(prop->data)));
        }
        if (len == 8)
        {
            t = fdt64_to_cpu(*((fdt64_t *)(prop->data)));
        }
    
        prop = fdt_get_property(fdt, rc, "linux,initrd-end", &len); // Just compatible with Linux
        if (prop)
        {
            if (len == 4)
            {
                e = fdt32_to_cpu(*((fdt32_t *)(prop->data)));
            }
            if (len == 8)
            {
                e = fdt64_to_cpu(*((fdt64_t *)(prop->data)));
            }
    
            if (t && e && e > t)
            {
                _initrd_path = std::to_string(t) + "," + std::to_string(e - t);
            }
        }
    }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

然后在 boot 中挂载之:

    // Mount rootfs
    auto rootfs = sysroot->initrd();
    if (rootfs.empty())
    {
        std::cout << "[W] Rootfs not specified!" << std::endl;
    }else{
        std::cout << "Mounting rootfs: " << rootfs << std::endl;
        auto rc = VirtualFS::mount ("/", rootfs.c_str(), 0, 0);
        if (rc < 0)
        {
            std::cout << "[E] Failed to mount rootfs: " << rc << std::endl;
            return rc;
        }
        std::cout << "[I] Rootfs mounted successfully!" << std::endl;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

接着在 k_main 中就可以直接使用 STL 的文件操作了:

    std::ifstream f("/2.txt");
    if(f.is_open())
    {
        std::string line;
        while(getline(f, line))
        {
            printf("!!!Reading from 2.txt: %s\n", line.c_str());
        }
        f.close();
    }
    else
    {
        printf("Failed to open file\n");
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

编译运行,

qemu-system-riscv64 -M virt -m 256M -nographic -bios build/opensbi/build/platform/generic/firmware/fw_jump.elf -kernel build/AbydOS_KNL -initrd build/1.cpio
1

得到:

Mounting rootfs: 2282749952,512
0x88100000, 512
[CPIOFS] cpio_info: 1 files, 5 max_path_size
[I] Rootfs mounted successfully!
> Booting harts... (Boot hart = 0)
> Switching to multicore mode
Timer interrupt for hart 0
Current Time: 2216260
Hello from hart 0!
!!!Reading from 2.txt: 114514
1
2
3
4
5
6
7
8
9
10

至此,文件系统框架搭建完成,也适配了 initrd,可以用于后续测试而不必急于编写真的文件系统驱动和存储设备驱动,总共耗时:一天。