当前位置: 首页 > Linux

从零开始写OS内核-简单文件系统

时间:2023-04-06 05:28:26 Linux

系列目录前言预备工作BIOS启动到实模式GDT和保护模式虚拟内存初步探索加载和进入内核显示和打印全局描述符表GDT中断处理虚拟内存堆和保护模式的完美实现malloc第一个内核线程多线程操作和切换锁以及多线程同步进入用户态进程系统调用的实现简单的文件系统加载可执行程序键盘驱动运行shell准备在前面的文章中我们已经建立了进程和系统调用的框架,并且已经实现了第一个fork系统调用。至此,所有的进程及其线程都是在内核中手动创建的,线程的工作函数也是事先准备好的固定函数,纯粹是为了测试。一个真正的OS当然需要能够加载用户提供的程序到进程中运行,这就会用到我们要实现exec的第二个系统调用。不过,在此之前,还有一项准备工作要做。既然要加载用户程序,当然需要从磁盘加载。目前,我们的内核不具备与磁盘交互的能力。本文将实现一个非常简单的文件系统。文件系统(filesystem)这个词经常有歧义,在不同的上下文中有不同的含义,所以初学者经常会感到困惑。比如我们经常听到的windows系统的FAT和NTFS文件系统,Linux系统的EXT文件系统,有时还会听到Linux虚拟文件系统VFS(VirtualFileSystem)等等。计算机界有一句话,任何技术问题都可以通过增加一个中间层来解决。Linux的文件系统架构完美地体现了这一理念。上面听到的各种名词,只是属于整个大文件系统概念下的不同层次。下面分别来看一下这三层的具体职责。VirtualFileSystem从上到下,最顶层的VirtualFileSystem是Linux内核构建的抽象文件系统,大致可以对应我们平时看到的系统中的文件和目录:bash>ls-l/drwxr-xr-x2rootroot4096Jan132019bindrwxr-xr-x4rootroot4096Jan112019bootdrwxr-xr-x3rootroot4096Feb32020data/usr/bin/cat/home/foo/hello.txtlayer在我们用户的心理概念中最接近于文件系统,但它其实是抽象的,因为你不知道这些文件下的设备和存储格式,作为用户你不需要关心。VFS屏蔽了这些底层的细节,所以这一层被称为Virtualfilesystem。VFS在逻辑上是一个树形结构,根目录/在最上面,每个节点可能是一个目录(灰色),也可能是一个普通文件(绿色)。存储文件系统VFS中各个节点的文件或目录是抽象的,它们必须对应于特定存储设备(如磁盘)上的文件实体,由VFS下层进行管理。比如我们经常听到的EXT2、NTFS等,虽然在术语上也叫文件系统,但它们描述的是文件在硬件上的存储和组织方式,所以它的名字应该叫做“存储文件系统”(storagefilesystem)。系统)。磁盘和内存一样,上面的数据并不是杂乱无章的,它们必须按照一定的结构组织起来,这样上层才能按照规范解析和正确索引需要的数据。比如EXT2文件系统格式:EXT2的整个存储空间会被分成几个块组,然后在每个组内部组织文件的存储,包括各种元信息和最重要的inode,对应每个文件,用于存放每个文件的基本元信息,指针指向文件的具体数据块(蓝色部分)。事实上,存储系统也组织了目录层次的概念。比如有的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文件系统,会挂载到VFS的根目录/,这样VFS就可以开始查询/中的目录和文件了。比如用户需要读取一个文件:/home/hello.txt系统会从前到后逐级查询这个目录:/是根目录,现在挂载到/dev/sda1分区,而分区是EXT2存储格式,所以系统会根据EXT2系统的格式查询分区顶层名为home的节点;注意,这里VFS是树状结构,而EXT2其实也是树状结构,也可以从上到下查看查询;找到EXT2顶层的home节点,发现确实是目录类型的节点,没问题;然后在home目录下搜索hello.txt文件,如果能找到则读取;这里总是按照EXT2系统的格式在/dev/sda1分区上逐级查找;虽然VFS中的路径是一个抽象的概念,但是在实际访问文件的时候,这个路径会被投影到它挂载的磁盘分区上,以便在文件系统中查询。上面的例子只挂载了一个磁盘分区。其实在Linux下,可以在VFS上找一个目录节点挂载一个新的磁盘分区。甚至这个分区也不需要是EXT格式,只要内核能支持解析这个格式即可。比如我们有一个磁盘分区/dev/hda2,它是NTFS格式的(比如你双系统windows的D:\盘),我们将它挂载到VFS的/mnt节点:这个newdiskpartitionmount上去之后,从VFS的角度,可以从NTFS文件系统格式的mnt向下访问,比如读取这个文件:/mnt/bar当VFS访问mnt节点时,发现是一个挂载点,并且挂载的磁盘分区是一个NTFS文件系统,那么它会解析下一个NTFS格式的路径——它会尝试在这个磁盘分区上找到并读取/bar路径。上面提到的文件系统接口,当VFS访问不同节点上的文件时,会跟踪它属于哪个磁盘分区,以及该分区是什么存储文件系统(如EXT、NTFS),然后使用对应的文件系统格式进行读取获取磁盘分区数据。这里,为了兼容各种文件系统,VFS在实现上会先定义一系列统一的文件操作接口,然后各种不同类型的具体文件系统分别实现这些接口。这是典型的面向对象编程。例如:classFileSystem{public:int32read_file(constchar*filename,char*buffer,uint32start,uint32length)=0;int32write_file(constchar*filename,constchar*buffer,uint32start,uint32length)=0;int32stat_file(constchar*文件名,file_stat_t*stat)=0;//...}以上是用C++代码的演示(当然内核是用C语言写的,这里只是为了演示其面向对象的编程方式),定义了抽象类FileSystem,它定义了各种文件操作接口,所有这些都是纯虚函数。各种具体的文件系统只需要继承和实现这些接口,例如://...}再次让我声明,以上内容仅用于演示目的。当然,真正的LinuxVFS中的接口和实现并没有这么简单,但结构是差不多的。代码实现本项目不会使用EXT这样复杂的文件系统,也不会实现完整的VFS功能。它只会搭建它的基础框架,嵌入一个非常简单的我们自己定制的存储文件系统。首先定义文件系统的接口,类似于上面的抽象类,在src/fs/vfs.h文件中:structfile_system{enumfs_typetype;disk_partition_t分区;//函数stat_file_funcstat_file;list_dir_func列表目录;read_data_func读取数据;write_data_func写数据;};typedefstructfile_systemfs_t;可以看到上面定义了各种文件操作的函数指针为接口,其原型为:typedefint32(*stat_file_func)(constchar*filename,file_stat_t*stat);typedefint32(*list_dir_func)(char*dir);typedefint32(*read_data_func)(constchar*filename,char*buffer,uint32start,uint32length);typedefint32(*write_data_func)(constchar*filename,constchar*buffer,uint32start,uint32length);naive_fs实现我们不需要实现像EXT这样复杂的存储文件系统。在本项目中,我们只实现了一个非常简单的文件系统,其功能非常有限:磁盘镜像数据是预先刻录的,只能读不能写;只有一层根目录,没有下级目录;我们为一个目的定制这个文件系统一是演示,二是项目使用。我们需要用它来保存加载运行的用户程序,所以只需要可读即可,不需要复杂的目录结构。一层就够了。All所有的文件都放在这一层。虽然它很底层,但它仍然是一个文件系统。我们不妨将它命名为naive_fs,因为它真的很幼稚和简单。naive_fs的存储结构如下:头部绿色部分为整数,记录文件总数,也是固定的;后面的灰色部分是每个文件的元信息;最后蓝色的部分是具体的文件数据,通过每个文件的元信息(文件偏移量,文件大小)可以定位到它的数据存放在哪里;你会发现这其实和我们之前实现的堆类似,是一个非常简单直接的元+数据结构。我写了一个工具,在user/disk_image_writer.c中,它会读取user/progs目录下的所有文件(这个目录还没有,下一篇我们会在这里编译链接用户程序),然后写入按照上面naive_fs文件系统的格式,将它们写入到磁盘镜像文件user_disk_image中,然后将镜像文件写入到我们的内核磁盘镜像srcoll.img中。ddif=user/user_disk_imageof=scroll.imgbs=512count=2048seek=2057conv=notrunc写入位置从磁盘的第2057扇区开始,因为前面是bootloader和kernelimage。接下来我们来实现naive_fs的代码,其实就是上面函数指针的实现。代码在src/fs/naive_fs.c中:staticfs_tnaive_fs;voidinit_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;//将文件元数据加载到内存中。//...}init_naive_fs函数,将所有文件的meta部分读取并保存在内存中,类似于一个文件列表,然后readwritestat等各种函数根据这些文件的meta信息实现对文件的操作,这很简单。比如读取一个文件,首先根据文件名找到meta,得到文件在磁盘上的偏移量和大小,然后调用底层驱动读取数据:staticint32naive_fs_read_file(char*filename,char*buffer,uint32start,uint32length){//按名称查找文件元数据。naive_file_meta_t*file_meta=nullptr;for(inti=0;ifilename,filename)==0){file_meta=meta;休息;}}if(file_meta==nullptr){返回-1;}uint32offset=file_meta->offset;uint32大小=file_meta->大小;如果(长度>尺寸){长度=尺寸;}//从磁盘读取文件数据。read_hard_disk((char*)buffer,naive_fs.partition.offset+offset+start,length);返回长度;}磁盘驱动我们还需要实现最底层的磁盘IO驱动,也就是上层naive_fs需要调用的,主要是一个函数read_hard_disk,因为我们只需要读取磁盘的函数即可。为了简单起见,我们这里底层IO还是使用bootloader中的读盘函数read_disk,通过操作磁盘管理设备的各个端口来实现。这是一个同步实现。真正的操作系统必须异步处理磁盘的IO,因为磁盘的速度很慢,系统不能阻塞等待,而是发出读写命令后继续处理其他事情,然后磁盘管理设备会通过中断来通知系统数据IO完成,数据准备就绪。综上所述,我们实现了一个简单的VFS和文件系统naive_fs,让我们看看内核是如何使用它来读取一个文件的,例如:char*buffer=(char*)kmalloc(1024);read_file("hello.txt",缓冲区,0,100);它在vfs.c中调用顶级VFS接口:int32read_file(char*filename,char*buffer,uint32start,uint32length)returnfs->read_data(filename,buffer,start,length);}VFS会根据给定的文件路径filename定位自己属于哪个文件系统,对应哪个磁盘分区。当然我们这里只挂载一个唯一的分区,文件系统类型是naive_fs,因为get_fs直接返回naive_fs的实体:fs_t*get_fs(char*path){returnget_naive_fs();}下一步就是用这个fs读取文件函数接口read_data,读取文件。本文是文件系统FileSystem整体架构的分层拆解和示例实现。它非常简单和初级,仅用于演示。希望能帮助大家全面了解操作系统是如何管理文件和底层存储的。认识。