共计 6593 个字符,预计需要花费 17 分钟才能阅读完成。
系列目录
- 序篇
- 筹备工作
- BIOS 启动到实模式
- GDT 与保护模式
- 虚拟内存初探
- 加载并进入 kernel
- 显示与打印
- 全局描述符表 GDT
- 中断解决
- 虚拟内存欠缺
- 实现堆和 malloc
- 第一个内核线程
- 多线程运行与切换
- 锁与多线程同步
- 进入用户态
- 过程的实现
- 零碎调用
- 简略的文件系统
- 加载可执行程序
- 键盘驱动
- 运行 shell
筹备
后面几篇中咱们曾经建设起了 process 和零碎调用的框架,并且曾经实现了第一个 fork
零碎调用。到目前为止,所有的 process 和它们的 threads 都是咱们在 kernel 里手动创立,thread 的工作函数也是提前准备好的固定函数,这只是纯正给测试用的。一个真正的 OS 当然须要有能力加载用户提供的程序到 process 中运行,这会用到咱们要实现的第二个零碎调用 exec
。
然而在此之前,还有一项筹备工作要做。既然要加载用户程序,那么当然须要从磁盘加载。目前咱们的 kernel 尚不具备和磁盘交互的能力,也没有 VFS 中每一个节点的文件或者目录都是形象的,它们都要任何文件系统,本篇就来实现一个非常简单的文件系统。
文件系统
文件系统(file system
)这个词往往带有二义性,在不同的语境里有不同的含意,所以初学者往往混同。例如咱们常常听到的 windows 零碎的 FAT
,NTFS
文件系统,Linux 零碎的 EXT
文件系统,有时候你又会听到 Linux 的虚构文件系统 VFS(Virtual File System
)等。
计算机世界里有句话,任何技术问题都能够通过加一个中间层来解决,Linux 的 file system
架构正是完满地体现了这种哲学。你听到的下面的各种术语,都只是分属于整个大 file system
概念下的不同的分层中。
上面咱们别离来看这三层的具体职责。
Virtual File System
从顶向下,顶层的 Virtual File System
是 Linux 内核构建进去的一个形象的文件系统,它实际上能够大抵地对应咱们平时看到的零碎中的文件和目录等:
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 就能够开始查问 /
里的目录和文件了。
例如用户须要读取一个文件:
/home/user/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 文件系统的格局向下拜访,例如读取这个文件:
/mnt/bar
当 VFS 拜访到 mnt
节点的时候,发现这是一个 mount 点,并且挂载的磁盘分区是一个 NTFS 文件系统,接下来就会以 NTFS 的格局去解析接下来的门路 – 它会去尝试查找并读取这个磁盘分区上的 /bar
门路。
file system 接口
下面讲到了,VFS 在拜访不同节点上的文件时,会跟踪它是属于哪一个磁盘分区以及该分区是什么存储文件系统(如 EXT,NTFS),而后应用对应的文件系统格局去读取磁盘分区数据。这里 VFS 为了兼容各种不同的文件系统,在实现上会首先定义一系列对立的文件操作的接口,而后各种具体的不同品种的文件系统再各自去实现这些接口,这是典型的面向对象编程的范式,例如:
class FileSystem {
public:
int32 read_file(const char* filename,
char* buffer,
uint32 start,
uint32 length) = 0;
int32 write_data(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,外面定义了各种文件操作接口,都是纯虚函数。各种具体的文件系统只须要继承并实现这些接口,例如:
class Ext2FileSystem : public FileSystem {
public:
int32 read_file(const char* filename,
char* buffer,
uint32 start,
uint32 length) ;
// ...
}
再次申明一下,以上只是为了演示用,真正的 Linux 的 VFS 里的接口和实现当然没这么简略,但构造是相似的。
代码实现
这个我的项目里不会应用 EXT 那样简单的文件系统,也不会实现残缺的 VFS 性能,只会将它根本的框架搭建起来,并嵌入一个咱们本人定制的非常简单的存储文件系统。
首先定义 file system 的接口,相似下面的抽象类那样,在 src/fs/vfs.h
文件中:
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;
能够看到下面定义了各种文件操作的函数指针作为接口,它们 原型是:
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/disk_image_writer.c,它会读取 user/progs
目录下的所有文件(这个目录目前还不存在,下一篇咱们会编译链接用户程序放在这里),而后将它们依照下面 naive_fs 文件系统的格局,将它们写入磁盘镜像文件 user_disk_image
,再镜像文件一并刻写进咱们的 kernel 磁盘镜像 srcoll.img
里就能够了。
dd if=user/user_disk_image of=scroll.img bs=512 count=2048 seek=2057 conv=notrunc
写入地位从磁盘的第 2057 个 sector 开始,因为后面是 boot loader 和 kernel 镜像。
接着咱们来实现 naive_fs
的代码,实际上就是下面的各个函数指针的实现,代码在 src/fs/naive_fs.c 里:
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_data;
naive_fs.write_data = naive_fs_write_data;
naive_fs.list_dir = naive_fs_list_dir;
// load file metas to memory.
// ...
}
init_naive_fs
函数里,将所有文件的 meta 局部都读入并保留在内存,相似一份文件名单,而后 read
write
stat
等各种函数就根据这些文件的 meta 信息实现对文件的操作,非常简单。
例如读文件,先依据文件名找到 meta,失去文件在磁盘上的偏移量和大小,再调用底层驱动去读取数据:
static int32 naive_fs_read_data(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
须要调用的,次要就一个函数 read_hard_disk,因为咱们只须要读磁盘的性能就能够了。为了简略起见,这里的底层 IO 咱们依然沿用了相似 boot loader 里的读磁盘函数 read_disk,通过操作磁盘治理设施的各个端口实现,这是一个同步的实现形式。真正的操作系统对磁盘的 IO 的解决必定是异步的,因为磁盘的速度十分慢,零碎不可能阻塞期待它,而是收回读写命令后就持续解决其它事件,而后磁盘治理设施通过中断的形式来告诉零碎数据 IO 结束,数据曾经 ready。
总结
以上咱们实现了一个简略的 VFS 和文件系统 naive_fs
,上面看下 kernel 是如何应用它来读取一个文件的,例如:
char* buffer = (char*)kmalloc(1024);
read_file("hello.txt", buffer, 0, 100);
它调用的是顶层 VFS 的接口,在 vfs.c 中:
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
,因为 get_fs 间接返回 naive_fs 的实体。
接下来就是用 naive_fs 的 read 文件函数接口,实现读取文件。
本篇是对文件系统 File System
的一个整体架构的分层拆解和样例实现,非常简单高级,仅供演示应用,心愿它能帮忙你对操作系统是如何管理文件和底层存储有个全面的认知。