type
status
date
slug
summary
tags
category
icon
password
文件与文件描述符
- 文件访问的接口
- 在内存和I/O资源之间建立了数据交换的通道
- 它只是将我们调用
translated_byte_buffer
获得的包含多个切片的Vec
进一步包装起来,通过len
方法可以得到缓冲区的长度
- 为UserBuffer实现了IntoIterator特征
- 里面的into_iter()函数可以获得UserBuffer的迭代器UserBufferIterator
- 为UserBufferIterator实现了Iterator特征
- 里面的next()函数让UserBuffer可以被逐字进行读写
标准输入和标准输出
- 为标准输入输出提供实现File特征
- 标准输入文件
Stdin
是只读文件,只允许进程通过read
从里面读入,目前每次仅支持读入一个字符
- 标准输出文件
Stdout
是只写文件,只允许进程通过write
写入到里面,实现方法是遍历每个切片,将其转化为字符串通过print!
宏来输出
文件描述符与文件描述符表
文件描述符表:
- 每个线程都带有一个线性的文件描述符表
- 记录它所请求内核打开并可以读写的那些文件集合
文件描述符:
- 文件描述符为非负整数
- 表示文件描述符表中一个打开的 文件描述符 所处的位置(可理解为数组下标)
- 在tcbInner里面新增这个属性
Option
使得我们可以区分一个文件描述符当前是否空闲,当它是None
的时候是空闲的,而Some
则代表它已被占用;Arc
首先提供了共享引用能力。后面我们会提到,可能会有多个进程共享同一个文件对它进行读写。此外被它包裹的内容会被放到内核堆而不是栈上,于是它便不需要在编译期有着确定的大小;dyn
关键字表明Arc
里面的类型实现了File/Send/Sync
三个 Trait ,但是编译期无法知道它具体是哪个类型(可能是任何实现了File
Trait 的类型如Stdin/Stdout
,故而它所占的空间大小自然也无法确定),需要等到运行时才能知道它的具体类型。
文件I/O操作
初始化tcbInner的时候:先把012这三个文件描述符分给标准输入输出流
在 fork 时,子进程需要完全继承父进程的文件描述符表来和父进程共享所有文件
文件读写系统调用
文件系统接口
与课堂所学相比,我们实现的文件系统进行了很大的简化:
- 扁平化:仅存在根目录
/
一个目录,所有的文件都放在根目录内。直接以文件名索引文件。
- 不设置用户和用户组概念,不记录文件访问/修改的任何时间戳,不支持软硬链接。
- 只实现了最基本的文件系统相关系统调用。
打开与读写文件的系统调用
打开文件
其中:flags:
封装:
顺序读写文件
用之前的
sys_read/sys_write
两个系统调用来对它进行读写了用户应用中:
- 用只写模式+创建模式打开文件
filea
的总大小不超过缓冲区的大小,因此通过单次 read
即可将内容全部读出来而更常见的情况是需要进行多次
read
,直到返回值为 0 才能确认文件已被读取完毕简易文件系统 easy-fs
松耦合模块化设计思路
简易文件系统的实现分成两个不同的 crate :
easy-fs
为简易文件系统的核心部分,它是一个库形式 crate,实现一种简单的文件系统磁盘布局;
easy-fs-fuse
是一个能在开发环境(如 Ubuntu)中运行的应用程序,它可以对easy-fs
进行测试,或者将为我们内核开发的应用打包为一个 easy-fs 格式的文件系统镜像。
- 磁盘块设备接口层:以块为单位对磁盘块设备进行读写的 trait 接口
- 块缓存层:在内存中缓存磁盘块的数据,避免频繁读写磁盘
- 磁盘数据结构层:磁盘上的超级块、位图、索引节点、数据块、目录项等核心数据结构和相关处理
- 磁盘块管理器层:合并了上述核心数据结构和磁盘布局所形成的磁盘文件系统数据结构
- 索引节点层:内核,管理索引节点,实现了文件创建/文件打开/文件读写等成员函数
块设备接口层
- 直接从磁盘读块和写块
- 管理了一块底层设备
块缓存层
- 一块512字节
block_id
记录了这个块的编号;
block_device
记录块所属的底层设备;block_device
字段表示一个指向实现了BlockDevice
trait 的类型的引用计数智能指针。这意味着block_device
可以指向任何实现了BlockDevice
trait 的具体类型的实例,而且可以在多个线程之间安全地共享和访问这个实例
modified
记录自从这个块缓存从磁盘载入内存之后,它有没有被修改过。
BlockCache提供的方法:
- 在 Rust 中,
FnOnce
是一个特质(trait),它代表了一种可调用的闭包(函数),该闭包可以被调用一次(仅仅一次),因为它会“消耗”自身(consume itself)。换句话说,FnOnce
的闭包会取得闭包捕获的所有权并消费它,所以它只能被调用一次
- 如何使用modify?
- 由于这些数据结构目前位于内存中的缓冲区中,我们需要将
BlockCache
的modified
标记为 true 表示该缓冲区已经被修改,之后需要将数据写回磁盘块才能真正将修改同步到磁盘
块缓存全局管理器
- 队列
queue
维护块编号和块缓存的二元组
- 内存只能同时缓存有限个磁盘块。当我们要对一个磁盘块进行读写时,块缓存全局管理器检查它是否已经被载入内存中,如果是则直接返回,否则就读取磁盘块到内存
- 这里使用一种类 FIFO 的缓存替换算法
BlockCacheManager提供的方法:
- 传入块的id和块所属的底层设备
- 如果缓存队列中有该块id,就返回一个指向这个缓存块的智能指针,与queue里面的智能指针并不是同一个
- 如果缓存队列里面查不到这个id,就新建一个缓存块智能指针
- 如果缓存队列满了,就移除掉只被引用过一次的块
- 将这个新建的缓存块加入队列
- Mutex:保证同一时间只有一个线程能访问这个数据,避免数据竞争
Arc<Mutex<BlockCache>>
表示一个原子引用计数智能指针,其中的数据是一个被互斥锁包裹的BlockCache
对象
- 强引用计数:
Arc::strong_count(&pair.1)
,即除了块缓存管理器保留的一份副本之外,在外面没有副本正在使用
磁盘布局及磁盘上数据结构
easy-fs 磁盘按照块编号从小到大顺序分成 5 个连续区域:
- 第一个区域只包括一个块,它是 超级块 (Super Block),用于定位其他连续区域的位置,检查文件系统合法性。
- 第二个区域是一个索引节点位图,长度为若干个块。它记录了索引节点区域中有哪些索引节点已经被分配出去使用了。
- 第三个区域是索引节点区域,长度为若干个块。其中的每个块都存储了若干个索引节点。
- 第四个区域是一个数据块位图,长度为若干个块。它记录了后面的数据块区域中有哪些已经被分配出去使用了。
- 最后的区域则是数据块区域,其中的每个被分配出去的块保存了文件或目录的具体内容。
在内核中使用 easy-fs
块设备驱动层
内核可访问的块设备实例:
在 qemu 上,我们使用
VirtIOBlock
访问 VirtIO 块设备,并将它全局实例化为 BLOCK_DEVICE
,使内核的其他模块可以访问内核索引节点层
OSInode
就表示进程中一个被打开的常规文件或目录
readable/writable
分别表明该文件是否允许通过sys_read/write
进行读写,读写过程中的偏移量offset
和Inode
则加上互斥锁丢到OSInodeInner
中
其中:Inode 索引节点
- 通过Inode可以:为此我们设计索引节点
Inode
暴露给文件系统的使用者,让他们能够直接对文件和目录进行操作
- 一个Inode对应一个DiskNode,DiskNode被放在磁盘中比较固定的位置,而
Inode
是放在内存中的,它是记录文件索引节点信息的数据结构
block_id
和block_offset
记录该Inode
对应的DiskInode
保存在磁盘上的具体位置方便我们后续对它进行访问
fs
是指向EasyFileSystem
的一个指针,因为对Inode
的种种操作实际上都是要通过底层的文件系统来完成
通过Inode访问磁盘节点的方法:
获取文件根目录:
目录节点实现的功能
impl
Inode
文件索引:
- 传入的参数为文件名
find
方法只会被根目录Inode
调用,文件系统中其他文件的Inode
不会调用这个方法。它首先调用find_inode_id
方法尝试从根目录的DiskInode
上找到要索引的文件名对应的 inode 编号。这就需要将根目录内容中的所有目录项都读到内存进行逐个比对。如果能够找到的话,find
方法会根据查到 inode 编号对应生成一个Inode
用于后续对文件的访问
文件列举:
- 列举根目录下全部的文件
文件创建:
- 传入文件名
- 如果文件已经存在,返回None
- 否则返回新创建的节点
文件清空:
- 在以某些标志位打开文件(例如带有 CREATE 标志打开一个已经存在的文件)的时候,需要首先将文件清空。在索引到文件的
Inode
之后可以调用clear
方法
文件读写:
文件描述符层
OSInode
也是要一种要放到进程文件描述符表中,通过 sys_read/write
进行读写的文件,我们需要为它实现 File
Trait文件系统相关内核机制实现
文件系统初始化
获取文件系统的根目录:
通过根目录,可以执行某些文件系统的功能:
通过 sys_open 打开文件
实现file_open内核函数:
这里主要是实现了
OpenFlags
各标志位的语义。例如只有 flags
参数包含 CREATE 标志位才允许创建文件;而如果文件已经存在,则清空文件的内容通过 sys_exec 加载并执行应用
有了文件系统支持后,
sys_exec
所需的表示应用 ELF 格式数据改为从文件系统中获取:- 通过open_file打开文件,OpenFlags::RDONLY表示用只读模式打开
- 如果找到文件了,就读出节点里面的data,然后当前任务执行这些程序
- 如果没找到文件就返回-1
小结
- ROOT_INODE
- 整个easy-fs文件系统只有一个目录,就是根目录ROOT_INODE
- 使用根目录查找文件,只需要输入文件的文件名,根目录就可以给出文件所在的inode_id。注意这个inode_id指的是内存中的inode编号,通过这个inode,可以获取文件索引节点DiskInode,这个节点就可以真正地去找到文件所在的block
- find方法:访问根节点的DiskInode,读上面的目录项,返回文件的索引节点
- find方法是通过调用find_inode_id方法实现的,将find_inode_id里面的DiskInode类型的参数看成是ROOT_INODE对应的磁盘索引节点
Lab6
pub fn sys_spawn(_path: *const u8) -> isize
- 需要稍作更改,因为不是靠loader去物理内存取出可执行文件的二进制数据了,而是靠文件系统在磁盘里面取出数据
- 需要仿照sys_exec里面,使用open_file函数获取_path文件名对应的Inode,然后通过read_all函数读出这个索引节点对应的物理磁盘节点(即文件)里面存的数据
pub fn sys_fstat(_fd: usize, _st: *mut Stat) -> isize
- 找到这个文件对应的inode_id
pub fn sys_linkat(_old_name: *const u8, _new_name: *const u8) -> isize
- 关于硬连接
- 基于这一点,在DiskInode中新增属性:
- 然后为DiskInode新增函数:硬连接数量++和硬连接数量—以及获取硬连接数量,这样一来就可以通过Inode的modify_disk_inode方法来获取硬连接数,然后每次删除硬连接和建立硬连接的时候修改硬连接数
硬链接数量通常记录在磁盘中的 inode 节点中。每个文件系统都有自己的数据结构来表示 inode 节点,在这些数据结构中会包含一个字段来记录硬链接数量。这样做的好处是可以保证硬链接数量的一致性,即使文件系统被卸载或重启,硬链接数量也能得到正确的维护
- 其余的主要就是仿照create就行了,将old_name对应的inode的inode_id和new_name作为参数新建目录项,唯一的区别就是不用去分配inode_id
- 但是我写的这个只能由root_inode调用
pub fn sys_unlinkat(_name: *const u8) -> isize
- 思路很简单,就是使用name找到目录项,然后将那个目录项修改为DirEntry::empty()就行了
- 如果inode只剩一个硬连接(最开始建立的那个),就把它的数据块也clear
- 最后inode对应的DiskInode的硬连接数—
- Author:orangec
- URL:orange’s blog | welcome to my blog (clovy.top)/cbdaf2884d9e442a8b569390c0066a35
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!
Relate Posts