add os[1-8]-ref for os refereces, add guide, add README

This commit is contained in:
Yu Chen
2022-06-27 22:22:44 +08:00
parent 7c1679774c
commit d752a67137
360 changed files with 32863 additions and 1 deletions

View File

@@ -0,0 +1,118 @@
引言
=========================================
本章导读
-----------------------------------------
本章将基于文件描述符实现父子进程之间的通信机制——管道。
我们还将扩展 ``exec`` 系统调用,使之能传递运行参数,并进一步改进 shell 程序,使其支持重定向符号 ``>````<``
实践体验
-----------------------------------------
获取本章代码:
.. code-block:: console
$ git clone https://github.com/LearningOS/rCore-Tutorial-Code-2022S.git
$ cd rCore-Tutorial-Code-2022S
$ git checkout ch7
在 qemu 模拟器上运行本章代码:
.. code-block:: console
$ cd os
$ make run
进入shell程序后可以运行管道机制的简单测例 ``ch7b_pipetest`` ``ch7b_pipetest`` 需要保证父进程通过管道传输给子进程的字符串不会发生变化。
测例输出大致如下:
.. code-block::
>> ch7b_pipetest
Read OK, child process exited!
pipetest passed!
Shell: Process 2 exited with code 0
>>
同样的,也可以运行较为复杂的测例 ``ch7b_pipe_large_test``,体验通过两个管道实现双向通信。
此外在本章我们为shell程序支持了输入/输出重定向功能,可以将一个应用的输出保存到一个指定的文件。例如,下面的命令可以将 ``ch7b_yield`` 应用的输出保存在文件 ``fileb`` 当中,并在应用执行完毕之后确认它的输出:
.. code-block::
>> ch7b_yield > fileb
Shell: Process 2 exited with code 0
>> ch7b_cat fileb
Hello, I am process 2.
Back in process 2, iteration 0.
Back in process 2, iteration 1.
Back in process 2, iteration 2.
Back in process 2, iteration 3.
Back in process 2, iteration 4.
yield pass.
Shell: Process 2 exited with code 0
>>
本章代码树
-----------------------------------------
.. code-block::
── os
   └── src
   ├── ...
   ├── fs
   │   ├── inode.rs
   │   ├── mod.rs
   │   ├── pipe.rs(新增:实现了 File Trait 的第三个实现——可用来进程间通信的管道)
   │   └── stdio.rs
   ├── mm
   │   ├── address.rs
   │   ├── frame_allocator.rs
   │   ├── heap_allocator.rs
   │   ├── memory_set.rs
   │   ├── mod.rs
   │   └── page_table.rs
   ├── syscall
   │   ├── fs.rs(修改添加了sys_pipe和sys_dup)
   │   ├── mod.rs
   │   └── process.rs(修改sys_exec添加了对参数的支持)
   ├── task
      ├── context.rs
      ├── manager.rs
      ├── mod.rs
      ├── pid.rs
      ├── processor.rs
      ├── switch.rs
      ├── switch.S
      └── task.rs(修改在exec中将参数压入用户栈中)
cloc easy-fs os
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
Rust 42 317 434 3574
Assembly 4 53 26 526
make 1 13 4 48
TOML 2 4 2 23
-------------------------------------------------------------------------------
SUM: 49 387 466 4171
-------------------------------------------------------------------------------
.. 本章代码导读
.. -----------------------------------------------------
.. 在本章第一节 :doc:`/chapter6/1file-descriptor` 中,我们引入了文件的概念,用它来代表进程可以读写的多种被内核管理的硬件/软件资源。进程必须通过系统调用打开一个文件,将文件加入到自身的文件描述符表中,才能通过文件描述符(也就是某个特定文件在自身文件描述符表中的下标)来读写该文件。
.. 文件的抽象 Trait ``File`` 声明在 ``os/src/fs/mod.rs`` 中,它提供了 ``read/write`` 两个接口,可以将数据写入应用缓冲区抽象 ``UserBuffer`` ,或者从应用缓冲区读取数据。应用缓冲区抽象类型 ``UserBuffer`` 来自 ``os/src/mm/page_table.rs`` 中,它将 ``translated_byte_buffer`` 得到的 ``Vec<&'static mut [u8]>`` 进一步包装,不仅保留了原有的分段读写能力,还可以将其转化为一个迭代器逐字节进行读写,这在读写一些流式设备的时候特别有用。
.. 在进程控制块 ``TaskControlBlock`` 中需要加入文件描述符表字段 ``fd_table`` ,可以看到它是一个向量,里面保存了若干实现了 ``File`` Trait 的文件,由于采用动态分发,文件的类型可能各不相同。 ``os/src/syscall/fs.rs`` 的 ``sys_read/write`` 两个读写文件的系统调用需要访问当前进程的文件描述符表,用应用传入内核的文件描述符来索引对应的已打开文件,并调用 ``File`` Trait 的 ``read/write`` 接口; ``sys_close`` 这可以关闭一个文件。调用 ``TaskControlBlock`` 的 ``alloc_fd`` 方法可以在文件描述符表中分配一个文件描述符。进程控制块的其他操作也需要考虑到新增的文件描述符表字段的影响,如 ``TaskControlBlock::new`` 的时候需要对 ``fd_table`` 进行初始化, ``TaskControlBlock::fork`` 中则需要将父进程的 ``fd_table`` 复制一份给子进程。
.. 到本章为止我们支持两种文件:标准输入输出和管道。不同于前面章节,我们将标准输入输出分别抽象成 ``Stdin`` 和 ``Stdout`` 两个类型,并为他们实现 ``File`` Trait 。在 ``TaskControlBlock::new`` 创建初始进程的时候,就默认打开了标准输入输出,并分别绑定到文件描述符 0 和 1 上面。
.. 管道 ``Pipe`` 是另一种文件,它可以用于父子进程间的单向进程间通信。我们也需要为它实现 ``File`` Trait 。 ``os/src/syscall/fs.rs`` 中的系统调用 ``sys_pipe`` 可以用来打开一个管道并返回读端/写端两个文件的文件描述符。管道的具体实现在 ``os/src/fs/pipe.rs`` 中,本章第二节 :doc:`/chapter6/2pipe` 中给出了详细的讲解。管道机制的测试用例可以参考 ``user/src/bin`` 目录下的 ``pipetest.rs`` 和 ``pipe_large_test.rs`` 两个文件。

View File

@@ -0,0 +1,364 @@
管道
============================================
管道的系统调用原型及使用方法
--------------------------------------------
新增为当前进程打开一个管道(包含一个只读文件,一个只写文件)的系统调用:
.. code-block:: rust
/// 功能:为当前进程打开一个管道。
/// 参数pipe 表示应用地址空间中的一个长度为 2 的 usize 数组的起始地址,内核需要按顺序将管道读端
/// 和写端的文件描述符写入到数组中。
/// 返回值:如果出现了错误则返回 -1否则返回 0 。可能的错误原因是:传入的地址不合法。
/// syscall ID59
pub fn sys_pipe(pipe: *mut usize) -> isize;
用户库会将其包装为 ``pipe`` 函数:
.. code-block:: rust
// user/src/lib.rs
pub fn pipe(pipe_fd: &mut [usize]) -> isize { sys_pipe(pipe_fd) }
只有当一个管道的所有读端文件/写端文件都被关闭之后,管道占用的资源才会被回收。
.. code-block:: rust
/// 功能:当前进程关闭一个文件。
/// 参数fd 表示要关闭的文件的文件描述符。
/// 返回值:如果成功关闭则返回 0 ,否则返回 -1 。可能的出错原因:传入的文件描述符并不对应一个打开的文件。
/// syscall ID57
pub fn sys_close(fd: usize) -> isize;
它会在用户库中被包装为 ``close`` 函数。
我们从测例 ``ch7b_pipetest`` 中理解管道的使用方法:
.. code-block:: rust
:linenos:
// user/src/bin/ch7b_pipetest.rs
#![no_std]
#![no_main]
#[macro_use]
extern crate user_lib;
use user_lib::{fork, close, pipe, read, write, wait};
static STR: &str = "Hello, world!";
#[no_mangle]
pub fn main() -> i32 {
// create pipe
let mut pipe_fd = [0usize; 2];
pipe(&mut pipe_fd);
// read end
assert_eq!(pipe_fd[0], 3);
// write end
assert_eq!(pipe_fd[1], 4);
if fork() == 0 {
// child process, read from parent
// close write_end
close(pipe_fd[1]);
let mut buffer = [0u8; 32];
let len_read = read(pipe_fd[0], &mut buffer) as usize;
// close read_end
close(pipe_fd[0]);
assert_eq!(core::str::from_utf8(&buffer[..len_read]).unwrap(), STR);
println!("Read OK, child process exited!");
0
} else {
// parent process, write to child
// close read end
close(pipe_fd[0]);
assert_eq!(write(pipe_fd[1], STR.as_bytes()), STR.len() as isize);
// close write end
close(pipe_fd[1]);
let mut child_exit_code: i32 = 0;
wait(&mut child_exit_code);
assert_eq!(child_exit_code, 0);
println!("pipetest passed!");
0
}
}
在父进程中,我们通过 ``pipe`` 打开一个管道文件数组,其中 ``pipe_fd[0]`` 保存了管道读端的文件描述符,而 ``pipe_fd[1]`` 保存了管道写端的文件描述符。在 ``fork`` 之后,子进程会完全继承父进程的文件描述符表,于是子进程也可以通过同样的文件描述符来访问同一个管道的读端和写端。之前提到过管道是单向的,在这个测例中我们希望管道中的数据从父进程流向子进程,也即父进程仅通过管道的写端写入数据,而子进程仅通过管道的读端读取数据。
因此,在第 25 和第 34 行,分别第一时间在子进程中关闭管道的写端和在父进程中关闭管道的读端。父进程在第 35 行将字符串 ``STR`` 写入管道的写端,随后在第 37 行关闭管道的写端;子进程在第 27 行从管道的读端读取字符串,并在第 29 行关闭。
如果想在父子进程之间实现双向通信,我们就必须创建两个管道。有兴趣的读者可以参考测例 ``ch7b_pipe_large_test``
通过 sys_close 关闭文件
--------------------------------------------
关闭文件的系统调用 ``sys_close`` 实现非常简单,我们只需将进程控制块中的文件描述符表对应的一项改为 ``None`` 代表它已经空闲即可,同时这也会导致内层的引用计数类型 ``Arc`` 被销毁,会减少一个文件的引用计数,当引用计数减少到 0 之后,文件所占用的资源就会被自动回收。
.. code-block:: rust
// os/src/syscall/fs.rs
pub fn sys_close(fd: usize) -> isize {
let task = current_task().unwrap();
let mut inner = task.acquire_inner_lock();
if fd >= inner.fd_table.len() {
return -1;
}
if inner.fd_table[fd].is_none() {
return -1;
}
inner.fd_table[fd].take();
0
}
基于文件的管道
--------------------------------------------
我们将管道的一端(读端或写端)抽象为 ``Pipe`` 类型:
.. code-block:: rust
// os/src/fs/pipe.rs
pub struct Pipe {
readable: bool,
writable: bool,
buffer: Arc<Mutex<PipeRingBuffer>>,
}
``readable````writable`` 分别指出该管道端可否支持读取/写入,通过 ``buffer`` 字段还可以找到该管道端所在的管道自身。后续我们将为它实现 ``File`` Trait ,之后它便可以通过文件描述符来访问。
而管道自身,也就是那个带有一定大小缓冲区的字节队列,我们抽象为 ``PipeRingBuffer`` 类型:
.. code-block:: rust
// os/src/fs/pipe.rs
const RING_BUFFER_SIZE: usize = 32;
#[derive(Copy, Clone, PartialEq)]
enum RingBufferStatus {
FULL,
EMPTY,
NORMAL,
}
pub struct PipeRingBuffer {
arr: [u8; RING_BUFFER_SIZE],
head: usize,
tail: usize,
status: RingBufferStatus,
write_end: Option<Weak<Pipe>>,
}
- ``RingBufferStatus`` 记录了缓冲区目前的状态:``FULL`` 表示缓冲区已满不能再继续写入; ``EMPTY`` 表示缓冲区为空无法从里面读取;而 ``NORMAL`` 则表示除了 ``FULL````EMPTY`` 之外的其他状态。
- ``PipeRingBuffer````arr/head/tail`` 三个字段用来维护一个循环队列,其中 ``arr`` 为存放数据的数组, ``head`` 为循环队列队头的下标, ``tail`` 为循环队列队尾的下标。
- ``PipeRingBuffer````write_end`` 字段还保存了它的写端的一个弱引用计数,这是由于在某些情况下需要确认该管道所有的写端是否都已经被关闭了,通过这个字段很容易确认这一点。
从内存管理的角度,每个读端或写端中都保存着所属管道自身的强引用计数,且我们确保这些引用计数只会出现在管道端口 ``Pipe`` 结构体中。于是,一旦一个管道所有的读端和写端均被关闭,便会导致它们所属管道的引用计数变为 0 ,循环队列缓冲区所占用的资源被自动回收。虽然 ``PipeRingBuffer`` 中保存了一个指向写端的引用计数,但是它是一个弱引用,也就不会出现循环引用的情况导致内存泄露。
.. chyyuu 介绍弱引用???
管道创建
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
通过 ``PipeRingBuffer::new`` 可以创建一个新的管道:
.. code-block:: rust
// os/src/fs/pipe.rs
impl PipeRingBuffer {
pub fn new() -> Self {
Self {
arr: [0; RING_BUFFER_SIZE],
head: 0,
tail: 0,
status: RingBufferStatus::EMPTY,
write_end: None,
}
}
}
``Pipe````read/write_end_with_buffer`` 方法可以分别从一个已有的管道创建它的读端和写端:
.. code-block:: rust
// os/src/fs/pipe.rs
impl Pipe {
pub fn read_end_with_buffer(buffer: Arc<Mutex<PipeRingBuffer>>) -> Self {
Self {
readable: true,
writable: false,
buffer,
}
}
pub fn write_end_with_buffer(buffer: Arc<Mutex<PipeRingBuffer>>) -> Self {
Self {
readable: false,
writable: true,
buffer,
}
}
}
可以看到,读端和写端的访问权限进行了相应设置:不允许向读端写入,也不允许从写端读取。
通过 ``make_pipe`` 方法可以创建一个管道并返回它的读端和写端:
.. code-block:: rust
// os/src/fs/pipe.rs
impl PipeRingBuffer {
pub fn set_write_end(&mut self, write_end: &Arc<Pipe>) {
self.write_end = Some(Arc::downgrade(write_end));
}
}
/// Return (read_end, write_end)
pub fn make_pipe() -> (Arc<Pipe>, Arc<Pipe>) {
let buffer = Arc::new(Mutex::new(PipeRingBuffer::new()));
let read_end = Arc::new(
Pipe::read_end_with_buffer(buffer.clone())
);
let write_end = Arc::new(
Pipe::write_end_with_buffer(buffer.clone())
);
buffer.lock().set_write_end(&write_end);
(read_end, write_end)
}
注意,我们调用 ``PipeRingBuffer::set_write_end`` 在管道中保留它的写端的弱引用计数。
现在来实现创建管道的系统调用 ``sys_pipe``
.. code-block:: rust
:linenos:
// os/src/task/task.rs
impl TaskControlBlockInner {
pub fn alloc_fd(&mut self) -> usize {
if let Some(fd) = (0..self.fd_table.len())
.find(|fd| self.fd_table[*fd].is_none()) {
fd
} else {
self.fd_table.push(None);
self.fd_table.len() - 1
}
}
}
// os/src/syscall/fs.rs
pub fn sys_pipe(pipe: *mut usize) -> isize {
let task = current_task().unwrap();
let token = current_user_token();
let mut inner = task.acquire_inner_lock();
let (pipe_read, pipe_write) = make_pipe();
let read_fd = inner.alloc_fd();
inner.fd_table[read_fd] = Some(pipe_read);
let write_fd = inner.alloc_fd();
inner.fd_table[write_fd] = Some(pipe_write);
*translated_refmut(token, pipe) = read_fd;
*translated_refmut(token, unsafe { pipe.add(1) }) = write_fd;
0
}
``TaskControlBlockInner::alloc_fd`` 可以在进程控制块中分配一个最小的空闲文件描述符来访问一个新打开的文件。它先从小到大遍历所有曾经被分配过的文件描述符尝试找到一个空闲的,如果没有的话就需要拓展文件描述符表的长度并新分配一个。
``sys_pipe`` 中,第 21 行我们调用 ``make_pipe`` 创建一个管道并获取其读端和写端,第 22~25 行我们分别为读端和写端分配文件描述符并将它们放置在文件描述符表中的相应位置中。第 26~27 行我们则是将读端和写端的文件描述符写回到应用地址空间。
管道读写
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
首先来看如何为 ``Pipe`` 实现 ``File`` Trait 的 ``read`` 方法,即从管道的读端读取数据。在此之前,我们需要对于管道循环队列进行封装来让它更易于使用:
.. code-block:: rust
:linenos:
// os/src/fs/pipe.rs
impl PipeRingBuffer {
pub fn read_byte(&mut self) -> u8 {
self.status = RingBufferStatus::NORMAL;
let c = self.arr[self.head];
self.head = (self.head + 1) % RING_BUFFER_SIZE;
if self.head == self.tail {
self.status = RingBufferStatus::EMPTY;
}
c
}
pub fn available_read(&self) -> usize {
if self.status == RingBufferStatus::EMPTY {
0
} else {
if self.tail > self.head {
self.tail - self.head
} else {
self.tail + RING_BUFFER_SIZE - self.head
}
}
}
pub fn all_write_ends_closed(&self) -> bool {
self.write_end.as_ref().unwrap().upgrade().is_none()
}
}
``PipeRingBuffer::read_byte`` 方法可以从管道中读取一个字节,注意在调用它之前需要确保管道缓冲区中不是空的。它会更新循环队列队头的位置,并比较队头和队尾是否相同,如果相同的话则说明管道的状态变为空 ``EMPTY`` 。仅仅通过比较队头和队尾是否相同不能确定循环队列是否为空,因为它既有可能表示队列为空,也有可能表示队列已满。因此我们需要在 ``read_byte`` 的同时进行状态更新。
``PipeRingBuffer::available_read`` 可以计算管道中还有多少个字符可以读取。我们首先需要需要判断队列是否为空,因为队头和队尾相等可能表示队列为空或队列已满,两种情况 ``available_read`` 的返回值截然不同。如果队列为空的话直接返回 0否则根据队头和队尾的相对位置进行计算。
``PipeRingBuffer::all_write_ends_closed`` 可以判断管道的所有写端是否都被关闭了,这是通过尝试将管道中保存的写端的弱引用计数升级为强引用计数来实现的。如果升级失败的话,说明管道写端的强引用计数为 0 ,也就意味着管道所有写端都被关闭了,从而管道中的数据不会再得到补充,待管道中仅剩的数据被读取完毕之后,管道就可以被销毁了。
下面是 ``Pipe````read`` 方法的实现:
.. code-block:: rust
:linenos:
// os/src/fs/pipe.rs
impl File for Pipe {
fn read(&self, buf: UserBuffer) -> usize {
assert_eq!(self.readable, true);
let mut buf_iter = buf.into_iter();
let mut read_size = 0usize;
loop {
let mut ring_buffer = self.buffer.lock();
let loop_read = ring_buffer.available_read();
if loop_read == 0 {
if ring_buffer.all_write_ends_closed() {
return read_size;
}
drop(ring_buffer);
suspend_current_and_run_next();
continue;
}
// read at most loop_read bytes
for _ in 0..loop_read {
if let Some(byte_ref) = buf_iter.next() {
unsafe { *byte_ref = ring_buffer.read_byte(); }
read_size += 1;
} else {
return read_size;
}
}
}
}
}
- 第 6 行的 ``buf_iter`` 将传入的应用缓冲区 ``buf`` 转化为一个能够逐字节对于缓冲区进行访问的迭代器,每次调用 ``buf_iter.next()`` 即可按顺序取出用于访问缓冲区中一个字节的裸指针。
- 第 7 行的 ``read_size`` 用来维护实际有多少字节从管道读入应用的缓冲区。
- ``File::read`` 的语义是要从文件中最多读取应用缓冲区大小那么多字符。这可能超出了循环队列的大小,或者由于尚未有进程从管道的写端写入足够的字符,因此我们需要将整个读取的过程放在一个循环中,当循环队列中不存在足够字符的时候暂时进行任务切换,等待循环队列中的字符得到补充之后再继续读取。
这个循环从第 8 行开始,第 10 行我们用 ``loop_read`` 来保存循环这一轮次中可以从管道循环队列中读取多少字符。如果管道为空则会检查管道的所有写端是否都已经被关闭,如果是的话,说明我们已经没有任何字符可以读取了,这时可以直接返回;否则我们需要等管道的字符得到填充之后再继续读取,因此我们调用 ``suspend_current_and_run_next`` 切换到其他任务,等到切换回来之后回到循环开头再看一下管道中是否有字符了。在调用之前我们需要手动释放管道自身的锁,因为切换任务时候的 ``__switch`` 并不是一个正常的函数调用。
如果 ``loop_read`` 不为 0 ,在这一轮次中管道中就有 ``loop_read`` 个字节可以读取。我们可以迭代应用缓冲区中的每个字节指针并调用 ``PipeRingBuffer::read_byte`` 方法来从管道中进行读取。如果这 ``loop_read`` 个字节均被读取之后还没有填满应用缓冲区就需要进入循环的下一个轮次,否则就可以直接返回了。
``Pipe````write`` 方法——即通过管道的写端向管道中写入数据的实现和 ``read`` 的原理类似,篇幅所限在这里不再赘述,感兴趣的读者可自行查阅。

View File

@@ -0,0 +1,337 @@
命令行参数与标准 I/O 重定向
=================================================
命令行参数
-------------------------------------------------
使用 C 语言开发 Linux 应用时,可以使用标准库提供的 ``argc/argv`` 来获取命令行参数我们希望在我们自己的内核和shell程序上支持这个功能。为了支持命令行参数 ``sys_exec`` 的系统调用接口需要发生变化:
.. code-block:: rust
// user/src/syscall.rs
pub fn sys_exec(path: &str, args: &[*const u8]) -> isize;
可以看到,它的参数多出了一个 ``args`` 数组,数组中的每个元素都是命令行参数字符串的起始地址。实际传递给内核的实际上是这个数组的起始地址:
.. code-block:: rust
// user/src/syscall.rs
pub fn sys_exec(path: &str, args: &[*const u8]) -> isize {
syscall(SYSCALL_EXEC, [path.as_ptr() as usize, args.as_ptr() as usize, 0])
}
// user/src/lib.rs
pub fn exec(path: &str, args: &[*const u8]) -> isize { sys_exec(path, args) }
shell程序的命令行参数分割
+++++++++++++++++++++++++++++++++++++++++++++++++
回忆一下在shell程序 ``user_shell`` 中,一旦接收到一个回车,我们就会将当前行的内容 ``line`` 作为一个名字并试图去执行同名的应用。但是现在 ``line`` 还可能包含一些命令行参数,只有最开头的一个才是要执行的应用名。因此我们要做的第一件事情就是将 ``line`` 用空格分割:
.. code-block:: rust
// user/src/bin/ch6b_user_shell.rs
let args: Vec<_> = line.as_str().split(' ').collect();
let mut args_copy: Vec<String> = args
.iter()
.map(|&arg| {
let mut string = String::new();
string.push_str(arg);
string
})
.collect();
args_copy
.iter_mut()
.for_each(|string| {
string.push('\0');
});
经过分割, ``args`` 中的 ``&str`` 都是 ``line`` 中的一段子区间,它们的结尾并没有包含 ``\0`` ,因为 ``line`` 是我们输入得到的,中间本来就没有 ``\0`` 。由于在向内核传入字符串的时候,我们只能传入字符串的起始地址,因此我们必须保证其结尾为 ``\0`` 。从而我们用 ``args_copy````args`` 中的字符串拷贝一份到堆上并在末尾手动加入 ``\0`` 。这样就可以安心的将 ``args_copy`` 中的字符串传入内核了。我们用 ``args_addr`` 来收集这些字符串的起始地址:
.. code-block:: rust
// user/src/bin/ch6b_user_shell.rs
let mut args_addr: Vec<*const u8> = args_copy
.iter()
.map(|arg| arg.as_ptr())
.collect();
args_addr.push(0 as *const u8);
向量 ``args_addr`` 中的每个元素都代表一个命令行参数字符串的起始地址。为了让内核能够获取到命令行参数的个数,我们在 ``args_addr`` 的末尾放入一个 0 ,这样内核看到它时就能知道命令行参数已经获取完毕了。
``fork`` 出来的子进程中,我们调用 ``exec`` 传入命令行参数。
sys_exec 将命令行参数压入用户栈
+++++++++++++++++++++++++++++++++++++++++++++++++
``sys_exec`` 中,首先需要将应用传进来的命令行参数取出来:
.. code-block:: rust
:linenos:
:emphasize-lines: 6-14,19
// os/src/syscall/process.rs
pub fn sys_exec(path: *const u8, mut args: *const usize) -> isize {
let token = current_user_token();
let path = translated_str(token, path);
let mut args_vec: Vec<String> = Vec::new();
loop {
let arg_str_ptr = *translated_ref(token, args);
if arg_str_ptr == 0 {
break;
}
args_vec.push(translated_str(token, arg_str_ptr as *const u8));
unsafe { args = args.add(1); }
}
if let Some(app_inode) = open_file(path.as_str(), OpenFlags::RDONLY) {
let all_data = app_inode.read_all();
let task = current_task().unwrap();
let argc = args_vec.len();
task.exec(all_data.as_slice(), args_vec);
// return argc because cx.x[10] will be covered with it later
argc as isize
} else {
-1
}
}
每次我们都可以从一个起始地址通过 ``translated_str`` 拿到一个字符串,直到 ``args`` 为 0 就说明没有更多命令行参数了。在第 19 行调用 ``TaskControlBlock::exec`` 的时候,我们需要将获取到的 ``args_vec`` 传入进去并将里面的字符串压入到用户栈上。
.. code-block:: rust
:linenos:
:emphasize-lines: 11-34,45,50,51
// os/src/task/task.rs
impl TaskControlBlock {
pub fn exec(&self, elf_data: &[u8], args: Vec<String>) {
// memory_set with elf program headers/trampoline/trap context/user stack
let (memory_set, mut user_sp, entry_point) = MemorySet::from_elf(elf_data);
let trap_cx_ppn = memory_set
.translate(VirtAddr::from(TRAP_CONTEXT).into())
.unwrap()
.ppn();
// push arguments on user stack
user_sp -= (args.len() + 1) * core::mem::size_of::<usize>();
let argv_base = user_sp;
let mut argv: Vec<_> = (0..=args.len())
.map(|arg| {
translated_refmut(
memory_set.token(),
(argv_base + arg * core::mem::size_of::<usize>()) as *mut usize
)
})
.collect();
*argv[args.len()] = 0;
for i in 0..args.len() {
user_sp -= args[i].len() + 1;
*argv[i] = user_sp;
let mut p = user_sp;
for c in args[i].as_bytes() {
*translated_refmut(memory_set.token(), p as *mut u8) = *c;
p += 1;
}
*translated_refmut(memory_set.token(), p as *mut u8) = 0;
}
// make the user_sp aligned to 8B
user_sp -= user_sp % core::mem::size_of::<usize>();
// **** access current TCB exclusively
let mut inner = self.inner_exclusive_access();
// substitute memory_set
inner.memory_set = memory_set;
// update trap_cx ppn
inner.trap_cx_ppn = trap_cx_ppn;
// initialize trap_cx
let mut trap_cx = TrapContext::app_init_context(
entry_point,
user_sp,
KERNEL_SPACE.exclusive_access().token(),
self.kernel_stack.get_top(),
trap_handler as usize,
);
trap_cx.x[10] = args.len();
trap_cx.x[11] = argv_base;
*inner.get_trap_cx() = trap_cx;
// **** release current PCB
}
}
第 11-34 行所做的主要工作是将命令行参数以某种格式压入用户栈。具体的格式可以参考下图(比如应用传入了两个命令行参数 ``aa````bb``
.. image:: user-stack-cmdargs.png
:align: center
- 首先需要在用户栈上分配一个字符串指针数组,也就是蓝色区域。数组中的每个元素都指向一个用户栈更低处的命令行参数字符串的起始地址。在第 12~24 行可以看到,最开始我们只是分配空间,具体的值要等到字符串被放到用户栈上之后才能确定更新。
- 第 23~32 行,我们逐个将传入的 ``args`` 中的字符串压入到用户栈中,对应于图中的橙色区域。为了实现方便,我们在用户栈上预留空间之后逐字节进行复制。注意 ``args`` 中的字符串是通过 ``translated_str`` 从应用地址空间取出的,它的末尾不包含 ``\0`` 。为了应用能知道每个字符串的长度,我们需要手动在末尾加入 ``\0``
- 第 34 行将 ``user_sp`` 以 8 字节对齐,在 Qemu 平台上其实可以忽略这一步。
我们还需要对应修改 Trap 上下文。首先是第 45 行,我们的 ``user_sp`` 相比之前已经发生了变化,它上面已经压入了命令行参数。同时,我们还需要修改 Trap 上下文中的 ``a0/a1`` 寄存器,让 ``a0`` 表示命令行参数的个数,而 ``a1`` 则表示图中 ``argv_base`` 即蓝色区域的起始地址。这两个参数在第一次进入对应应用的用户态的时候会被接收并用于还原命令行参数。
用户库从用户栈上还原命令行参数
+++++++++++++++++++++++++++++++++++++++++++++++++
在应用第一次进入用户态的时候,我们放在 Trap 上下文 a0/a1 两个寄存器中的内容可以被用户库中的入口函数以参数的形式接收:
.. code-block:: rust
:linenos:
:emphasize-lines: 10-24
// user/src/lib.rs
#[no_mangle]
#[link_section = ".text.entry"]
pub extern "C" fn _start(argc: usize, argv: usize) -> ! {
unsafe { // 初始化堆分配器
HEAP.lock()
.init(HEAP_SPACE.as_ptr() as usize, USER_HEAP_SIZE);
}
let mut v: Vec<&'static str> = Vec::new();
for i in 0..argc {
let str_start = unsafe {
((argv + i * core::mem::size_of::<usize>()) as *const usize).read_volatile()
};
let len = (0usize..).find(|i| unsafe {
((str_start + *i) as *const u8).read_volatile() == 0
}).unwrap();
v.push(
core::str::from_utf8(unsafe {
core::slice::from_raw_parts(str_start as *const u8, len)
}).unwrap()
);
}
exit(main(argc, v.as_slice()));
}
可以看到,在入口 ``_start`` 中我们就接收到了命令行参数个数 ``argc`` 和字符串数组的起始地址 ``argv`` 。但是这个起始地址不太好用,我们希望能够将其转化为编写应用的时候看到的 ``&[&str]`` 的形式。转化的主体在第 10~23 行,就是分别取出 ``argc`` 个字符串的起始地址(基于字符串数组的 base 地址 ``argv`` ),从它向后找到第一个 ``\0`` 就可以得到一个完整的 ``&str`` 格式的命令行参数字符串并加入到向量 ``v`` 中。最后通过 ``v.as_slice`` 就得到了我们在 ``main`` 主函数中看到的 ``&[&str]``
有了命令行参数支持,我们就可以编写命令行工具 ``ch6b_cat`` 来输出指定文件的内容了。读者可以自行参阅其实现。
标准输入输出重定向
-------------------------------------------------
为了增强 shell 程序使用文件系统时的灵活性,我们需要新增标准输入输出重定向功能。
重定向功能对于应用来说是透明的。在应用中除非明确指出了数据要从指定的文件输入或者输出到指定的文件,否则数据默认都是输入自进程文件描述表位置 0 处的标准输入,并输出到进程文件描述符表位置 1 处的标准输出。
为了对应用进程的文件描述符表进行某种替换,引入一个新的系统调用 ``sys_dup``
.. code-block:: rust
// user/src/syscall.rs
/// 功能:将进程中一个已经打开的文件复制一份并分配到一个新的文件描述符中。
/// 参数fd 表示进程中一个已经打开的文件的文件描述符。
/// 返回值:如果出现了错误则返回 -1否则能够访问已打开文件的新文件描述符。
/// 可能的错误原因是:传入的 fd 并不对应一个合法的已打开文件。
/// syscall ID24
pub fn sys_dup(fd: usize) -> isize;
这个系统调用的实现非常简单:
.. code-block:: rust
// os/src/syscall/fs.rs
pub fn sys_dup(fd: usize) -> isize {
let task = current_task().unwrap();
let mut inner = task.acquire_inner_lock();
if fd >= inner.fd_table.len() {
return -1;
}
if inner.fd_table[fd].is_none() {
return -1;
}
let new_fd = inner.alloc_fd();
inner.fd_table[new_fd] = Some(Arc::clone(inner.fd_table[fd].as_ref().unwrap()));
new_fd as isize
}
``sys_dup`` 函数中,首先检查传入 ``fd`` 的合法性。然后在文件描述符表中分配一个新的文件描述符,并保存 ``fd`` 指向的已打开文件的一份拷贝即可。
在shell程序 ``user_shell`` 分割命令行参数的时候,我们要检查是否存在通过 ``<````>`` 进行输入输出重定向的情况,如果存在的话则需要将它们从命令行参数中移除,并记录匹配到的输入文件名或输出文件名到字符串 ``input````output`` 中。注意为了实现方便我们这里假设输入shell程序的命令一定合法``<````>`` 最多只会出现一次,且后面总是会有一个参数作为重定向到的文件。
.. code-block:: rust
// user/src/bin/ch6b_user_shell.rs
// redirect input
let mut input = String::new();
if let Some((idx, _)) = args_copy
.iter()
.enumerate()
.find(|(_, arg)| arg.as_str() == "<\0") {
input = args_copy[idx + 1].clone();
args_copy.drain(idx..=idx + 1);
}
// redirect output
let mut output = String::new();
if let Some((idx, _)) = args_copy
.iter()
.enumerate()
.find(|(_, arg)| arg.as_str() == ">\0") {
output = args_copy[idx + 1].clone();
args_copy.drain(idx..=idx + 1);
}
打开文件和替换的过程则发生在 ``fork`` 之后的子进程分支中:
.. code-block:: rust
:linenos:
// user/src/bin/user_shell.rs
let pid = fork();
if pid == 0 {
// input redirection
if !input.is_empty() {
let input_fd = open(input.as_str(), OpenFlags::RDONLY);
if input_fd == -1 {
println!("Error when opening file {}", input);
return -4;
}
let input_fd = input_fd as usize;
close(0);
assert_eq!(dup(input_fd), 0);
close(input_fd);
}
// output redirection
if !output.is_empty() {
let output_fd = open(
output.as_str(),
OpenFlags::CREATE | OpenFlags::WRONLY
);
if output_fd == -1 {
println!("Error when opening file {}", output);
return -4;
}
let output_fd = output_fd as usize;
close(1);
assert_eq!(dup(output_fd), 1);
close(output_fd);
}
// child process
if exec(args_copy[0].as_str(), args_addr.as_slice()) == -1 {
println!("Error when executing!");
return -4;
}
unreachable!();
} else {
let mut exit_code: i32 = 0;
let exit_pid = waitpid(pid as usize, &mut exit_code);
assert_eq!(pid, exit_pid);
println!("Shell: Process {} exited with code {}", pid, exit_code);
}
- 输入重定向发生在第 6~16 行。我们尝试打开输入文件 ``input````input_fd`` 中。之后,首先通过 ``close`` 关闭标准输入所在的文件描述符 0 。之后通过 ``dup`` 来分配一个新的文件描述符来访问 ``input_fd`` 对应的输入文件。这里用到了文件描述符分配的重要性质:即必定分配可用描述符中编号最小的一个。由于我们刚刚关闭了描述符 0 ,那么在 ``dup`` 的时候一定会将它分配出去,于是现在应用进程的文件描述符 0 就对应到输入文件了。最后,因为应用进程的后续执行不会用到输入文件原来的描述符 ``input_fd`` ,所以就将其关掉。
- 输出重定向则发生在 18~31 行。它的原理和输入重定向几乎完全一致,只是通过 ``open`` 打开文件的标志不太相同

View File

@@ -0,0 +1,20 @@
chapter7练习
===========================================
编程作业
-------------------------------------------
本章无编程作业
问答作业
-------------------------------------------
(1) 举出使用 pipe 的一个实际应用的例子。
(2) 如果需要在多个进程间互相通信,则需要为每一对进程建立一个管道,非常繁琐,请设计一个更易用的多进程通信机制。
报告要求
---------------------------------------
- 完成问答题。
- (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。

View File

@@ -0,0 +1,10 @@
第七章:进程间通信
==============================================
.. toctree::
:maxdepth: 4
0intro
1pipe
2cmdargs-and-redirection
3exercise

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.