11.2. 命令行——输入(信号量)

本章代码对应 commit :1e329bb3c5ad4d58a74837697d5e08ac4904d0bd

用户 shell

// in usr/rust/src/bin/shell.rs

#![no_std]
#![no_main]

#[macro_use]
extern crate rust;

use rust::io::getc;

const LF: u8 = 0x0au8;
const CR: u8 = 0x0du8;

// IMPORTANT: Must define main() like this
#[no_mangle]
pub fn main() -> i32 {
    println!("Rust user shell");
    loop {
        let c = getc();
        match c {
            LF | CR => {
                print!("{}", LF as char);
                print!("{}", CR as char)
            }
            _ => print!("{}", c as char)
        }
    }
}

目前用户程序只做一件事:循环判断是否有字符输入,如果是回车符号或者换行符号,换行;如果是普通字符就直接打印。

// in usr/rust/src/io.rs

pub const STDIN: usize = 0;

pub fn getc() -> u8 {
    let mut c = 0u8;
    loop {
        let len = syscall::sys_read(STDIN, &mut c, 1);
        match len {
            1 => return c,
            0 => continue,
            _ => panic!("read stdin len = {}", len),
        }
    }
}

// in usr/rust/src/syscall.rs

pub fn sys_read(fd : usize, base : *const u8, len : usize) -> i32 {
    sys_call(SyscallId::Read, fd, base as usize , len , 0)
}

enum SyscallId {
    Read = 63,
    Write = 64,
    Exit = 93,
}

getc 只需简单的向 os 发起一个 syscall 即可。执行 make 编译用户程序,剩下的就是 os 的工作了。

处理 sys_read

首先,我们梳理一下处理 sys_read 的流程:

  1. 由用户程序通过 ecall 产生一个 sys_call ,通过传递的参数判断产生了一个 sys_read 请求。

  2. 判断缓冲区中是否有字符。

    • 如果有,则返回该字符。

    • 如果没有,则休眠该程序,等待产生键盘中断。

可以预见我们的 syscall 将越来越多,将 syscall 的实现全堆在一个函数里实在是太不优雅了,所以我们需要简化 interrupt::syscall ,并创建 syscall.rs

// in interrupt.rs

fn syscall(tf: &mut TrapFrame) {
    let ret = crate::syscall::syscall(
        tf.x[17],
        [tf.x[10], tf.x[11], tf.x[12]],
        tf,
    );
    tf.sepc += 4;
    tf.x[10] = ret as usize;
}

// in syscall.rs

use crate::context::TrapFrame;
use crate::process;

pub const SYS_READ: usize = 63;
pub const SYS_WRITE: usize = 64;
pub const SYS_EXIT: usize = 93;

pub fn syscall(id: usize, args: [usize;3], tf: &mut TrapFrame) -> isize {
    match id {
        SYS_READ => {
            return sys_read(args[0], args[1] as *mut u8, args[2]);
        },
        SYS_WRITE => {
            print!("{}", args[0] as u8 as char);
            return 0;
        },
        SYS_EXIT => {
            sys_exit(args[0]);
        },
        _ => {
            panic!("unknown syscall id {}", id);
        },
    };
    return 0;
}

fn sys_exit(code: usize) {
    process::exit(code);
}

// in lib.rs

mod syscall;

// fs/mod.rs

pub mod stdio;

每一步看起来都很简单,接下来要做的是通过 crate::fs::stdio::STDIN 中读取缓冲区字符。

缓冲区

所谓缓冲区其实就是一个数组,我们通过识别键盘中断,每产生一次键盘中断,就将按下的字符压入缓冲区中:

// in stdio.rs

use alloc::{ collections::VecDeque, sync::Arc };
use spin::Mutex;
use crate::process;
use crate::sync::condvar::*;

pub struct Stdin {
    buf: Mutex<VecDeque<char>>,
    pushed: Condvar,
}

impl Stdin {
    pub fn new() -> Self {
        Stdin {
            buf: Mutex::new(VecDeque::new()),
            pushed: Condvar::new(),
        }
    }

    pub fn push(&self, ch: char) {
        let mut lock = self.buf.lock();
        lock.push_back(ch);
        drop(lock);
        self.pushed.notify();
    }

    pub fn pop(&self) -> char {
        loop{
            let ret = self.buf.lock().pop_front();
            match ret {
                Some(ch) => {
                    return ch;
                },
                None => {
                    self.pushed.wait();
                },
            }
        }
    }
}

use lazy_static::*;
lazy_static!{
    pub static ref STDIN: Arc<Stdin> = Arc::new(Stdin::new());
}

对于一次 pop 操作,如果缓冲区中存在字符,则将其返回;如果不存在,则将该线程睡眠(不再加入调度)。

如果产生键盘中断(有字符输入),则唤醒休眠队列中的第一个线程,这里我们通过信号量对这些事务进行管理。为了更清晰的理解这里的 push 和 pop 操作,我们先看一下在 interrupt.rs 中是如何调用他们的:

// in interrupt.rs

use riscv::register::{stvec, sscratch, sie, sstatus};
#[no_mangle]
pub fn init() {
    extern {
        fn __alltraps();
    }
    unsafe {
        sscratch::write(0); // 给中断 asm 初始化
        sstatus::set_sie();
        stvec::write(__alltraps as usize, stvec::TrapMode::Direct);
        sie::set_sext(); // 开外部中断(串口)
    }
}

#[no_mangle]
pub extern "C" fn rust_trap(tf: &mut TrapFrame) {
    match tf.scause.cause() {
        ...
        Trap::Interrupt(Interrupt::SupervisorExternal) => {
            let ch = bbl::sbi::console_getchar() as u8 as char;
            external(ch as u8);
        },
        _ => panic!("unexpected trap"),
    }
}

fn external(ch: u8) {
    crate::fs::stdio::STDIN.push(ch as char);
}

use crate::process::tick;
fn super_timer() {
    clock_set_next_event();
    unsafe{
        TICK = TICK + 1;
        // if TICK % 100 == 0 {
        //     println!("100 ticks!");
        // }
    }
    tick();
}

// in syscall.rs

fn sys_read(fd: usize, base: *mut u8, len: usize) -> isize {
    unsafe { *base = crate::fs::stdio::STDIN.pop() as u8; }
    return 1;
}

简单的信号量

// in sync/mod.rs

pub mod condvar;

// in lib.rs

mod sync;

// in sync/condvar.rs

use spin::Mutex;
use alloc::{ collections::VecDeque, };
use crate::process::{ Tid, current_tid, yield_now, wake_up };

#[derive(Default)]
pub struct Condvar {
    wait_queue: Mutex<VecDeque<Tid>>,
}

impl Condvar {
    pub fn new() -> Self {
        Condvar::default()
    }

    pub fn wait(&self) {
        let mut queue = self.wait_queue.lock();
        queue.push_back(current_tid());
        drop(queue);
        yield_now();
    }

    pub fn notify(&self) {
        let mut queue = self.wait_queue.lock();
        if let Some(tid) = queue.pop_front() {
            wake_up(tid);
            drop(queue);
            yield_now();
        }
    }
}

这里的 yield_now 会将当前运行的线程状态置为 Status::Sleeping ,在 ThreadPool.retrieve 中会对线程的状态进行判断,如果是 Status::Ready 或者 Status::Running ,则会将其放入调度队列中,等待下一次调度。否则该线程将不再被调度(除非被 Condvar 重新加入)。

// in process/mod.rs

pub fn yield_now() {
    CPU.yield_now();
}

pub fn wake_up(tid : Tid) {
    CPU.wake_up(tid);
}

pub fn current_tid() -> usize {
    CPU.current_tid()
}

// in process/processor.rs

impl Processor {
    pub fn yield_now(&self) {
        let inner = self.inner();
        if !inner.current.is_none() {
            unsafe {
                let flags = disable_and_store(); // 禁止中断,获取当前 sstatus 的状态并保存。
                let tid = inner.current.as_mut().unwrap().0;
                let thread_info = inner.pool.threads[tid].as_mut().expect("thread not exits");
                if thread_info.present {
                    thread_info.status = Status::Sleeping;
                } else {
                    panic!("try to sleep an null thread !");
                }
                inner
                    .current
                    .as_mut()
                    .unwrap()
                    .1
                    .switch_to(&mut *inner.idle);   // 转到 idle 线程重新调度
                restore(flags);  // 使能中断,恢复 sstatus 的状态
            }
        }
    }

    pub fn wake_up(&self, tid: Tid) {
        let inner = self.inner();
        inner.pool.wakeup(tid);
    }

    pub fn current_tid(&self) -> usize {
        self.inner().current.as_mut().unwrap().0 as usize
    }

    pub fn run(&self) -> !{
        ...
        // println!("thread {} ran just now", tid);
        ...
    }
}

// in process/thread_pool.rs

pub struct ThreadInfo {
   pub status: Status,
   pub present: bool,
   thread: Option<Box<Thread>>,
}

pub struct ThreadPool {
    pub threads: Vec<Option<ThreadInfo>>, // 线程信号量的向量
    scheduler: Box<Scheduler>, // 调度算法
}

impl ThreadPool {
    ...
    pub fn retrieve(&mut self, tid: Tid, thread: Box<Thread> ) {
        let mut thread_info = self.threads[tid].as_mut().expect("thread not exits !");
        if thread_info.present {
            thread_info.thread = Some(thread);
            match thread_info.status {
                Status::Ready | Status::Running(_) => {
                    self.scheduler.push(tid);
                },
                _ => {
                    // println!("do nothing!");
                },
            }
        }
    }

    pub fn wakeup(&mut self, tid: Tid) {
        let proc = self.threads[tid].as_mut().expect("thread not exist");
        if proc.present {
            proc.status = Status::Ready;
            self.scheduler.push(tid);
        } else {
            panic!("try to sleep an null thread !");
        }
    }
}

执行 make run ,现在我们的命令行已经可以输入文本了,下一个目标是动态执行用户程序。

Last updated