文件读写

心态炸了。本来已经写好了,因为文件不小心被删了只能重写一遍。:cry:

我们要在用户态支持文件读写功能。具体的用户态程序如下:

  1. // usr/rust/src/bin/write.rs
  2. #![no_std]
  3. #![no_main]
  4. extern crate alloc;
  5. #[macro_use]
  6. extern crate user;
  7. use user::io::*;
  8. use user::syscall::{
  9. sys_open,
  10. sys_close,
  11. sys_read,
  12. sys_write,
  13. };
  14. const BUFFER_SIZE: usize = 20;
  15. const FILE: &'static str = "temp\0";
  16. const TEXT: &'static str = "Hello world!\0";
  17. #[no_mangle]
  18. pub fn main() -> usize {
  19. // 将字符串写到文件 temp 中
  20. let write_fd = sys_open(FILE.as_ptr(), O_WRONLY);
  21. sys_write(write_fd as usize, TEXT.as_ptr(), TEXT.len());
  22. println!("write to file 'temp' successfully...");
  23. sys_close(write_fd as i32);
  24. // 将字符串从文件 temp 读入内存
  25. let read_fd = sys_open(FILE.as_ptr(), O_RDONLY);
  26. let mut read = [0u8; BUFFER_SIZE];
  27. sys_read(read_fd as usize, &read[0] as *const u8, BUFFER_SIZE);
  28. println!("read from file 'temp' successfully...");
  29. // 检查功能是否正确
  30. let len = (0..BUFFER_SIZE).find(|&i| read[i] as u8 == 0).unwrap();
  31. print!("content = ");
  32. for i in 0usize..len {
  33. assert!(read[i] == TEXT.as_bytes()[i]);
  34. putchar(read[i] as char);
  35. }
  36. putchar('\n');
  37. sys_close(read_fd as i32);
  38. 0
  39. }

我们要实现两个新的系统调用:

  1. // usr/rust/src/syscall.rs
  2. enum SyscallId {
  3. Open = 56,
  4. Close = 57,
  5. ...
  6. }
  7. pub fn sys_open(path: *const u8, flags: i32) -> i64 {
  8. sys_call(SyscallId::Open, path as usize, flags as usize, 0, 0)
  9. }
  10. pub fn sys_close(fd: i32) -> i64 {
  11. sys_call(SyscallId::Close, fd as usize, 0, 0, 0)
  12. }

sys_open, sys_close 的 syscall id 分别为

文件读写 - 图1

。为了说明它们的功能,我们需要先介绍一下文件描述符 (fd, File Descriptor) 的概念。在一个进程内,文件描述符用来描述一个打开的文件,它被所有的线程共享。而 sys_open 的功能是为当前进程打开一个文件,并返回这个文件对应的文件描述符。这个文件描述符接下来可被读写相关的系统调用 sys_read, sys_write 利用来访问对应的文件。当所有的访问结束后,进程通过 sys_close 关闭文件描述符对应的文件,并回收文件描述符。

我们看到,传入 sys_open 的参数除了 path 表示文件路径,还有一个标志位 flags。它的作用是控制当前进程对打开文件的读写权限。与 Linux 一致,我们定义下面的标志位:

  1. // usr/rust/src/io.rs
  2. pub const O_RDONLY: i32 = 0; // 只读
  3. pub const O_WRONLY: i32 = 1; // 只写
  4. pub const O_RDWR: i32 = 2; // 可读可写
  5. pub const O_CREAT: i32 = 64; // 打开文件时若文件不存在,创建它
  6. pub const O_APPEND: i32 = 1024; // 从文件结尾开始写入

此外,之前定义的 sys_write 功能太弱,仅支持向标准输出写入单个字符。我们将其按照 sys_read 的标准重写,支持文件描述符、内存缓冲区以及长度。

  1. // usr/rust/src/syscall.rs
  2. pub fn sys_write(fd: usize, base: *const u8, len: usize) -> i64 {
  3. sys_call(SyscallId::Write, fd, base as usize, len, 0)
  4. }

调用 sys_write 的地方也要做相应修改:

  1. // usr/rust/src/io.rs
  2. +pub const STDOUT: usize = 1;
  3. pub fn putchar(ch: char) {
  4. - sys_write(ch as u8);
  5. + sys_write(STDOUT, &ch as *const char as *const u8, 1);
  6. }

注意到我们在打开文件时并没有使用标志位 O_CREAT ,因此假定文件系统内 temp 文件一定存在。因此,我们直接把这个文件和众多用户程序一起打包到磁盘镜像中。

  1. // usr/Makefile
  2. $(sfsimg): rcore-fs-fuse rust
  3. + @dd if=/dev/zero of=$(out_dir)/temp bs=1k count=2
  4. @rcore-fs-fuse --fs sfs $@ $(out_dir) zip

现在用户态准备完毕,我们来看看内核态要做出哪些变化。

首先我们来支持文件描述符机制。它是用来描述进程内打开的文件,而这个结构体我们定义如下:

  1. // os/src/fs/mod.rs
  2. pub mod file;
  3. // os/src/fs/file.rs
  4. use alloc::sync::Arc;
  5. use rcore_fs::vfs::INode;
  6. use rcore_fs_sfs::INodeImpl;
  7. use crate::fs::ROOT_INODE;
  8. // 文件描述符类型
  9. #[derive(Copy,Clone,Debug)]
  10. pub enum FileDescriptorType {
  11. FD_NONE, // 空类型
  12. FD_INODE, // INode 类型
  13. FD_DEVICE, // 设备类型
  14. }
  15. // 进程内打开的文件
  16. #[derive(Clone)]
  17. pub struct File {
  18. fdtype: FileDescriptorType,
  19. // 进程对该文件的权限
  20. readable: bool,
  21. writable: bool,
  22. // 该文件的 INode 指针,用于进行实际读写
  23. pub inode: Option<Arc<dyn INode>>,
  24. // 进程中该文件的偏移量指针
  25. offset: usize,
  26. }

一些简单的函数:

  1. // os/src/fs/file.rs
  2. impl File {
  3. // 初始化
  4. pub fn default() -> Self {
  5. File {
  6. fdtype: FileDescriptorType::FD_NONE,
  7. readable: false,
  8. writable: false,
  9. inode: None,
  10. offset: 0,
  11. }
  12. }
  13. // get/set 函数
  14. pub fn set_readable(&mut self, v: bool) { self.readable = v; }
  15. pub fn set_writable(&mut self, v: bool) { self.writable = v; }
  16. pub fn get_readable(&self) -> bool { self.readable }
  17. pub fn get_writable(&self) -> bool { self.writable }
  18. pub fn set_fdtype(&mut self, t: FileDescriptorType) { self.fdtype = t; }
  19. pub fn get_fdtype(&self) -> FileDescriptorType { self.fdtype }
  20. pub fn set_offset(&mut self, o: usize) { self.offset = o; }
  21. pub fn get_offset(&self) -> usize { self.offset }
  22. }

每个进程都有一个打开的文件列表,我们需要在进程控制块中开一个新的字段 ofile 。数组中的一项如果是 None ,表明这个文件描述符可用。

  1. // os/src/consts.rs
  2. +pub const NOFILE: usize = 16;
  3. // os/src/process/structs.rs
  4. +use crate::fs::file::File;
  5. +use spin::Mutex;
  6. +use alloc::sync::Arc;
  7. pub struct Thread {
  8. pub context: Context,
  9. pub kstack: KernelStack,
  10. pub wait: Option<Tid>,
  11. + pub ofile: [Option<Arc<Mutex<File>>>; NOFILE],
  12. }

在新建内核线程 new_kernel 以及获得启动线程 get_boot_thread 中,最终的返回值也需要进行变化:

  1. // os/src/process/structs.rs
  2. Box::new(Thread {
  3. ...
  4. + ofile: [None; NOFILE],
  5. })

为了能够编译通过,我们需要打开以下开关:

  1. // os/src/lib.rs
  2. #![feature(const_in_array_repeat_expressions)]

新建用户进程 new_user 中,我们进行类似的初始化,但是要将数组的前三项

文件读写 - 图2

赋值。这是因为它们分别表示标准输入 (stdin)、标准输出 (stdout)、标准错误输出 (stderr) ,在创建用户进程的时候默认打开。也就是再分配文件描述符的时候,是从

文件读写 - 图3

开始分配的。

  1. // os/src/process/structs.rs
  2. impl Thread {
  3. pub unsafe fn new_user(data: &[u8], wait_thread: Option<Tid>) -> Box<Thread> {
  4. ...
  5. let mut thread = Thread {
  6. context: Context::new_user_thread(entry_addr, ustack_top, kstack.top(), vm.token()),
  7. kstack: kstack,
  8. wait: wait_thread,
  9. ofile: [None; NOFILE],
  10. };
  11. for i in 0..3 {
  12. thread.ofile[i] = Some(Arc::new(Mutex::new(File::default())));
  13. }
  14. Box::new(thread)
  15. }
  16. }

在进程中分配、回收文件描述符:

  1. // os/src/process/structs.rs
  2. impl Thread {
  3. // 分配文件描述符
  4. pub fn alloc_fd(&mut self) -> i32 {
  5. let mut fd = 0;
  6. for i in 0usize..NOFILE {
  7. if self.ofile[i].is_none() {
  8. fd = i;
  9. break;
  10. }
  11. }
  12. self.ofile[fd] = Some(Arc::new(Mutex::new(File::default())));
  13. fd as i32
  14. }
  15. // 回收文件描述符
  16. pub fn dealloc_fd(&mut self, fd: i32) {
  17. assert!(self.ofile[fd as usize].is_some());
  18. self.ofile[fd as usize] = None;
  19. }
  20. }

接下来我们看看在内核态中如何实现这些系统调用。

  1. // os/src/syscall.rs
  2. pub const SYS_OPEN: usize = 56;
  3. pub const SYS_CLOSE: usize = 57;
  4. pub fn syscall(id: usize, args: [usize; 3], tf: &mut TrapFrame) -> isize {
  5. match id {
  6. SYS_OPEN => sys_open(args[0] as *const u8, args[1] as i32),
  7. SYS_CLOSE => sys_close(args[0] as i32),
  8. SYS_READ => unsafe { sys_read(args[0], args[1] as *mut u8, args[2]) },
  9. SYS_WRITE => unsafe { sys_write(args[0], args[1] as *const u8, args[2]) },
  10. ...
  11. }
  12. }

与之前的系统调用不同之处在于,在这些系统调用内,当前进程的进程控制块可能被修改。所以我们需要当前进程进程控制块的可变引用。

  1. // os/src/process/processor.rs
  2. impl Processor {
  3. ...
  4. pub fn current_thread_mut(&self) -> &mut Thread {
  5. self.inner().current.as_mut().unwrap().1.as_mut()
  6. }
  7. }
  8. // os/src/process/mod.rs
  9. pub fn current_thread_mut() -> &'static mut Thread {
  10. CPU.current_thread_mut()
  11. }

依次实现这些系统调用。

对于 sys_open ,分配文件描述符,并找到对应文件的 INode ,将指针保存下来。

  1. // os/src/syscall.rs
  2. fn sys_open(path: *const u8, flags: i32) -> isize {
  3. let thread = process::current_thread_mut();
  4. let fd = thread.alloc_fd() as isize;
  5. thread.ofile[fd as usize]
  6. .as_ref()
  7. .unwrap()
  8. .lock()
  9. .open_file(unsafe { from_cstr(path) }, flags);
  10. fd
  11. }
  12. // os/src/fs/file.rs
  13. pub fn open_file(&mut self, path: &'static str, flags: i32) {
  14. self.set_fdtype(FileDescriptorType::FD_INODE);
  15. self.set_readable(true);
  16. if (flags & 1) > 0 {
  17. self.set_readable(false);
  18. }
  19. if (flags & 3) > 0 {
  20. self.set_writable(true);
  21. }
  22. unsafe {
  23. self.inode = Some(ROOT_INODE.lookup(path).unwrap().clone());
  24. }
  25. self.set_offset(0);
  26. }

对于 sys_close ,直接回收对应的文件描述符即可。

  1. // os/src/syscall.rs
  2. fn sys_close(fd: i32) -> isize {
  3. let thread = process::current_thread_mut();
  4. assert!(thread.ofile[fd as usize].is_some());
  5. thread.dealloc_fd(fd);
  6. 0
  7. }

对于 sys_open ,如果不是标准输入的话,应该从对应的 INode 中读入。

  1. // os/src/syscall.rs
  2. unsafe fn sys_read(fd: usize, base: *mut u8, len: usize) -> isize {
  3. if fd == 0 {
  4. // 如果是标准输入
  5. unsafe {
  6. *base = crate::fs::stdio::STDIN.pop() as u8;
  7. }
  8. return 1;
  9. } else {
  10. let mut thread = process::current_thread_mut();
  11. assert!(thread.ofile[fd].is_some());
  12. let mut file = thread.ofile[fd as usize].as_ref().unwrap().lock();
  13. assert!(file.get_readable());
  14. match file.get_fdtype() {
  15. FileDescriptorType::FD_INODE => {
  16. let mut offset = file.get_offset();
  17. let s = file
  18. .inode
  19. .clone()
  20. .unwrap()
  21. .read_at(offset, core::slice::from_raw_parts_mut(base, len))
  22. .unwrap();
  23. offset += s;
  24. file.set_offset(offset);
  25. return s as isize;
  26. }
  27. _ => {
  28. panic!("fdtype not handled!");
  29. }
  30. }
  31. }
  32. }

对于 sys_write ,如果不是标准输出的话,输入到对应的 INode 中。

  1. // os/src/syscall.rs
  2. unsafe fn sys_write(fd: usize, base: *const u8, len: usize) -> isize {
  3. if fd == 1 {
  4. assert!(len == 1);
  5. unsafe { crate::io::putchar(*base as char); }
  6. return 1;
  7. } else {
  8. let thread = process::current_thread_mut();
  9. assert!(thread.ofile[fd].is_some());
  10. let mut file = thread.ofile[fd as usize].as_ref().unwrap().lock();
  11. assert!(file.get_writable());
  12. match file.get_fdtype() {
  13. FileDescriptorType::FD_INODE => {
  14. let mut offset = file.get_offset();
  15. let s = file
  16. .inode
  17. .clone()
  18. .unwrap()
  19. .write_at(offset, core::slice::from_raw_parts(base, len))
  20. .unwrap();
  21. offset += s;
  22. file.set_offset(offset);
  23. return s as isize;
  24. }
  25. _ => {
  26. panic!("fdtype not handled!");
  27. }
  28. }
  29. 0
  30. }
  31. }

让我们测试一下用户程序能否正常运行:

文件读写测试

  1. >> rust/write
  2. searching for program rust/write
  3. write to file 'temp' successfully...
  4. read from file 'temp' successfully...
  5. content = Hello world!
  6. thread 1 exited, exit code = 0

所有的代码可以在这里找到。