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 的流程:
由用户程序通过 ecall 产生一个 sys_call ,通过传递的参数判断产生了一个 sys_read 请求。
判断缓冲区中是否有字符。
如果有,则返回该字符。
如果没有,则休眠该程序,等待产生键盘中断。
可以预见我们的 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