常规文件:
- file:文件名字
- size:大小
- blocks:占据多少个块
- regular file: 常规文件
- inode:文件在底层的唯一标识(文件名->inode->file)
- links:硬链接数,文件名字不同,但是inode实际上一样,这就是硬链接
- uid:用户id
- gid:用户组id
- access:10个字符,第一个是类型,常规文件是-,后9位分成3组,所有者/ 用户组其他用户/ 剩下的其他用户 的权限
对于内核来说,常规文件就是一个字符序列
目录也算一个文件,文件中保存着目录项,就是下一级的目录/文件的名
access的首位是d。用户只能通过修改下一级的文件或者子目录,才能对目录产生影响。
pwd
pwd 可以打印当前进程所在的目录
文件系统:
- 数据都是存在物理设备上,文件系统就是把软件这种操作,转成对实际物理块的操作
- 虚拟文件系统:规定一组接口,能让多种对文件的操作统一格式
本章:
- 只有一个根目录
- 没有权限控制
- 不记录时间戳
- 不支持软硬链接
- 系统调用
- 打开
- 关闭
- 读写
文件打开
打开文件之前,应用首先进行系统调用sys_open,在这个进程的文件描述符表中加入这个文件,然后得到文件描述符,也就是这个表项,在文件描述符表里面的索引值。
描述符表里面有: flags 文件指针(所以能对文件进行读写)
fn sys_open(path: &str, flags: u32) -> isize
path:文件名字。(待打开文件的文件名字符串的起始地址)
这个字符串,是应用给内核的,按照地址空间隔离的规则,传递的应该是一个虚拟地址,也就是这个变量的虚拟地址,内核应该转为pa,访问实际的物理内存(文件是在磁盘上后来放到内存上,才能用的)。
flags:进程这次想如何打开这个文件(理解为哪个命令)
- 0:只读 RDONLY
- 0X001: 只写 WRONLY
- 0X002: 可读可写RDWR
- 0X200:允许创建文件,找不到就创建,如果存在就相当于没有,直接清零 CREATE
- 0X400:打开文件的时候直接清空文件,当成新文件 TRUNC
文件关闭
读写完毕要关闭,进程释放对这个文件占用的内核资源。
pub fn close(fd: usize) -> isize { sys_close(fd) }
参数是文件描述符,关闭以后这个文件在内核里面的资源被释放了,文件描述符回收了,进程用不了描述符表了。
文件的顺序读写
常规文件,字节序列,应该可以随便读写。但是read和write做不到。
进程对打开的常规文件,维护一个偏移量,初值一般是0。read和write,会从这个便宜了开始顺序读写。close后会把偏移量重置。
系统调用sys_lseek可以调整这个偏移量,这样就可以随机读写。(rCore没有实现)
tips:在应用程序里面,只要当成正常程序来写就可以了,不用考虑底层,因为页表转换啥的都是内核干的
同时涉及对磁盘和内存的访问,访问方式不同,内存直接系统调用,访问磁盘需要对磁盘发出请求来间接读写。要区别不同的数据结构。
- easy-fs:文件系统核心部分,实现了简单的系统磁盘布局
- easy-fs-fuse是能在开发环境运行的应用程序,可以对easy-fs进行测试,或者为内核开发的应用打包一个easy-fs格式的文件系统镜像。
简单来说,开发这个文件系统是按照应用程序库的方式来开发的。
块设备接口层
定义块设备的抽象接口BlockDevice Trait,这是给底层块设备的规定,要由块设备驱动来实现,也就是文件系统的使用者提供
- 把某个块读入内存缓冲区read_block
- 把缓冲区内容写入某个块write_block
块缓存层
cache功能,因为频繁调入调出速度慢,先把一大块内容读到缓冲区,如果被修改了,需要写回到磁盘。
读写前检查这个块是否在cache之中,如果是的话,一段时间内对这个块的操作都是在缓冲区之上(合并这些操作,或者叫暂存这些操作),而不是重复调用,导致修改后,另一个进程又去调旧的块,应该使用线性的流程避免错误。
块缓存
创建一个cache的时候,就会read_block某个块,读到缓冲区。
drop trait决定了cache被释放后,块是否需要写回内存。
块缓存全局管理器
cache是有限个的, 否则浪费太多内存。
维护一个队列,FIFO替换算法,只有arc引用数=1(只有cache块自身的引用无外界引用)才能被替换
get_block_cache就是提供给外界的接口,获取不到某个块的话也会自动去获取块。
get_block_cache -> block_cache::new() -> device.read_block
返回一个引用
磁盘布局
重点:逻辑上的目录树如何映射到磁盘上
- 超级块:最开始的一个块,以魔数的方式提供而合法性检查,可以定位其他连续区域的位置
- 索引节点的位图:占了若干个块,索引节点的占用情况
- 索引节点区:其中每个块都存储了若干个索引节点
- 数据块位图:数据块占用情况
- 数据块区域:真正保存数据
inode中包括了元数据,以及实际存储数据块的索引信息,从而能找到具体的数据块。有直接索引、间接索引。
超级块
文件系统初始化的时候可以创建一个超级块,包括了magic判断文件系统是否合法,以及各个区域的数目。
放在磁盘的第一个块。
位图:驻留在内存的,表示磁盘占用情况的数据结构
每个位图有多个块,一个块是512B,4096b,一个bit代表一个块是不是被占用。位图通过bit的分配,进行节点的分配和回收。
- 开始块id
- 区域块数
位图在磁盘上存储的方式是位图块,位图块占用一个磁盘块,看为一个4096bit,每组64bit。
分配:索引节点/数据块的块编号。
索引节点
索引节点区域每个块存的是diskinode,也就是每个文件/目录在磁盘上的存储形式。
DiskInode:一个索引节点
- size:文件/目录内容的字节数
- type_:索引节点的类型 文件/目录
- direct/indirect1/indirect2:数据块的索引
直接索引:在direct数组中直接得到inode
间接索引:indirect1指向一级索引,也是指向一个索引块,在数据块区域中
get block id:查找某个文件某个块的实际块编号
- 是否小于direct容量,如果是,直接用direct就可以找到
- 如果大于direct容量,小于direct+direct1所能检索的数据块数,就在direcrt1找
- direct1能检索的块数目的计算:direct1指向一个块,512B,这个块的每个u32都是一个inode编号,总共可以找到512B/4B的块
- 还不够就看direct2,direct2指向的那个块,都是一级索引,也就是有512B/4B的一级索引,(512/4)*(512/4)就是能检索到的数据块数量
初始化索引节点的时候,size=0,要用increase size扩容。
data blokcs 计算目前的size需要多少个数据块。
total blocks还要计算加上索引节点(这和多少个数据块有关系)
- 如果数据块比direct能容纳的数量小,就不需要额外的索引节点
- 如果大于direct,就要用到direct1,direct1是一个索引节点,+1
- 如果还大于direct1,就要用到direct2,direct2指向一个块,+1,这个块里面的每个u32都指向一个索引节点,一个索引块,就可以指向512/4的数据块,所以还要再加(数据块数-direct-direct1+一个索引节点指向的数据块数-1)/一个索引节点指向的数据块数,这样计算是为了向上取整
clear size:清空内容回收数据和索引的块,把需要清空的块的编号都交给device来清空。
read at:读写数据块的数据
传入:offset,buffer,device
执行:把文件的内容,从offset字节处开始读到缓冲区,返回读了多少个字节。
文件的数据块
实际只是字节数组
目录的数据块
目录的数据块应该是目录项,每个目录项都是二元组,(子目录/文件名,所在的索引节点的编号),每个目录项是32字节,每个数据块可以存储16个目录项
磁盘块管理器:easy-fs
- inode位图
- data位图
- device
- 索引节点区的起始块
- 数据块区的起始块
create,在块设备上创建并初始化一个easy-fs
根据inode位图占据的块数(传入的参数),确定inode区有多少个块。剩下的块,/4097向上取整作为数据块的位图占据的块数。
算完了,就把前total blocks(传入的参数)的块清零,第0块是超级块,传入各个区域的块数。
创建根目录,/,先在inode位图中分配一个inode,编号为0,这个inode,就是根目录在索引节点区的索引块。根据inode编号,调用get disk inode pos获得inode所在块和块内偏移,然后就可以调用get block cache和modify。
get disk inode pos:
传入inode id,根据inode的编号(也就是inode位图的编号),找到这个inode节点的block id,以及这个inode节点在这个块中的偏移量(在索引节点区域中,一个块,可以存储多个inode)
索引节点
diskinode是在文件系统中的索引节点,但是文件使用者不关心,让他们看到inode就行,因为他们要直接操作。
inode:
- block id
- block offset
- fs:efs的引用,对实际的设备块进行操作
- block device
使用者的接口
- open:打开efs
- 获取根目录的inode:root_inode方法,其实就是获取编号为0的索引节点的信息,构造一个inode返回
- 文件索引:在根目录->数据块中的目录项,去找对应名字,返回inode。这个过程是根目录inode,通过调用find,先去找name对应的inode,然后返回inode。
目前为止,虽然inode都是以arc的形式存在,但是不需要考虑drop,还不涉及文件/目录的删除,只有数据块的清零
文件列举 ls
收集根目录下所有的文件名,以vec返回
过程:read disk inode获取这个这个inode的真实diskinode的数据,读取diskinode中size,知道了总共有多少目录项,然后for循环获取
文件创建 create
检查文件是否已经在根目录下面了,如果存在返回Non额,不存在就新建一个(分配一个inode),把目录项插入到根目录的内容(数据块)中
tips:inode是vfs中提供给使用者的接口,虽然inode中有很多方法,和diskinode的方法几乎重名,但是都要通过risk disk inode来进行实际的底层操作,我理解这是为了构建一个完整的规范的抽象层。
文件清空 clear
首先返回所有要清空的数据块的编号,然后判断是不是和size相等,接着dealloc所有的数据块。
调用了clear size:索引块+数据块编号
文件读写
注意点是,在write之前,可能需要扩容,调用扩容方法,是否需要扩容是在扩容方法中判断的
efs的设计思路是作为一个应用库来设计:
作为应用库,可以暂时用std,如果是作为内核的一部分,属于裸机上的应用,必须no std
模拟块设备
基于linux,上的一个文件,视为一个块设备。这个设备需要实现blockdevice接口,前文提到的,设备需要使用efs和vfs使得别人可以使用它,所以需要向fs提供这些接口。
块设备接口
read和write block的时候,需要seek到块的开头位置。
也就是说,一个file是一个大块设备,需要用block size*id来seek到这个块。
打开块设备
创建一个fs.img模拟一个块设备,在块设备上初始化efs。
在内核中需要对接efs:
- 块设备驱动层:驱动块设备,并实现blockdevice的trait
- efs层:接受一个blockdevice并且在上面打开efs
- 内核索引节点层:把inode包装成osinode
- 文件描述符层:为常规文件的osinode实现file trait,才能使应用程序像file一样使用osinode
- 系统调用层:支持对常规文件操作的系统调用
文件简介
os来看file就是字节序列,解析内容是进程的任务。
操作系统把数据按文件来管理,把文件分配给进程。进程用统一接口file来读写数据。
file接口是介于内存和块设备之间的。
UserBuffer是一个字节数组的封装。
read是把文件读到缓冲区,write是把缓冲区的数据写入文件。
块设备驱动层
qemu模拟器平台
virtIO就是一个块设备,是在qemu启动时候的配置参数中声明的。
硬盘内容就是打包的efs镜像,理解为一块有数据的磁盘。
MMIO
内存映射IO,外设的设备寄存器可以通过特定物理地址访问。
qemu中MMIO的物理地址区间是从0x10001000开头的4KB。
为了能访问到外设总线,必须对特定内存区域进行映射。
创建内核地址空间的时候,建立页表映射(MMIO视为一个段)。
virtIO设备需要占用部分内存:VirtQueue环形队列,CPU可以向队列提交virtio的请求,也可以从队列中获取请求的返回。这里涉及到对于内存的分配和回收,需要内核来实现。(根据之前的地址空间的页表机制)
- 分配/回收物理页帧,这些页帧暂时放在QUEUE_FRAMES之中
- 这个队列是页表机制的页表一样的东西,属于硬件与软件的约定
内核索引节点层
osinode,比inode多了一些功能,比如保存了此前访问到的偏移量以及文件的读写权限。
文件描述符层
一个进程可以访问多个文件,每个进程有一个文件描述符表,每个描述符代表一个特定的IO资源。
open或者create一个文件,内核会返回文件描述符,close的时候需要提供文件描述符。
osinode是会放到文件描述符中的文件(因为它自己是一个文件节点,下面挂着数据块),可以进行读写操作,需要file trait。
两个进程无法同时访问一个文件。
文件描述符表
在TCB中加入描述符表。
fd_table:本质是一个vec,存的是option,none代表这个描述符没有被占用
实现应用访问文件(设计相关的系统调用)
文件系统初始化
初始化后可以把efs接入来用。
- 打开块设备,也就是在qemu启动时已经设置好的img,打开blockdevice。
- 从device上打开文件系统
- 获取根目录inode
打开文件
openfile:如果是create,需要清空原有数据块和索引节点,新建一个。
sys_open:app传来一个字符串的首地址,使用它的token翻一下,读取出来,调用openfile。最后返回一个fd。
sys_close:把fd那个项设置为None
基于文件加载执行应用
sys_exec修改:从文件系统中找到这个文件,然后从inode中read全部数据,丢到task.exec