# 与进程有关的重要系统调用 - Open-Source-OS-Training-Camp-2022 文档

Hide navigation sidebar
Hide table of contents sidebar
Toggle table of contents sidebar

# 与进程有关的重要系统调用

## 重要系统调用

### fork 系统调用

    /// 功能：由当前进程 fork 出一个子进程。
    /// 返回值：对于子进程返回 0，对于当前进程则返回子进程的 PID 。
    /// syscall ID：220
    pub fn sys_fork() -> isize;

### exec 系统调用

    /// 功能：将当前进程的地址空间清空并加载一个特定的可执行文件，返回用户态后开始它的执行。
    /// 参数：字符串 path 给出了要加载的可执行文件的名字；
    /// 返回值：如果出错的话（如找不到名字相符的可执行文件）则返回 -1，否则不应该返回。
    /// 注意：path 必须以 "\0" 结尾，否则内核将无法确定其长度
    /// syscall ID：221
    pub fn sys_exec(path: &str) -> isize;

利用 `fork` 和 `exec` 的组合，我们能让创建一个子进程，并令其执行特定的可执行文件。

### waitpid 系统调用

    /// 功能：当前进程等待一个子进程变为僵尸进程，回收其全部资源并收集其返回值。
    /// 参数：pid 表示要等待的子进程的进程 ID，如果为 -1 的话表示等待任意一个子进程；
    /// exit_code 表示保存子进程返回值的地址，如果这个地址为 0 的话表示不必保存。
    /// 返回值：如果要等待的子进程不存在则返回 -1；否则如果要等待的子进程均未结束则返回 -2；
    /// 否则返回结束的子进程的进程 ID。
    /// syscall ID：260
    pub fn sys_waitpid(pid: isize, exit_code: *mut i32) -> isize;

`sys_waitpid` 在用户库中被封装成两个不同的 API， `wait(exit_code: &mut i32)` 和 `waitpid(pid: usize, exit_code: &mut i32)`， 前者用于等待任意一个子进程，后者用于等待特定子进程。它们实现的策略是如果子进程还未结束，就以 yield 让出时间片：

     1// user/src/lib.rs
     2
     3pub fn wait(exit_code: &mut i32) -> isize {
     4    loop {
     5        match sys_waitpid(-1, exit_code as *mut _) {
     6            -2 => { sys_yield(); }
     7            n => { return n; }
     8        }
     9    }
    10}

## 应用程序示例

借助这三个重要系统调用，我们可以开发功能更强大的应用。下面是两个案例： 用户初始程序-init 和 shell程序-user_shell 。

### 用户初始程序-initproc

在内核初始化完毕后创建的第一个进程，是 用户初始进程 (Initial Process) ，它将通过 `fork+exec` 创建 `user_shell` 子进程，并将被用于回收僵尸进程。

     1// user/src/bin/ch5b_initproc.rs
     2
     3#![no_std]
     4#![no_main]
     5
     6#[macro_use]
     7extern crate user_lib;
     8
     9use user_lib::{
    10    fork,
    11    wait,
    12    exec,
    13    yield_,
    14};
    15
    16#[no_mangle]
    17fn main() -> i32 {
    18    if fork() == 0 {
    19        exec("ch5b_user_shell\0");
    20    } else {
    21        loop {
    22            let mut exit_code: i32 = 0;
    23            let pid = wait(&mut exit_code);
    24            if pid == -1 {
    25                yield_();
    26                continue;
    27            }
    28            println!(
    29                "[initproc] Released a zombie process, pid={}, exit_code={}",
    30                pid,
    31                exit_code,
    32            );
    33        }
    34    }
    35    0
    36}

- 第 19 行为 `fork` 出的子进程分支，通过 `exec` 启动shell程序 `user_shell` ， 注意我们需要在字符串末尾手动加入 `\0` 。

- 第 21 行开始则为父进程分支，表示用户初始程序-initproc自身。它不断循环调用 `wait` 来等待并回收系统中的僵尸进程占据的资源。 如果回收成功的话则会打印一条报告信息给出被回收子进程的 PID 和返回值；否则就 `yield_` 交出 CPU 资源并在下次轮到它执行的时候再回收看看。

### shell程序-user_shell

user_shell 需要捕获用户输入并进行解析处理，为此添加一个能获取用户输入的系统调用：

    /// 功能：从文件中读取一段内容到缓冲区。
    /// 参数：fd 是待读取文件的文件描述符，切片 buffer 则给出缓冲区。
    /// 返回值：如果出现了错误则返回 -1，否则返回实际读到的字节数。
    /// syscall ID：63
    pub fn sys_read(fd: usize, buffer: &mut [u8]) -> isize;

实际调用时，我们必须要同时向内核提供缓冲区的起始地址及长度：

    // user/src/syscall.rs

    pub fn sys_read(fd: usize, buffer: &mut [u8]) -> isize {
        syscall(SYSCALL_READ, [fd, buffer.as_mut_ptr() as usize, buffer.len()])
    }

我们在用户库中将其进一步封装成每次能够从 标准输入 中获取一个字符的 `getchar` 函数。

shell程序 `user_shell` 实现如下：

     1// user/src/bin/ch5b_user_shell.rs
     2
     3#![no_std]
     4#![no_main]
     5
     6extern crate alloc;
     7
     8#[macro_use]
     9extern crate user_lib;
    10
    11const LF: u8 = 0x0au8;
    12const CR: u8 = 0x0du8;
    13const DL: u8 = 0x7fu8;
    14const BS: u8 = 0x08u8;
    15
    16use alloc::string::String;
    17use user_lib::{fork, exec, waitpid, yield_};
    18use user_lib::console::getchar;
    19
    20#[no_mangle]
    21pub fn main() -> i32 {
    22    println!("Rust user shell");
    23    let mut line: String = String::new();
    24    print!(">> ");
    25    loop {
    26        let c = getchar();
    27        match c {
    28            LF | CR => {
    29                println!("");
    30                if !line.is_empty() {
    31                    line.push('\0');
    32                    let pid = fork();
    33                    if pid == 0 {
    34                        // child process
    35                        if exec(line.as_str()) == -1 {
    36                            println!("Error when executing!");
    37                            return -4;
    38                        }
    39                        unreachable!();
    40                    } else {
    41                        let mut exit_code: i32 = 0;
    42                        let exit_pid = waitpid(pid as usize, &mut exit_code);
    43                        assert_eq!(pid, exit_pid);
    44                        println!(
    45                            "Shell: Process {} exited with code {}",
    46                            pid, exit_code
    47                        );
    48                    }
    49                    line.clear();
    50                }
    51                print!(">> ");
    52            }
    53            BS | DL => {
    54                if !line.is_empty() {
    55                    print!("{}", BS as char);
    56                    print!(" ");
    57                    print!("{}", BS as char);
    58                    line.pop();
    59                }
    60            }
    61            _ => {
    62                print!("{}", c as char);
    63                line.push(c as char);
    64            }
    65        }
    66    }
    67}

可以看到，在以第 25 行开头的主循环中，每次都是调用 `getchar` 获取一个用户输入的字符， 并根据它相应进行一些动作。第 23 行声明的字符串 `line` 则维护着用户当前输入的命令内容，它也在不断发生变化。

- 如果用户输入回车键（第 28 行），那么user_shell 会 fork 出一个子进程（第 34 行开始）并试图通过 `exec` 系统调用执行一个应用，应用的名字在字符串 `line` 中给出。如果 exec 的返回值为 -1 ， 说明在应用管理器中找不到对应名字的应用，此时子进程就直接打印错误信息并退出；否则子进程将开始执行目标应用。

  fork 之后的 user_shell 进程自己的逻辑可以在第 41 行找到。它在等待 fork 出来的子进程结束并回收掉它的资源，还会顺带收集子进程的退出状态并打印出来。

- 如果用户输入退格键（第 53 行），首先我们需要将屏幕上当前行的最后一个字符用空格替换掉， 这可以通过输入一个特殊的退格字节 `BS` 来实现。其次，user_shell 进程内维护的 `line` 也需要弹出最后一个字符。

- 如果用户输入了一个其他字符（第 61 行），就接将它打印在屏幕上，并加入到 `line` 中。

- 按键 `Ctrl+A` 再输入 `X` 来退出qemu模拟器。