在内核中使用 easy-fs¶
块设备驱动层¶
在 drivers
子模块中的 block/mod.rs
中,我们可以找到内核访问的块设备实例 BLOCK_DEVICE
:
// os/src/drivers/block/mod.rs
type BlockDeviceImpl = virtio_blk::VirtIOBlock;
lazy_static! {
pub static ref BLOCK_DEVICE: Arc<dyn BlockDevice> = Arc::new(BlockDeviceImpl::new());
}
在 qemu 上,我们使用 VirtIOBlock
访问 VirtIO 块设备,并将它全局实例化为 BLOCK_DEVICE
,使内核的其他模块可以访问。
在启动 Qemu 模拟器的时候,我们可以配置参数来添加一块 VirtIO 块设备:
1# os/Makefile
2
3FS_IMG := ../user/target/$(TARGET)/$(MODE)/fs.img
4
5run: build
6 @qemu-system-riscv64 \
7 -machine virt \
8 -nographic \
9 -bios $(BOOTLOADER) \
10 -device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA) \
11 -drive file=$(FS_IMG),if=none,format=raw,id=x0 \
12 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0
第 11 行,我们为虚拟机添加一块虚拟硬盘,内容为我们之前通过
easy-fs-fuse
工具打包的包含应用 ELF 的 easy-fs 镜像,并命名为x0
。第 12 行,我们将硬盘
x0
作为一个 VirtIO 总线中的一个块设备接入到虚拟机系统中。virtio-mmio-bus.0
表示 VirtIO 总线通过 MMIO 进行控制,且该块设备在总线中的编号为 0 。
内存映射 I/O (MMIO, Memory-Mapped I/O) 指通过特定的物理内存地址来访问外设的设备寄存器。查阅资料,可知 VirtIO 总线的 MMIO 物理地址区间为从 0x10001000 开头的 4KiB 。
在 config
子模块中我们硬编码 Qemu 上的 VirtIO 总线的 MMIO 地址区间(起始地址,长度)。在创建内核地址空间的时候需要建立页表映射:
// os/src/config.rs
pub const MMIO: &[(usize, usize)] = &[
(0x10001000, 0x1000),
];
// os/src/mm/memory_set.rs
use crate::config::MMIO;
impl MemorySet {
/// Without kernel stacks.
pub fn new_kernel() -> Self {
...
println!("mapping memory-mapped registers");
for pair in MMIO {
memory_set.push(MapArea::new(
(*pair).0.into(),
((*pair).0 + (*pair).1).into(),
MapType::Identical,
MapPermission::R | MapPermission::W,
), None);
}
memory_set
}
}
这里我们进行的是透明的恒等映射,让内核可以兼容于直接访问物理地址的设备驱动库。
由于设备驱动的开发过程比较琐碎,我们这里直接使用已有的 virtio-drivers crate,感兴趣的同学可以自行了解。
内核索引节点层¶
内核将 easy-fs
提供的 Inode
进一步封装为 OS 中的索引节点 OSInode
。
// os/src/fs/inode.rs
pub struct OSInode {
readable: bool,
writable: bool,
inner: UPSafeCell<OSInodeInner>,
}
pub struct OSInodeInner {
offset: usize,
inode: Arc<Inode>,
}
OSInode
就表示进程中一个被打开的常规文件或目录。 readable/writable
分别表明该文件是否允许通过 sys_read/write
进行读写,读写过程中的偏移量 offset
和 Inode
则加上互斥锁丢到 OSInodeInner
中。
文件描述符层¶
OSInode
也是要一种要放到进程文件描述符表中,通过 sys_read/write
进行读写的文件,我们需要为它实现 File
Trait :
// os/src/fs/inode.rs
impl File for OSInode {
fn readable(&self) -> bool { self.readable }
fn writable(&self) -> bool { self.writable }
fn read(&self, mut buf: UserBuffer) -> usize {
let mut inner = self.inner.lock();
let mut total_read_size = 0usize;
for slice in buf.buffers.iter_mut() {
let read_size = inner.inode.read_at(inner.offset, *slice);
if read_size == 0 {
break;
}
inner.offset += read_size;
total_read_size += read_size;
}
total_read_size
}
fn write(&self, buf: UserBuffer) -> usize {
let mut inner = self.inner.lock();
let mut total_write_size = 0usize;
for slice in buf.buffers.iter() {
let write_size = inner.inode.write_at(inner.offset, *slice);
assert_eq!(write_size, slice.len());
inner.offset += write_size;
total_write_size += write_size;
}
total_write_size
}
}
read/write
的实现也比较简单,只需遍历 UserBuffer
中的每个缓冲区片段,调用 Inode
写好的 read/write_at
接口就好了。注意 read/write_at
的起始位置是在 OSInode
中维护的 offset
,这个 offset
也随着遍历的进行被持续更新。在 read/write
的全程需要获取 OSInode
的互斥锁,保证两个进程无法同时访问同个文件。
本章我们为 File
Trait 新增了 readable/writable
两个抽象接口,从而在 sys_read/sys_write
的时候进行简单的访问权限检查。
文件系统相关内核机制实现¶
文件系统初始化¶
为了使用 easy-fs
提供的抽象,内核需要进行一些初始化操作。我们需要从块设备 BLOCK_DEVICE
上打开文件系统,并从文件系统中获取根目录的 inode 。
// os/src/fs/inode.rs
lazy_static! {
pub static ref ROOT_INODE: Arc<Inode> = {
let efs = EasyFileSystem::open(BLOCK_DEVICE.clone());
Arc::new(EasyFileSystem::root_inode(&efs))
};
}
这之后就可以使用根目录的 inode ROOT_INODE
,在内核中调用 easy-fs
的相关接口了。例如,在文件系统初始化完毕之后,调用 list_apps
函数来打印所有可用应用的文件名:
// os/src/fs/inode.rs
pub fn list_apps() {
println!("/**** APPS ****");
for app in ROOT_INODE.ls() {
println!("{}", app);
}
println!("**************/")
}
通过 sys_open 打开文件¶
在内核中也定义一份打开文件的标志 OpenFlags
:
// os/src/fs/inode.rs
bitflags! {
pub struct OpenFlags: u32 {
const RDONLY = 0;
const WRONLY = 1 << 0;
const RDWR = 1 << 1;
const CREATE = 1 << 9;
const TRUNC = 1 << 10;
}
}
impl OpenFlags {
/// Do not check validity for simplicity
/// Return (readable, writable)
pub fn read_write(&self) -> (bool, bool) {
if self.is_empty() {
(true, false)
} else if self.contains(Self::WRONLY) {
(false, true)
} else {
(true, true)
}
}
}
它的 read_write
方法可以根据标志的情况返回要打开的文件是否允许读写。简单起见,这里假设标志自身一定合法。
接着,我们实现 open_file
内核函数,可根据文件名打开一个根目录下的文件:
// os/src/fs/inode.rs
pub fn open_file(name: &str, flags: OpenFlags) -> Option<Arc<OSInode>> {
let (readable, writable) = flags.read_write();
if flags.contains(OpenFlags::CREATE) {
if let Some(inode) = ROOT_INODE.find(name) {
// clear size
inode.clear();
Some(Arc::new(OSInode::new(
readable,
writable,
inode,
)))
} else {
// create file
ROOT_INODE.create(name)
.map(|inode| {
Arc::new(OSInode::new(
readable,
writable,
inode,
))
})
}
} else {
ROOT_INODE.find(name)
.map(|inode| {
if flags.contains(OpenFlags::TRUNC) {
inode.clear();
}
Arc::new(OSInode::new(
readable,
writable,
inode
))
})
}
}
这里主要是实现了 OpenFlags
各标志位的语义。例如只有 flags
参数包含 CREATE 标志位才允许创建文件;而如果文件已经存在,则清空文件的内容。
在其基础上, sys_open
也就很容易实现了。
通过 sys_exec 加载并执行应用¶
有了文件系统支持后, sys_exec
所需的表示应用 ELF 格式数据改为从文件系统中获取:
1// os/src/syscall/process.rs
2
3pub fn sys_exec(path: *const u8, mut args: *const usize) -> isize {
4let token = current_user_token();
5let path = translated_str(token, path);
6let mut args_vec: Vec<String> = Vec::new();
7loop {
8 let arg_str_ptr = *translated_ref(token, args);
9 if arg_str_ptr == 0 {
10 break;
11 }
12 args_vec.push(translated_str(token, arg_str_ptr as *const u8));
13 unsafe {
14 args = args.add(1);
15 }
16}
17if let Some(app_inode) = open_file(path.as_str(), OpenFlags::RDONLY) {
18 let all_data = app_inode.read_all();
19 let task = current_task().unwrap();
20 let argc = args_vec.len();
21 task.exec(all_data.as_slice(), args_vec);
22 argc as isize
23} else {
24 -1
25}
注意上面代码片段中的高亮部分。当执行获取应用的 ELF 数据的操作时,首先调用 open_file
函数,以只读的方式在内核中打开应用文件并获取它对应的 OSInode
。接下来可以通过 OSInode::read_all
将该文件的数据全部读到一个向量 all_data
中:
之后,就可以从向量 all_data
中拿到应用中的 ELF 数据,当解析完毕并创建完应用地址空间后该向量将会被回收。
同样的,我们在内核中创建初始进程 initproc
也需要替换为基于文件系统的实现:
// os/src/task/mod.rs
lazy_static! {
pub static ref INITPROC: Arc<TaskControlBlock> = Arc::new({
let inode = open_file("ch6b_initproc", OpenFlags::RDONLY).unwrap();
let v = inode.read_all();
TaskControlBlock::new(v.as_slice())
});
}