From d12053ece3d95a6703bbae5de45e010306575361 Mon Sep 17 00:00:00 2001 From: Yu Chen Date: Tue, 19 Jul 2022 17:36:23 +0800 Subject: [PATCH] update ch5 --- source/chapter5/1process.rst | 4 ++-- source/chapter5/3implement-process-mechanism.rst | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/source/chapter5/1process.rst b/source/chapter5/1process.rst index 2e42f1f1..4e6f0e20 100644 --- a/source/chapter5/1process.rst +++ b/source/chapter5/1process.rst @@ -15,7 +15,7 @@ .. _term-process: -在本章的引言中,出于方便应用开发和使得应用功能更加强大的目标,我们引入了进程的概念。在本章之前,我们有 **任务** 的概念,即 **正在执行的程序** ,主角是程序。而相比于 **任务** , **进程** (Process) 的含义是 **在操作系统管理下的程序的一次执行过程**。这里的程序成了形容词,而执行过程成为了主角,这充分体现了动态变化的执行特点。尽管这说起来很容易,但事实上进程是一个内涵相当丰富且深刻、难以从单个角度解释清楚的抽象概念。我们可以先试着从动态和静态的角度来进行初步的思考。我们知道,当一个应用的源程序被编译器成功构建之后,它会从源代码变为某种格式的可执行文件,如果将其展开,可以在它的内存布局中看到若干个功能迥异的逻辑段。但如果仅是这样,它也就只是某种格式特殊的、被 **静态** 归档到存储器上的一个文件而已。 +在本章的引言中,出于方便应用开发和使得应用功能更加强大的目标,我们引入了进程的概念。在本章之前,我们有 **任务** 的概念,即 **正在执行的程序** ,主角是程序。而相比于 **任务** , **进程** (Process) 的含义是 **在操作系统管理下的程序的一次执行过程**。这里的“程序的”成了形容词,而执行过程成为了主角,这充分体现了动态变化的执行特点。尽管这说起来很容易,但事实上进程是一个内涵相当丰富且深刻、难以从单个角度解释清楚的抽象概念。我们可以先试着从动态和静态的角度来进行初步的思考。我们知道,当一个应用的源程序被编译器成功构建之后,它会从源代码变为某种格式的可执行文件,如果将其展开,可以在它的内存布局中看到若干个功能迥异的逻辑段。但如果仅是这样,它也就只是某种格式特殊的、被 **静态** 归档到存储器上的一个文件而已。 然而,可执行文件与其他类型文件的根本性不同在于它可以被内核加载并执行。这一执行过程自然是不能凭空进行的,而是需要占据某些真实的硬件资源。例如,可执行文件一定需要被加载到物理内存的某些区域中才能执行,另外还可能需要预留一些可执行文件内存布局中未规划的区域(比如堆和栈),这就会消耗掉部分内存空间;在执行的时候需要占据一个 CPU 的全部硬件资源,我们之前介绍过的有通用寄存器(其中程序计数器 pc 和栈指针 sp 两个意义尤其重大)、CSR 、各级 cache 、TLB 等。 @@ -178,7 +178,7 @@ exec 系统调用 其中 ``wait`` 表示等待任意一个子进程结束,根据 ``sys_waitpid`` 的约定它需要传的 pid 参数为 ``-1`` ;而 ``waitpid`` 则等待一个进程标识符的值为pid 的子进程结束。在具体实现方面,我们看到当 ``sys_waitpid`` 返回值为 ``-2`` ,即要等待的子进程存在但它却尚未退出的时候,我们调用 ``yield_`` 主动交出 CPU 使用权,待下次 CPU 使用权被内核交还给它的时候再次调用 ``sys_waitpid`` 查看要等待的子进程是否退出。这样做可以减小 CPU 资源的浪费。 -目前的实现风格是尽可能简化内核,因此 ``sys_waitpid`` 是立即返回的,即它的返回值只能给出返回这一时刻的状态。如果这一时刻要等待的子进程还尚未结束,那么也只能如实向应用报告这一结果。于是用户库 ``user_lib`` 就需要负责对返回状态进行持续的监控,因此它里面便需要进行循环检查。在后面的实现中,我们会将 ``sys_waitpid`` 的内核实现设计为 **阻塞** 的,也即直到得到一个确切的结果位置都停在内核内,也就意味着内核返回给应用的结果可以直接使用。那时 ``wait`` 和 ``waitpid`` 两个 API 的实现便会更加简单。 +目前的实现风格是尽可能简化内核,因此 ``sys_waitpid`` 是立即返回的,即它的返回值只能给出返回这一时刻的状态。如果这一时刻要等待的子进程还尚未结束,那么也只能如实向应用报告这一结果。于是用户库 ``usr/src/lib.rs`` 就需要负责对返回状态进行持续的监控,因此它里面便需要进行循环检查。在后续的实现中,我们会将 ``sys_waitpid`` 的内核实现设计为 **阻塞** 的,即直到得到一个确切的结果之前,其对应的进程暂停(不再继续执行)在内核内;如果 ``sys_waitpid`` 需要的值能够得到,则它对应的进程会被内核唤醒继续执行,且内核返回给应用的结果可以直接使用。那时 ``wait`` 和 ``waitpid`` 两个 API 的实现便会更加简单。 用户初始程序 initproc ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/source/chapter5/3implement-process-mechanism.rst b/source/chapter5/3implement-process-mechanism.rst index c7948c41..34e58c9d 100644 --- a/source/chapter5/3implement-process-mechanism.rst +++ b/source/chapter5/3implement-process-mechanism.rst @@ -10,7 +10,7 @@ - 进程调度机制:当进程主动调用 ``sys_yield`` 交出 CPU 使用权或者内核把本轮分配的时间片用尽的进程换出且换入下一个进程; - 进程生成机制:介绍进程相关的两个重要系统调用 ``sys_fork/sys_exec`` 的实现; - 进程资源回收机制:当进程调用 ``sys_exit`` 正常退出或者出错被内核终止之后如何保存其退出码,其父进程通过 ``sys_waitpid`` 系统调用收集该进程的信息并回收其资源。 -- 字符输入机制:为了支对shell程序-user_shell获得字符输入,介绍 ``sys_read`` 系统调用的实现; +- 字符输入机制:为了支持shell程序-user_shell获得字符输入,介绍 ``sys_read`` 系统调用的实现; 初始进程的创建 -------------------------------------------- @@ -101,6 +101,7 @@ 通过调用 ``task`` 子模块提供的 ``suspend_current_and_run_next`` 函数可以暂停当前任务并切换到下一个任务,当应用调用 ``sys_yield`` 主动交出使用权、本轮时间片用尽或者由于某些原因内核中的处理无法继续的时候,就会在内核中调用此函数触发调度机制并进行任务切换。下面给出了两种典型的使用情况: .. code-block:: rust + :linenos: :emphasize-lines: 4,18 // os/src/syscall/process.rs @@ -265,7 +266,7 @@ fork 系统调用的实现 它基本上和新建进程控制块的 ``TaskControlBlock::new`` 是相同的,但要注意以下几点: - 子进程的地址空间不是通过解析 ELF 文件,而是通过在第 8 行调用 ``MemorySet::from_existed_user`` 复制父进程地址空间得到的; -- 第 26 行,我们让子进程和父进程的 ``base_size`` ,也即应用数据的大小保持一致; +- 第 24 行,我们让子进程和父进程的 ``base_size`` ,也即应用数据的大小保持一致; - 在 fork 的时候需要注意父子进程关系的维护。第 28 行我们将父进程的弱引用计数放到子进程的进程控制块中,而在第 33 行我们将子进程插入到父进程的孩子向量 ``children`` 中。 我们在子进程内核栈上压入一个初始化的任务上下文,使得内核一旦通过任务切换到该进程,就会跳转到 ``trap_return`` 来进入用户态。而在复制地址空间的时候,子进程的 Trap 上下文也是完全从父进程复制过来的,这可以保证子进程进入用户态和其父进程回到用户态的那一瞬间 CPU 的状态是完全相同的(后面我们会让它们的返回值不同从而区分两个进程)。而两个进程的应用数据由于地址空间复制的原因也是完全相同的,这是 fork 语义要求做到的。 @@ -413,6 +414,7 @@ exec 系统调用的实现 过去的 ``trap_handler`` 实现是这样处理系统调用的: .. code-block:: rust + :linenos: // os/src/trap/mod.rs @@ -435,6 +437,7 @@ exec 系统调用的实现 这里的 ``cx`` 是当前应用的 Trap 上下文的可变引用,我们需要通过查页表找到它具体被放在哪个物理页帧上,并构造相同的虚拟地址来在内核中访问它。对于系统调用 ``sys_exec`` 来说,一旦调用它之后,我们会发现 ``trap_handler`` 原来上下文中的 ``cx`` 失效了——因为它是用来访问之前地址空间中 Trap 上下文被保存在的那个物理页帧的,而现在它已经被回收掉了。因此,为了能够处理类似的这种情况,我们在 ``syscall`` 分发函数返回之后需要重新获取 ``cx`` ,目前的实现如下: .. code-block:: rust + :linenos: // os/src/trap/mod.rs @@ -466,7 +469,8 @@ shell程序 user_shell 的输入机制 为了实现shell程序 ``user_shell`` 的输入机制,我们需要实现 ``sys_read`` 系统调用使得应用能够取得用户的键盘输入。 .. code-block:: rust - + :linenos: + // os/src/syscall/fs.rs use crate::sbi::console_getchar;