实现终端

我们的终端也很简单:其功能为你输入想要执行的用户程序如 rust/hello_world ,随后按下回车,内核就会帮你执行这个程序。

所以,我们需要实现一个新的系统调用:

  • 执行程序,系统调用

    实现终端 - 图1

终端的实现基于上一节所讲的记事本:

  1. // usr/rust/src/bin/user_shell.rs
  2. #![no_std]
  3. #![no_main]
  4. extern crate alloc;
  5. #[macro_use]
  6. extern crate user;
  7. const LF: u8 = 0x0au8;
  8. const CR: u8 = 0x0du8;
  9. const DL: u8 = 0x7fu8;
  10. const BS: u8 = 0x08u8;
  11. use alloc::string::String;
  12. use user::io::getc;
  13. use user::io::putchar;
  14. use user::syscall::sys_exec;
  15. #[no_mangle]
  16. pub fn main() {
  17. println!("Rust user shell");
  18. // 保存本行已经输入的内容
  19. let mut line: String = String::new();
  20. print!(">> ");
  21. loop {
  22. let c = getc();
  23. match c {
  24. LF | CR => {
  25. // 如果遇到回车或换行
  26. println!("");
  27. if !line.is_empty() {
  28. println!("searching for program {}", line);
  29. // 这里在程序名结尾需要手动添加 '\0',因为 Rust 编译器不会帮我们在字符串结尾附上 '\0'
  30. line.push('\0');
  31. // 使用系统调用执行程序
  32. sys_exec(line.as_ptr());
  33. // 清空本行内容
  34. line.clear();
  35. }
  36. print!(">> ");
  37. }
  38. DL => {
  39. // 如果是退格键
  40. if !line.is_empty() {
  41. putchar(BS as char);
  42. putchar(' ');
  43. putchar(BS as char);
  44. line.pop();
  45. }
  46. }
  47. _ => {
  48. // 否则正常输入
  49. print!("{}", c as char);
  50. line.push(c as char);
  51. }
  52. }
  53. }
  54. }

以及用户态的系统调用

  1. // usr/rust/src/syscall.rs
  2. enum SyscallId {
  3. ...
  4. Exec = 221,
  5. }
  6. // 传入路径字符串的地址
  7. pub fn sys_exec(path: *const u8) {
  8. sys_call(SyscallId::Exec, path as usize, 0, 0, 0);
  9. }

那我们如何在内核中实现这个系统调用呢?大概流程是:

  1. 解析传入的路径字符串
  2. 创建一个对应的用户线程,并加入调度

现在的问题是我们只有一个输出即输出到屏幕,如果用户线程和终端线程同时运行,他们输出的信息会混杂在一起让我们很难区分。因此我们的做法是:借用上一节阻塞的方法,当终端线程准备启动其他用户线程时,它会放弃 CPU 资源进入阻塞状态;直到被启动的用户线程结束后才唤醒启动它的终端线程。这样就可解决这个问题。

但是也不必使用上一节中的条件变量,我们在线程结构体中加入:

  1. // src/process/structs.rs
  2. pub struct Thread {
  3. ...
  4. pub wait: Option<Tid>,
  5. }

这表示正在等待这个线程运行结束的线程 Tid 。在线程退出时:

  1. // src/process/processor.rs
  2. impl Processor {
  3. pub fn exit(&self, code: usize) -> ! {
  4. disable_and_store();
  5. let inner = self.inner();
  6. let tid = inner.current.as_ref().unwrap().0;
  7. inner.pool.exit(tid);
  8. println!("thread {} exited, exit code = {}", tid, code);
  9. // 加入这个判断
  10. // 如果有一个线程正在等待当前线程运行结束
  11. // 将其唤醒
  12. if let Some(wait) = inner.current.as_ref().unwrap().1.wait {
  13. inner.pool.wakeup(wait);
  14. }
  15. inner.current
  16. .as_mut()
  17. .unwrap()
  18. .1
  19. .switch_to(&mut inner.idle);
  20. loop {}
  21. }
  22. }

由于 Thread 的字段发生了变化,之前所有创建 Thread 的代码都要做出相应的修改,将 wait 字段的值设置为 None 即可。新建用户线程时,要新加入一个参数 wait_thread

  1. // src/process/structs.rs
  2. impl Thread {
  3. pub fn new_kernel(entry: usize) -> Box<Thread> {
  4. unsafe {
  5. let kstack_ = KernelStack::new();
  6. Box::new(Thread {
  7. context: Context::new_kernel_thread(entry, kstack_.top(), satp::read().bits()),
  8. kstack: kstack_,
  9. wait: None
  10. })
  11. }
  12. }
  13. pub fn get_boot_thread() -> Box<Thread> {
  14. Box::new(Thread {
  15. context: Context::null(),
  16. kstack: KernelStack::new_empty(),
  17. wait: None
  18. })
  19. }
  20. pub unsafe fn new_user(data: &[u8], wait_thread: Option<Tid>) -> Box<Thread> {
  21. ...
  22. Box::new(
  23. Thread {
  24. context: Context::new_user_thread(entry_addr, ustack_top, kstack.top(), vm.token()),
  25. kstack: kstack,
  26. proc: Some(
  27. Arc::new(
  28. Process {
  29. vm: Arc::new(vm)
  30. }
  31. ),
  32. ),
  33. wait: wait_thread
  34. }
  35. )
  36. ...
  37. }
  38. }

现在我们在内核中实现该系统调用:

  1. // src/syscall.rs
  2. pub const SYS_EXEC: usize = 221;
  3. pub fn syscall(id: usize, args: [usize; 3], tf: &mut TrapFrame) -> isize {
  4. match id {
  5. ...
  6. SYS_EXEC => {
  7. sys_exec(args[0] as *const u8)
  8. },
  9. ...
  10. }
  11. }
  12. pub unsafe fn from_cstr(s: *const u8) -> &'static str {
  13. use core::{ slice, str };
  14. // 使用迭代器获得字符串长度
  15. let len = (0usize..).find(|&i| *s.add(i) == 0).unwrap();
  16. str::from_utf8(slice::from_raw_parts(s, len)).unwrap()
  17. }
  18. fn sys_exec(path: *const u8) -> isize {
  19. let valid = process::execute(unsafe { from_cstr(path) }, Some(process::current_tid()));
  20. // 如果正常执行,则阻塞终端线程,等到启动的这个用户线程运行结束
  21. if valid { process::yield_now(); }
  22. // 不能正常执行,直接返回;或者被启动线程结束后唤醒终端线程之后返回
  23. return 0;
  24. }
  25. // src/process/mod.rs
  26. // 返回值表示是否正常执行
  27. pub fn execute(path: &str, host_tid: Option<Tid>) -> bool {
  28. let find_result = ROOT_INODE.lookup(path);
  29. match find_result {
  30. Ok(inode) => {
  31. let data = inode.read_as_vec().unwrap();
  32. // 这里创建用户线程时,传入 host_tid
  33. let user_thread = unsafe { Thread::new_user(data.as_slice(), host_tid) };
  34. CPU.add_thread(user_thread);
  35. true
  36. },
  37. Err(_) => {
  38. // 如果找不到路径字符串对应的用户程序
  39. println!("command not found!");
  40. false
  41. }
  42. }
  43. }

这样我们在线程初始化中直接调用这个封装好的函数就好了。

  1. // src/process/mod.rs
  2. pub fn init() {
  3. ...
  4. execute("rust/user_shell", None);
  5. ...
  6. }

这里虽然还是将 rust/user_shell 硬编码到内核中,但是好歹它可以交互式运行其他程序了!

试一试运行 rust/hello_world ,它工作的很好;rust/notebook 也不赖,但是我们没有实现 Ctrl+c 的功能,因此就无法从记事本中退出了。随便输入一个不存在的程序,终端也不会崩溃,而是会提示程序不存在!

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