lab5
这一章讲文件系统,其实核心是将外设device。正是由于一切皆文件,才能对外设有统一的管理,或者说为了对外设有统一的管理,于是诞生了一切皆文件的概念。这两个我不清楚哪个在前,一个是目的,一个是方法,或许是方法早就有,恰好对上了这个目的。
计算机底层的发展,好像是一部双向链表,在某一历史节点,这两个结点连在了一起。从此他们便是一个整体
- 实验目的
文件系统、磁盘、设备驱动、文件系统服务、微内核
谈到微内核呢,很有意思。鸿蒙os是微内核,linux是宏内核;目前来看,微内核好慢,宏内核还挺好。微内核慢是因为需要不断进行进程之间的通信,内核需要不断处理信息之间的传递,但是把不同服务程序提出去,在用户态执行,如果可以做到通信的速度进一步加快,每一个进程的独立性提高,不仅安全性这一微内核本身的优势还在,还有了更好的速度。
微内核这和网络中的TCP/IP协议很像,不同程序都先到底层进行数据传送,但是在多用户的时候这种计网中类似的结构就很快了,主要在于交换机技术是的能够不同的网络信息进行排队调度,使得或许的共享资源传送更加快速。
当然,还有一个有意思的,就是linux的宏内核设计使得对于类似安卓这种不希望将自己的东西开源的,会调用内核接口,然后在用户层实现,这倒是引起了开源社区的大大不满。但是如果最开始就是微内核,那就大大避免了这个问题,大胆想想,华为这是想当安卓界的linux啊。
文件系统,比如FAT、HPFS 和NTFS都是一种文件系统,似乎是不同的文件(磁盘)组织格式,各司其职,但是具体有什么用,我不懂。
首先将外设,一般会拿最经典的磁盘,至于打印机能不能有,也行,一次传输一个字符这种。磁盘作为空间的扩容,是内存的拓展,内存是磁盘的cache。文件系统是管理磁盘的工具。
广义上,一切带标识的、在逻辑上有完整意义的字节序列都可以称为“文件”。文件系统将外部设备中的资源抽象为文件,从而可以统一管理外部设备,实现对数据的存储、组织、访问和修改等操作。
- 文件系统总览图
我们先啥都不看,就分析这张图
宏观上看,这里是微内核结构的完美体现。虽然我觉得lab4
的由用户处理tlb_mod
很怪,但是这里将文件系统服务进程放在用户态就很合理。在用户态,文件系统方便地对不同用户发出的请求进行调度,并进行磁盘驱动。
大抵是因为和磁盘的调用相比,进程间通信的开销是算小的。
在每个用户进程中,不同外设提供了统一的用户接口。
对于每个外设,有自己对于接口的实现,进一步要么通过自己用户空间进行访问,要么通过和fs(file system)进行通信。
fs进程包括首先对请求的分发、对于每个请求对应的有几个接口,在映射到磁盘块的具体实现。用到内存作为磁盘的cache的思想,先去内存固定的位置来找有没有,没有就通过磁盘驱动,进入内核访问磁盘。
IDE:Integrated Drive Electronics
集成驱动器电子设备,是硬盘接口。
外设驱动的思想体现了层次化设计和上层的抽象和封装。提供统一的接口。
文件系统结构。mos中,fs目录下存放的是fs进程的代码,user/lib
中的是文件系统的用户库。
文件对应一个数据结构,引入了文件描述符等结构。
三个目录:tools
,fs
,user/lib
MOS
将进程页表映射到了fs进程,内核仅提供对设备物理地址的读写操作的系统调用
简单分析一下
一般的用户进程,有了访问文件的接口,从用户的角度来看,就是文件可写了,可读了。
fs这个用户进程,是一个有专门功能的进程,一方面需要进行通信,这里就涉及对用户进程请求的调度(比如一个很有意思的,linux的mv指令,对于在用一个文件系统的会直接改个映射,非常快,但是不同文件系统,就会先复制然后删除之前的。这个就是有fs进程进行判断并处理的),另一方面有硬件驱动,能够通过内核进行外设读写。
IDE磁盘驱动
- 不同的硬件设备有不同的操作方式和信号接口。操作系统需要通过设备驱动程序来翻译和传递这些特定的命令和信号,使得系统可以正常地使用这些硬件设备。
不太懂这些特定的命令和信号是什么意思
mips
存储器地址映射,几乎每一种外设都是通过读写设备上的寄存器来进行数据通信。外设寄存器也称为I/O
端口,主要用来访问I/O
设备。外设寄存器通常包括控制寄存器、状态寄存器和数据寄存器,这些寄存器被映射到指定的物理地址空间。
实验中的mips
体系结构没有复杂的I/O
端口的概念,使用统一的内存映射I/O
模型。在mips
的内核地址空间实现了硬件级别的物理地址和内核虚拟地址的转换机制。
但是在这里使用qemu
模拟操作系统,I/O
设备的物理地址是完全固定的,可以通过简单的读写某些固定的内核虚拟地址来完成驱动程序的功能。
关于这里为什么使用kseg1
这一个进行存取不会经过cache
的地址空间,来作为使用Memory-Mapped I/O
技术来访问的,我觉得应该是外设要求不经过外设于是产生了这一段地址空间。
作为相互印证的有,内核一般是不使用kseg1
的,会将内核的.text .data .bss
这些段放到kseg0
中,同时bootloader
好像也是运行在kseg1
的,是因为bootloader
需要完成很多初始化的工作,其中就包括对cache
的初始化。因此也需要一块不经过cache
的地址空间。
这么说来,似乎什么操作系统都得有这么一个类似的不经过cache
的地址空间。
但从linux
来说,在x86
架构下是没有这个的,但是linux
有一套mmap
的api
,可以设置禁用缓存的策略。同时x86
用的是DMA
直接对内存进行和其他设备的内存访问。
嘶,这里写思考题的时候我也纳闷了
guide-book 48页说运行在kseg1中的bootloader在载入内核前会进行cache的初始化
但是我翻了翻王道,搜了搜,又说会硬件自动完成初始化。
这里我的评价是,不同硬件条件不一样,运行在不同硬件架构的boot也不一样
MALTA
的console设备是一个典型的NS16550
设备。MALTA 开发板上的 console 设备确实是基于 NS16550 UART(Universal Asynchronous Receiver/Transmitter,通用异步收发器)的。NS16550 是一种非常经典的 UART 接口设备,被广泛应用在计算机系统中,用以提供串行通信。
其基地址是ox1800_03F8
。只需要在0x1800_03F8 + 0xA000_0000
这个地址写入字符就可以在控制台看到相应的输出。这里都已经牵涉到写入了,已经是在内核态了(printk
,助教特意把这个名字改成了kernel)。
本次需要编写的IDE
磁盘驱动程序位于用户空间,需要先经过系统调用进入内核。
那就在注册两个呗。sys_wrtie_dev sys_read_dev
这两个是为了在内核空间完成I/O
操作。
有傻子
我还找了半天
过了好久好久,终于又开始写os了,这就是饥饿现象吗
- 扇区sector:圆环的一个片段
- 磁道track:不同半径的同心圆
- 柱面cylinder:不同盘片相同半径的磁道所组成的圆柱面
- 磁头head:每个磁盘有两个面,当对磁盘进行读写操作是,磁盘在盘片上快速移动
对于磁盘寻址,可以按照柱面-磁头-扇区(Cylinder-Head-Sector, CHS)的方式来定位一个扇区。
PIIX4 I/O
的具体方式还是不一样。MALTA
平台上的PIIX4
磁盘控制器基地址为0x180001F0
,与I/O
相关的寄存器相对与改地址的偏移和对应的功能如下表所示。
这里实际上就对应了我们鞥能够对外设进行的操作,而这个接口是有PIIX4
来提供的。
通过这个可以看到扇区有28位,也就是我们最多可以寻址$2^{28}$个扇区,而每个扇区的大小是512B
,因此可以访问$128GB$的磁盘空间。
- 驱动程序编写
当需要从磁盘的指定位置读取或者写入一个扇区是,需要调用read_sector
函数将磁盘中对应扇区的数据读到磁盘缓冲区中,
或者使用write_sector
函数来缓冲区中的数据写入磁盘。
注意,与串口通信相同,这里所有的地址操作,都需要从物理地址转换成虚拟地址
but why?留个坑
在磁盘读写的流程中,需要反复检查IDE
设备是否已经就绪。这是由于IDE
外设一般不能立即完成数据操作,需要CPU
检查IDE
状态,并等待操作完成。
这里用到了我们不希望用到的轮询,但我们主要考虑的是实现的角度,具体性能问题不是我们关注的重点
这里构建了一个检查IDE
状态的函数wait_ide
,用于等待IDE
上的操作就绪
在IDE
设备就绪后,我们就可以对其进行读写操作了。
-
设置操作扇区的数目,这里只操作一个扇区,因此设置为1
这个应该就是随便操作操作?或者设置的是按照一个扇区一个扇区的访问?
-
一次设置操作扇区号,一次设置刚刚出现的那几个在基地址偏移上的寄存器。
因此这里还需要扇区殉职模式和磁盘编号。具体实现就是通过位运算将各值组合,并一齐写入对应地址。
-
完成一系列设置后,再次等待
IDE
设备准备就绪,并在此之后,通过系统调用读取或写入扇区,即连续向相同的地址读取或写入4字节 -
最后,检查
IDE
设备状态,确认扇区读取和写入成功
等等,刚刚那些都是在内核状态下的?那我们的微内核呢
原来得模仿上面的通过系统调用完成fs/ide.c中的相应函数,实现用户态对与磁盘的读写操作,注意这里的fs进程也是用户态下的一个进程。
只是用户进程与fs进程之间涉及到通信,必然需要进行系统调用进行ipc,进程间通信,来共享数据等。
文件系统结构
这一部分从内容上讲感觉比较简单,主要包括磁盘文件系统和文件结构体和块缓存三个部分。
磁盘文件系统指的是在磁盘文件中,如何组织各个文件。
- 从文件结构上面来讲,是块状串联的结构
- Block0是引导区
- Block1时超级块
- 接下来几个块是位图,用来标志各个数据块的使用情况。
使用一组枚举变量标志了每个块的用处
enum {
BLOCK_FREE = 0,
BLOCK_BOOT = 1,
BLOCK_BMAP = 2,
BLOCK_SUPER = 3,
BLOCK_DATA = 4,
BLOCK_FILE = 5,
BLOCK_INDEX = 6,
};
总感觉这个不只是这点作用
关于blockno为0的问题,blockno=0说明是在引导区,所以这一块地方必然是不能被释放的。
有一句话非常精髓:文件系统需要负责维护磁盘块的申请和释放
对于每个文件(万物皆文件,把目录视为一种特殊的文件),如何进行管理呢
其中最关键的还是数据结构的考虑。
struct File {
char f_name[MAXNAMELEN]; // filename 文件名称,最大长度为128
uint32_t f_size; // file size in bytes 文件的大小,单位为字节
uint32_t f_type; // file type 文件类型,有普通文件FTYPE_REG和目录FTYPE_DIR两种
uint32_t f_direct[NDIRECT]; // 文件的直接指针,每个文件控制块有10个直接指针,用来记录文件的数据块在磁盘上的位置
// 每个磁盘块的大小为4KB,也就是这10个直接指针能够表示最大40KB的文件
uint32_t f_indirect; // 文件大于40KB时,需要用到间接指针。
// ((int*)(disk[dirf->f_indirect].data))[i]
struct File* f_dir; // the pointer to the dir where this file is in, valid only in memory.指向文件所属的文件目录
char f_pad[FILE_STRUCT_SIZE - MAXNAMELEN - (3 + NDIRECT) * 4 - sizeof(void*)];
// 是为了让整数个文件结构体占用一个磁盘块,填充结构体中剩下的字节
} __attribute__((aligned(4), packed));
注释写得比较清楚了。这里关键需要思考的是f_indirect
这个间接指针。
其实,这个指向了磁盘中的某一个块,直接去取即可。
具体部分可以循file_create
的迹。
关于free_block
我的是
if (blockno == 0 || blockno >= super->s_nblocks) {
return;
}
学长的是
if(blockno==0||(super!=NULL&&blockno>=super->s_nblocks)){
return;
}
显然学长的更加合理。(虽然没必要,但是要知道C语言是一个容易内存泄漏的东东,这波我站学长!
当我认为这里已经没有问题的时候,突然看到
if (f->f_name == NULL) {
return f;
}
而学长的是:
if (f->f_name[0] == '\0') return f;
显然学长的更合理!(虽然本质是一样的)