实现记事本

为了实现上节中交互式终端的目标,先不管运行程序,我们首先要能够通过键盘向终端程序中输入。也就是说,我们要实现一个用户程序,它能够接受键盘的输入,并将键盘输入的字符显示在屏幕上。这不能叫一个终端,姑且叫它记事本吧。

这个用户程序需要的功能是:接受键盘输入(可以被称为“标准输入”)的一个字符。

为此我们需约定这样一个系统调用:

  • 文件读入,系统调用

    实现记事本 - 图1

我们先在用户程序模板中声明该系统调用:

  1. // usr/rust/src/syscall.rs
  2. enum SyscallId {
  3. Read = 63,
  4. }
  5. pub fn sys_read(fd: usize, base: *const u8, len: usize) -> i64 {
  6. sys_call(SyscallId::Read, fd, base as usize, len, 0)
  7. }

这里的系统调用接口设计上是一个记事本所需功能更强的文件读入:传入的参数中,fd 表示文件描述符,base 表示要将读入的内容保存到的虚拟地址,len 表示最多读入多少字节。其返回值是成功读入的字节数。

方便起见,我们还是将这个系统调用封装一下来实现我们所需的功能。

  1. // usr/rust/src/io.rs
  2. use crate::syscall::sys_read;
  3. // 每个进程默认打开三个文件
  4. // 标准输入 stdin fd = 0
  5. // 标准输出 stdout fd = 1
  6. // 标准错误输出 stderr fd = 2
  7. pub const STDIN: usize = 0;
  8. // 调用 sys_read 从标准输入读入一个字符
  9. pub fn getc() -> u8 {
  10. let mut c = 0u8;
  11. assert_eq!(sys_read(STDIN, &mut c, 1), 1);
  12. c
  13. }

接下来我们可以利用 getc 着手实现我们的记事本了!

  1. // usr/rust/src/bin/notebook.rs
  2. #![no_std]
  3. #![no_main]
  4. #[macro_use]
  5. extern crate user;
  6. use user::io::getc;
  7. use user::io::putchar;
  8. const LF: u8 = 0x0au8;
  9. const CR: u8 = 0x0du8;
  10. const BS: u8 = 0x08u8;
  11. const DL: u8 = 0x7fu8;
  12. #[no_mangle]
  13. pub fn main() {
  14. println!("Welcome to notebook!");
  15. let mut line_count = 0;
  16. loop {
  17. let c = getc();
  18. match c {
  19. LF | CR => {
  20. line_count = 0;
  21. print!("{}", LF as char);
  22. print!("{}", CR as char);
  23. }
  24. DL => if line_count > 0 {
  25. // 支持退格键
  26. // 写法来源:https://github.com/mit-pdos/xv6-riscv-fall19/blob/xv6-riscv-fall19/kernel/console.c#L41
  27. putchar(BS as char);
  28. putchar(' ');
  29. putchar(BS as char);
  30. line_count -= 1;
  31. }
  32. _ => {
  33. line_count += 1;
  34. print!("{}", c as char);
  35. }
  36. }
  37. }
  38. }

很简单,就是将接受到的字符打印到屏幕上。

看一下 getc 的实现,我们满怀信心 sys_read 的返回值是

实现记事本 - 图2

,也就是确保一定能够读到字符。不过真的是这样吗?

缓冲区

实际上,我们用一个缓冲区来表示标准输入。你可以将其看作一个字符队列。

  • 键盘是生产者:每当你按下键盘,所对应的字符会加入队尾;
  • sys_read 是消费者:每当调用 sys_read 函数,会将队头的字符取出,并返回。

sys_read 的时候,如果队列不是空的,那么一切都好;如果队列是空的,由于它要保证能够读到字符,因此它只能够等到什么时候队列中加入了新的元素再返回。

而这里的“等”,又有两种等法:

最简单的等法是:在原地 while (q.empty()) {} 。也就是知道队列非空才跳出循环,取出队头的字符并返回。

另一种方法是:当 sys_read 发现队列是空的时候,自动放弃 CPU 资源进入睡眠(或称阻塞)状态,也就是从调度单元中移除当前所在线程,不再参与调度。而等到某时刻按下键盘的时候,发现有个线程在等着这个队列非空,于是赶快将它唤醒,重新加入调度单元,等待 CPU 资源分配过来继续执行。

后者相比前者的好处在于:前者占用了 CPU 资源却不干活,只是在原地等着;而后者虽然也没法干活,却很有自知之明的把 CPU 资源让给其他线程使用,这样就提高了 CPU 的利用率。

我们就使用后者来实现 sys_read

条件变量

这种线程将 CPU 资源放弃,并等到某个条件满足才准备继续运行的机制,可以使用条件变量 (Condition Variable) 来描述。而它的实现,需要依赖几个新的线程调度机制。

  1. // src/process/mod.rs
  2. // 当前线程自动放弃 CPU 资源并进入阻塞状态
  3. // 线程状态: Running(Tid) -> Sleeping
  4. pub fn yield_now() {
  5. CPU.yield_now();
  6. }
  7. // 某些条件满足,线程等待 CPU 资源从而继续执行
  8. // 线程状态: Sleeping -> Ready
  9. pub fn wake_up(tid: Tid) {
  10. CPU.wake_up(tid);
  11. }
  12. // 获取当前线程的 Tid
  13. pub fn current_tid() -> usize {
  14. CPU.current_tid()
  15. }
  16. // src/process/processor.rs
  17. impl Processor {
  18. ...
  19. pub fn yield_now(&self) {
  20. let inner = self.inner();
  21. if !inner.current.is_none() {
  22. unsafe {
  23. // 由于要进入 idle 线程,必须关闭异步中断
  24. // 手动保存之前的 sstatus
  25. let flags = disable_and_store();
  26. let tid = inner.current.as_mut().unwrap().0;
  27. let thread_info = inner.pool.threads[tid].as_mut().expect("thread not existed when yielding");
  28. // 修改线程状态
  29. thread_info.status = Status::Sleeping;
  30. // 切换到 idle 线程
  31. inner.current
  32. .as_mut()
  33. .unwrap()
  34. .1
  35. .switch_to(&mut *inner.idle);
  36. // 从 idle 线程切换回来
  37. // 恢复 sstatus
  38. restore(flags);
  39. }
  40. }
  41. }
  42. pub fn wake_up(&self, tid: Tid) {
  43. let inner = self.inner();
  44. inner.pool.wakeup(tid);
  45. }
  46. pub fn current_tid(&self) -> usize {
  47. self.inner().current.as_mut().unwrap().0 as usize
  48. }
  49. }
  50. // src/process/thread_pool.rs
  51. // 改成 public
  52. pub struct ThreadInfo {
  53. // 改成 public
  54. pub status: Status,
  55. pub thread: Option<Box<Thread>>,
  56. }
  57. pub struct ThreadPool {
  58. // 改成 public
  59. pub threads: Vec<Option<ThreadInfo>>,
  60. scheduler: Box<dyn Scheduler>,
  61. }
  62. impl ThreadPool {
  63. ...
  64. pub fn wakeup(&mut self, tid: Tid) {
  65. let proc = self.threads[tid].as_mut().expect("thread not exist when waking up");
  66. proc.status = Status::Ready;
  67. self.scheduler.push(tid);
  68. }
  69. }

下面我们用这几种线程调度机制来实现条件变量。

  1. // src/lib.rs
  2. mod sync;
  3. // src/sync/mod.rs
  4. pub mod condvar;
  5. // src/sync/condvar.rs
  6. use spin::Mutex;
  7. use alloc::collections::VecDeque;
  8. use crate::process::{ Tid, current_tid, yield_now, wake_up };
  9. #[derive(Default)]
  10. pub struct Condvar {
  11. // 加了互斥锁的 Tid 队列
  12. // 存放等待此条件变量的众多线程
  13. wait_queue: Mutex<VecDeque<Tid>>,
  14. }
  15. impl Condvar {
  16. pub fn new() -> Self {
  17. Condvar::default()
  18. }
  19. // 当前线程等待某种条件满足才能继续执行
  20. pub fn wait(&self) {
  21. // 将当前 Tid 加入此条件变量的等待队列
  22. self.wait_queue
  23. .lock()
  24. .push_back(current_tid());
  25. // 当前线程放弃 CPU 资源
  26. yield_now();
  27. }
  28. // 条件满足
  29. pub fn notify(&self) {
  30. // 弹出等待队列中的一个线程
  31. let tid = self.wait_queue.lock().pop_front();
  32. if let Some(tid) = tid {
  33. // 唤醒该线程
  34. wake_up(tid);
  35. }
  36. }
  37. }

讲清楚了机制,下面我们看一下具体实现。

缓冲区实现

  1. // src/fs/mod.rs
  2. pub mod stdio;
  3. // src/fs/stdio.rs
  4. use alloc::{ collections::VecDeque, sync::Arc };
  5. use spin::Mutex;
  6. use crate::process;
  7. use crate::sync::condvar::*;
  8. use lazy_static::*;
  9. pub struct Stdin {
  10. // 字符队列
  11. buf: Mutex<VecDeque<char>>,
  12. // 条件变量
  13. pushed: Condvar,
  14. }
  15. impl Stdin {
  16. pub fn new() -> Self {
  17. Stdin {
  18. buf: Mutex::new(VecDeque::new()),
  19. pushed: Condvar::new(),
  20. }
  21. }
  22. // 生产者:输入字符
  23. pub fn push(&self, ch: char) {
  24. // 将字符加入字符队列
  25. self.buf
  26. .lock()
  27. .push_back(ch);
  28. // 如果此时有线程正在等待队列非空才能继续下去
  29. // 将其唤醒
  30. self.pushed.notify();
  31. }
  32. // 消费者:取出字符
  33. // 运行在请求字符输入的线程上
  34. pub fn pop(&self) -> char {
  35. loop {
  36. // 将代码放在 loop 里面防止再复制一遍
  37. // 尝试获取队首字符
  38. let ret = self.buf.lock().pop_front();
  39. match ret {
  40. Some(ch) => {
  41. // 获取到了直接返回
  42. return ch;
  43. },
  44. None => {
  45. // 否则队列为空,通过 getc -> sys_read 获取字符的当前线程放弃 CPU 资源
  46. // 进入阻塞状态等待唤醒
  47. self.pushed.wait();
  48. // 被唤醒后回到循环开头,此时可直接返回
  49. }
  50. }
  51. }
  52. }
  53. }
  54. lazy_static! {
  55. pub static ref STDIN: Arc<Stdin> = Arc::new(Stdin::new());
  56. }

生产者:键盘中断

首先我们要能接受到外部中断,而 OpenSBI 默认将外部中断和串口开关都关上了,因此我们需要手动将他们打开:

  1. // src/interrupt.rs
  2. use crate::memory::access_pa_via_va;
  3. use riscv::register::sie;
  4. pub fn init() {
  5. ...
  6. // enable external interrupt
  7. sie::set_sext();
  8. // closed by OpenSBI, so we open them manually
  9. // see https://github.com/rcore-os/rCore/blob/54fddfbe1d402ac1fafd9d58a0bd4f6a8dd99ece/kernel/src/arch/riscv32/board/virt/mod.rs#L4
  10. init_external_interrupt();
  11. enable_serial_interrupt();
  12. }
  13. pub unsafe fn init_external_interrupt() {
  14. let HART0_S_MODE_INTERRUPT_ENABLES: *mut u32 = access_pa_via_va(0x0c00_2080) as *mut u32;
  15. const SERIAL: u32 = 0xa;
  16. HART0_S_MODE_INTERRUPT_ENABLES.write_volatile(1 << SERIAL);
  17. }
  18. pub unsafe fn enable_serial_interrupt() {
  19. let UART16550: *mut u8 = access_pa_via_va(0x10000000) as *mut u8;
  20. UART16550.add(4).write_volatile(0x0B);
  21. UART16550.add(1).write_volatile(0x01);
  22. }

这里的内存尚未被映射,我们在内存模块初始化时完成映射:

  1. // src/memory/mod.rs
  2. pub fn kernel_remap() {
  3. let mut memory_set = MemorySet::new();
  4. extern "C" {
  5. fn bootstack();
  6. fn bootstacktop();
  7. }
  8. memory_set.push(
  9. bootstack as usize,
  10. bootstacktop as usize,
  11. MemoryAttr::new(),
  12. Linear::new(PHYSICAL_MEMORY_OFFSET),
  13. None,
  14. );
  15. memory_set.push(
  16. access_pa_via_va(0x0c00_2000),
  17. access_pa_via_va(0x0c00_3000),
  18. MemoryAttr::new(),
  19. Linear::new(PHYSICAL_MEMORY_OFFSET),
  20. None
  21. );
  22. memory_set.push(
  23. access_pa_via_va(0x1000_0000),
  24. access_pa_via_va(0x1000_1000),
  25. MemoryAttr::new(),
  26. Linear::new(PHYSICAL_MEMORY_OFFSET),
  27. None
  28. );
  29. unsafe {
  30. memory_set.activate();
  31. }
  32. }

也因此,内存模块要比中断模块先初始化。

  1. // src/init.rs
  2. #[no_mangle]
  3. pub extern "C" fn rust_main() -> ! {
  4. extern "C" {
  5. fn end();
  6. }
  7. crate::memory::init(
  8. ((end as usize - KERNEL_BEGIN_VADDR + KERNEL_BEGIN_PADDR) >> 12) + 1,
  9. PHYSICAL_MEMORY_END >> 12
  10. );
  11. crate::interrupt::init();
  12. crate::fs::init();
  13. crate::process::init();
  14. crate::timer::init();
  15. crate::process::run();
  16. loop {}
  17. }

随后,我们对外部中断进行处理:

  1. // src/interrupt.rs
  2. #[no_mangle]
  3. pub fn rust_trap(tf: &mut TrapFrame) {
  4. ...
  5. Trap::Interrupt(Interrupt::SupervisorExternal) => external(),
  6. ...
  7. }
  8. fn external() {
  9. // 键盘属于一种串口设备,而实际上有很多种外设
  10. // 这里我们只考虑串口
  11. let _ = try_serial();
  12. }
  13. fn try_serial() -> bool {
  14. // 通过 OpenSBI 获取串口输入
  15. match super::io::getchar_option() {
  16. Some(ch) => {
  17. // 将获取到的字符输入标准输入
  18. if (ch == '\r') {
  19. crate::fs::stdio::STDIN.push('\n');
  20. }
  21. else {
  22. crate::fs::stdio::STDIN.push(ch);
  23. }
  24. true
  25. },
  26. None => false
  27. }
  28. }
  29. // src/io.rs
  30. pub fn getchar() -> char {
  31. let c = sbi::console_getchar() as u8;
  32. match c {
  33. 255 => '\0',
  34. c => c as char
  35. }
  36. }
  37. // 调用 OpenSBI 接口
  38. pub fn getchar_option() -> Option<char> {
  39. let c = sbi::console_getchar() as isize;
  40. match c {
  41. -1 => None,
  42. c => Some(c as u8 as char)
  43. }
  44. }

消费者:sys_read 实现

这就很简单了。

  1. // src/syscall.rs
  2. pub const SYS_READ: usize = 63;
  3. pub fn syscall(id: usize, args: [usize; 3], tf: &mut TrapFrame) -> isize {
  4. match id {
  5. SYS_READ => {
  6. sys_read(args[0], args[1] as *mut u8, args[2])
  7. }
  8. ...
  9. }
  10. }
  11. // 这里 fd, len 都没有用到
  12. fn sys_read(fd: usize, base: *mut u8, len: usize) -> isize {
  13. unsafe {
  14. *base = crate::fs::stdio::STDIN.pop() as u8;
  15. }
  16. return 1;
  17. }

这里我们要写入用户态内存,但是 CPU 默认并不允许在内核态访问用户态内存,因此我们要在内存初始化的时候将开关打开:

  1. // src/memory/mod.rs
  2. use riscv::register::sstatus;
  3. pub fn init(l: usize, r: usize) {
  4. unsafe {
  5. sstatus::set_sum();
  6. }
  7. // 以下不变
  8. FRAME_ALLOCATOR.lock().init(l, r);
  9. init_heap();
  10. kernel_remap();
  11. println!("++++ setup memory! ++++");
  12. }

现在我们可以将要运行的程序从 rust/hello_world 改成 rust/notebook 了!

将多余的线程换入换出提示信息删掉,运行一下,我们已经实现了字符的输入及显示了!可以享受输入带来的乐趣了!(大雾

如果记事本不能正常工作,可以在这里找到已有的代码。