//xia仔k:基于C++从0到1手写Linux高性能网络编程框架
准备
前面几篇中我们曾经树立起了 process 和系统调用的框架,并且曾经完成了第一个 fork
系统调用。到目前为止,一切的 process 和它们的 threads 都是我们在 kernel 里手动创立,thread 的工作函数也是提早准备好的固定函数,这只是地道给测试用的。一个真正的 OS 当然需求有才能加载用户提供的程序到 process 中运转,这会用到我们要完成的第二个系统调用 exec
。
但是在此之前,还有一项准备工作要做。既然要加载用户程序,那么当然需求从磁盘加载。目前我们的 kernel 尚不具备和磁盘交互的才能,本篇就来完成一个十分简单的文件系统。
文件系统
文件系统(file system
)这个词常常带有二义性,在不同的语境里有不同的含义,所以初学者常常混杂。例如我们经常听到的 windows 系统的 FAT
,NTFS
文件系统,Linux 系统的 EXT
文件系统,有时分你又会听到 Linux 的虚拟文件系统 VFS (Virtual File System
)等。
计算机世界里有句话,任何技术问题都能够经过加一个中间层来处理,Linux 的 file system
架构正是圆满地表现了这种哲学。你听到的上面的各种术语,都只是分属于整个大 file system
概念下的不同的分层中。
下面我们分别来看这三层的详细职责。
Virtual File System
从顶向下,顶层的 Virtual File System
是 Linux 内核构建出来的一个笼统的文件系统,它实践上能够大致地对应我们平常看到的系统中的文件和目录等:
yaml
bash> ls -l /
drwxr-xr-x 2 root root 4096 Jan 13 2019 bin
drwxr-xr-x 4 root root 4096 Jan 11 2019 boot
drwxr-xr-x 3 root root 4096 Feb 3 2020 data
/usr/bin/cat
/home/foo/hello.txt
这一层最接近我们用户心理概念上的文件系统,但它其实是笼统的,由于你并不晓得这些文件底下的设备和存储格式,作为用户也并不需求关怀,VFS 屏蔽了这些底层的细节,所以这一层叫 Virtual
文件系统。VFS 从逻辑上看是一个树状构造,顶端是根目录 /
,每个节点可能是目录(灰色)或者普通文件(绿色)。
存储文件系统
VFS 中每一个节点的文件或者目录都是笼统的,它们都要对应到详细的存储设备(如磁盘)上的文件实体,这就是 VFS 之下的那一层所管理的。例如我们常听到的 EXT2
,NTFS
等,它们虽然在术语上也叫 file system
,但它们描绘的是文件在硬件上的存储和组织方式,所以它的名字更应该叫"存储文件系统"(storage system
)。磁盘,和内存一样,上面的数据不是杂乱无章的,它们必然是以某种构造组织起来的,这样上层才干依据它的标准去解析并正确地索引到想要的数据。
例如 EXT2 文件系统格式:
EXT2 的整个存储空间会被分为若干个 block group
,然后每个 group 内部再组织文件的存储,包含了各种 meta 信息,以及最重要的 inode
,它对应于每一个文件,用于存储每个文件的根本 meta 信息,以及指针指向文件详细的数据块(蓝色局部)。
这个存储系统的内部事实上也会组织起目录层级的概念,例如有些 inode 是普通文件,有些则是目录,目录会指引你去寻觅到它下层的 inode。整个磁盘文件系统就像是一本书的索引,通知你如何去找到一个文件的数据在那里。
存储文件系统,普通是树立在我们平常所说的磁盘分区(partition
)上的,例如 windows 里到的 C 盘 D 盘,Linux 里的 dev/hda1
,/dev/sda1
等。我们平常所说的磁盘格式化
,就是指将磁盘某个分区,按某种存储文件系统的格式给初始化,相似于在磁盘分区上张起一张逻辑上的构造网。
存储文件系统有很多品种型,EXT2
只是其中一种。我们以至能够本人定制一种文件系统。在本项目中,我们会完成一个最简单的文件系统并且运用它来制造用户磁盘镜像。
硬件驱动层
再往下一层是硬件 IO 层,也就是硬件驱动,它直接和硬件交互。它这里曾经没有数据组织和存储逻辑上的任何概念了,地道是一个呆板的 IO,例如你通知它,我需求读取硬盘上从位置 x 到位置 y 的数据,或者我需求在硬盘上位置 w 到位置 z 的范围内写入什么什么数据。
访问一个文件
一个存储文件系统,或者说磁盘分区,是如何被放进 VFS 组织的这个树状构造里去的呢?在 Linux 里这叫挂载(mount
),例如一开端对 VFS 而言,整棵树都是空的,只要一个根节点 /
,但是我们普通肯定会有一个系统分区,例如 /dev/sda1
,也就是通常你 Linux 装机用的分区,这个分区是一个 EXT2 文件系统,它会被 mount
到 VFS 的根目录 /
上去,这样 VFS 就能够开端查询 /
里的目录和文件了。
例如用户需求读取一个文件:
arduino
/home/hello.txt
系统会把这个目录从前往后,一级一级地查询:
/
是根目录,它如今被 mount 了/dev/sda1
这个分区,并且该分区是 EXT2 存储格式的,于是系统就依照 EXT2 系统的格式,去查询这个分区顶层里名字为 home 的节点;留意这里 VFS 有树状构造,EXT2 其实里也有树状构造,它也是能够从顶向下查询的;- 在 EXT2 顶层找到了 home 这个节点,发现它的确是一个目录类型的节点,没问题;然后查找 home 目录下的
hello.txt
文件,假如能找到,那么读取之;
这里一直是在依照 EXT2 系统的格式,一级一级地在 /dev/sda1
这个分区上查找;固然 VFS 里的途径是一个笼统的概念,但在真正访问文件时,这个途径会被投射到它所 mount 的磁盘分区的文件系统里去查询。
以上的例子只是挂载了单一的磁盘分区,事实上 Linux 下能够在 VFS 上找一个目录节点挂载新的磁盘分区,以至这个分区都不用是 EXT 格式的,只需内核能支持解析这个格式。例如我们有个磁盘分区 /dev/hda2
,它是 NTFS 格式的(例如你的双系统 windows 上的 D:\ 盘),我们将它 mount
到 VFS 的 /mnt
这个节点上:
这个新的磁盘分区 mount 上去后,从 VFS 的视角来看,它就能从 mnt
开端以 NTFS 文件系统的格式向下访问,例如读取这个文件:
bash
/mnt/bar
当 VFS 访问到 mnt
节点的时分,发现这是一个 mount 点,并且挂载的磁盘分区是一个 NTFS 文件系统,接下来就会以 NTFS 的格式去解析接下来的途径 - 它会去尝试查找并读取这个磁盘分区上的 /bar
途径。
file system 接口
上面讲到了,VFS 在访问不同节点上的文件时,会跟踪它是属于哪一个磁盘分区以及该分区是什么存储文件系统(如 EXT,NTFS),然后运用对应的文件系统格式去读取磁盘分区数据。这里 VFS 为了兼容各种不同的文件系统,在完成上会首先定义一系列统一的文件操作的接口,然后各种详细的不同品种的文件系统再各自去完成这些接口,这是典型的面向对象编程的范式,例如:
arduino
class FileSystem {
public:
int32 read_file(const char* filename,
char* buffer,
uint32 start,
uint32 length) = 0;
int32 write_file(const char* filename,
const char* buffer,
uint32 start,
uint32 length) = 0;
int32 stat_file(const char* filename,
file_stat_t* stat) = 0;
// ...
}
以上用 C++ 代码做一个演示(当然内核是用 C 言语写的,这里只是为了演示它面向对象编程的形式),定义了笼统类 FileSystem,里面定义了各种文件操作接口,都是纯虚函数。各种详细的文件系统只需求继承并完成这些接口,例如:
arduino
class Ext2FileSystem : public FileSystem {
public:
int32 read_file(const char* filename,
char* buffer,
uint32 start,
uint32 length) ;
// ...
}
再次声明一下,以上只是为了演示用,真正的 Linux 的 VFS 里的接口和完成当然没这么简单,但构造是相似的。
代码完成
这个项目里不会运用 EXT 那样复杂的文件系统,也不会完成完好的 VFS 功用,只会将它根本的框架搭建起来,并嵌入一个我们本人定制的十分简单的存储文件系统。
相似上面的笼统类那样,在 src/fs/vfs.h
文件中:
ini
struct file_system {
enum fs_type type;
disk_partition_t partition;
// functions
stat_file_func stat_file;
list_dir_func list_dir;
read_data_func read_data;
write_data_func write_data;
};
typedef struct file_system fs_t;
能够看到上面定义了各种文件操作的函数指针作为接口,它们 原型是:
arduino
typedef int32 (*stat_file_func)(const char* filename,
file_stat_t* stat);
typedef int32 (*list_dir_func)(char* dir);
typedef int32 (*read_data_func)(const char* filename,
char* buffer,
uint32 start,
uint32 length);
typedef int32 (*write_data_func)(const char* filename,
const char* buffer,
uint32 start,
uint32 length);
naive_fs 完成
我们不用完成一个 EXT 那样复杂的存储文件系统,在这个项目里我们只完成一个十分简单的文件系统,它的功用十分有限:
- 磁盘镜像数据提早刻好,只能读,不能写;
- 只要一层根目录,没有下级目录;
我们定制这个文件系统的目的一是为了演示,二是为了给项目运用,我们需求用它来保管用户程序以供加载并运转,所以只需求能读就能够了,也不需求复杂的目录构造,一层就足够了,一切的文件全放在这层。虽然十分低级,但它依然不失为一个文件系统,我们无妨将它命名为 naive_fs
,由于它真的十分 naive,十分 simple。
naive_fs
的存储构造是这样的:

- 头部绿色局部是一个整数,记载总文件数量,这也是固定的;
- 后面灰色局部是每个文件的 meta 信息;
- 最后蓝色局部是详细的文件数据,用每个文件的 meta 信息(
file offset
,file size
)能够定位到它的数据存储在什么位置;
你会发现这个其实和我们之前完成的 heap 有异曲同工之处,都是十分简单直白的 meta + data
构造。
我写了一个工具,它会读取 user/progs
目录下的一切文件(这个目录目前还不存在,下一篇我们会编译链接用户程序放在这里),然后将它们依照上面 naive_fs 文件系统的格式,将它们写入磁盘镜像文件 user_disk_image
,再将镜像文件一并刻写进我们的 kernel 磁盘镜像 srcoll.img
里就能够了。
bash
dd if=user/user_disk_image of=scroll.img bs=512 count=2048 seek=2057 conv=notrunc
写入位置从磁盘的第 2057 个 sector 开端,由于前面是 boot loader 和 kernel 镜像。
接着我们来完成 naive_fs
的代码,实践上就是上面的各个函数指针的完成
ini
static fs_t naive_fs;
void init_naive_fs() {
naive_fs.type = NAIVE;
naive_fs.stat_file = naive_fs_stat_file;
naive_fs.read_data = naive_fs_read_file;
naive_fs.write_data = naive_fs_write_file;
naive_fs.list_dir = naive_fs_list_dir;
// load file metas to memory.
// ...
}
init_naive_fs
函数里,将一切文件的 meta 局部都读入并保管在内存,相似一份文件名单,然后 read
write
stat
等各种函数就根据这些文件的 meta 信息完成对文件的操作,十分简单。
例如读文件,先依据文件名找到 meta,得到文件在磁盘上的偏移量和大小,再调用底层驱动去读取数据:
ini
static int32 naive_fs_read_file(char* filename,
char* buffer,
uint32 start,
uint32 length) {
// Find file meta by name.
naive_file_meta_t* file_meta = nullptr;
for (int i = 0; i < file_num; i++) {
naive_file_meta_t* meta = file_metas + i;
if (strcmp(meta->filename, filename) == 0) {
file_meta = meta;
break;
}
}
if (file_meta == nullptr) {
return -1;
}
uint32 offset = file_meta->offset;
uint32 size = file_meta->size;
if (length > size) {
length = size;
}
// Read file data from disk.
read_hard_disk((char*)buffer, naive_fs.partition.offset + offset + start, length);
return length;
}
磁盘驱动
我们还要完成最底层的磁盘 IO 驱动,这是上层的 naive_fs
需求调用的,由于我们只需求读磁盘的功用就能够了。为了简单起见,这里的底层 IO 我们依然沿用了相似 boot loader 里的读磁盘函数,经过操作磁盘管理设备的各个端口完成,这是一个同步的完成方式。真正的操作系统对磁盘的 IO 的处置肯定是异步的,由于磁盘的速度十分慢,系统不可能阻塞等候它,而是发出读写命令后就继续处置其它事情,然后磁盘管理设备经过中缀的方式来通知系统数据 IO 终了,数据曾经 ready。
总结
以上我们完成了一个简单的 VFS 和文件系统 naive_fs
,下面看下 kernel 是如何运用它来读取一个文件的,例如:
scss
char* buffer = (char*)kmalloc(1024);
read_file("hello.txt", buffer, 0, 100);
它调用的是顶层 VFS 的接口
arduino
int32 read_file(char* filename, char* buffer, uint32 start, uint32 length) {
fs_t* fs = get_fs(filename);
return fs->read_data(filename, buffer, start, length);
}
VFS 会依据给定的文件途径 filename
,定位它是属于哪一个文件系统,底下对应哪一个磁盘分区。当然我们这里只挂载了一个独一的分区,文件系统类型就是 naive_fs
, 直接返回 naive_fs 的实体:
arduino
fs_t* get_fs(char* path) {
return get_naive_fs();
}
接下来就是用这个 fs 的读文件函数接口 read_data
,读取文件。
本篇是对文件系统 File System
的一个整体架构的分层拆解和样例完成,十分简单初级,仅供演示运用,希望它能协助你对操作系统是如何管理文件和底层存储有个全面的认知。