Skip to content

Latest commit

 

History

History
370 lines (211 loc) · 14.8 KB

chapter6.md

File metadata and controls

370 lines (211 loc) · 14.8 KB

文件系统接口

常规文件:

  • 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

在内核中需要对接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