start ch3.

This commit is contained in:
Exusial 2021-08-08 18:26:05 +08:00
parent b2e9e9a200
commit 643bc1ac50
3 changed files with 98 additions and 194 deletions

View File

@ -42,8 +42,7 @@
本章所介绍的多道程序和分时多任务系统都有一些共同的特点:在内存中同一时间可以驻留多个应用。所有的应用都是在系统启动的时候分别加载到内存的不同区域中。由于目前计算机系统中只有一个处理器,则同一时间最多只有一个应用在执行,剩下的应用则处于就绪状态,需要内核将处理器分配给它们才能开始执行。一旦应用开始执行,它就处于运行状态了。 本章所介绍的多道程序和分时多任务系统都有一些共同的特点:在内存中同一时间可以驻留多个应用。所有的应用都是在系统启动的时候分别加载到内存的不同区域中。由于目前计算机系统中只有一个处理器,则同一时间最多只有一个应用在执行,剩下的应用则处于就绪状态,需要内核将处理器分配给它们才能开始执行。一旦应用开始执行,它就处于运行状态了。
本章我们主要是着眼于抢占式的支持多进程的操作系统通过时钟中断来达到进程的切换。当然我们的OS也支持进程主动放弃执行。具体请阅读之后的章节。
本章主要是设计和实现建立支持 **多道程序** 的二叠纪“锯齿螈”初级操作系统、支持多道程序的三叠纪“始初龙”协作式操作系统和支持 **分时多任务** 的三叠纪“腔骨龙”抢占式操作系统,从而对可支持运行一批应用程序的多种执行环境有一个全面和深入的理解,并可归纳抽象出 **任务** **任务切换** 等操作系统的概念。
.. note:: .. note::
@ -59,22 +58,10 @@
.. _term-multiprogramming: .. _term-multiprogramming:
.. _term-time-sharing-multitasking: .. _term-time-sharing-multitasking:
**多道程序** (Multiprogramming) 和 **分时多任务** (Time-Sharing Multitasking) 对于应用的要求是不同的,因此我们分别为它们编写了不同的应用,代码也被放在两个不同的分支上。对于它们更加深入的讲解请参考本章正文,我们在引言中仅给出运行代码的方法。
获取多道程序的代码:
.. code-block:: console
$ git clone https://github.com/rcore-os/rCore-Tutorial-v3.git
$ cd rCore-Tutorial-v3
$ git checkout ch3-coop
获取分时多任务系统的代码: 获取分时多任务系统的代码:
.. code-block:: console .. code-block:: console
$ git clone https://github.com/rcore-os/rCore-Tutorial-v3.git
$ cd rCore-Tutorial-v3
$ git checkout ch3 $ git checkout ch3
在 qemu 模拟器上运行本章代码: 在 qemu 模拟器上运行本章代码:
@ -290,15 +277,6 @@
本章代码导读 本章代码导读
----------------------------------------------------- -----------------------------------------------------
本章的重点是实现对应用之间的协作式和抢占式任务切换的操作系统支持。与上一章的操作系统实现相比,有如下一些不同的情况导致实现上也有不同: 随着章节的推进我们的OS从测例嵌入main.c到批处理的OS系统再到本章的多进程OS可以说我们的OS已经初具雏形了。在进入本章的内容之前大家需要对进程有一个清晰的认识。进程就是“执行中的程序”因此每一个进程需要有程序运行所需要的资源。每一个进程又有着自己的优先级使得进程在调度的时候存在某种机制使得高优先级的进程能够优先执行而低优先级的进程又不能永远不执行。我们本章就要设计一个支持优先级进程调度的OS。
- 多个应用同时放在内存中,所以他们的起始地址是不同的,且地址范围不能重叠 同时进程的引入也意味着同时存在多个app在运行这意味着我们需要一次将多个测例的bin文件移动到指定的内存位置之中并且为每一个bin都做好配套的初始化。
- 应用在整个执行过程中会暂停或被抢占,即会有主动或被动的任务切换
对于第一个不同,需要对应用程序的地址空间布局进行调整,每个应用的地址空间都不相同,且不能重叠。这并不要修改应用程序本身。通过一个脚本 ``build.py`` 来让编译器在编译不同应用时用到的链接脚本 ``linker.ld`` 中的 ``BASE_ADDRESS`` 都不同,且有足够大的地址间隔。这样就可以让每个应用所在的内存空间是不同的。
对于第二个不同,需要实现任务切换,这就需要在上一章的 ``trap`` 上下文切换的基础上,再加上一个 ``task`` 上下文切换,才能完成完整的任务切换。这里面的关键数据结构是表示应用执行上下文的 ``TaskContext`` 和具体完成上下文切换的汇编语言编写的 ``__switch`` 函数。一个应用的执行需要被操作系统管理起来,这是通过 ``TaskControlBlock`` 数据结构来表示应用执行上下文的动态过程和动态状态(运行态、就绪态等)。再加上让应用程序第一次执行的前期初始化准备而建立的 ``TaskManager`` 数据结构的全局变量实例 ``TASK_MANAGER`` ,形成了本章的难点部分。
当然,但应用程序可以在用户态执行后,还需要有新的系统调用 ``sys_yield`` 的实现来支持应用自己的主动暂停;还要添加对时钟中断的处理,来支持抢占应用执行的抢占式切换。有了时钟中断,就可以在一定时间内打断应用的执行,并主动切换到另外一个应用,这部分主要是通过对 ``trap_handler`` 函数中进行扩展,来完成在时钟中断产生时可能进行的任务切换。 ``TaskManager`` 数据结构的成员函数 ``run_next_task`` 来实现基于任务控制块的切换,并会具体调用 ``__switch`` 函数完成硬件相关部分的任务上下文切换。
如果理解了上面的数据结构和相关函数的关系和访问情况,那么就可以比较容易理解本章改进后的操作系统。

View File

@ -11,131 +11,42 @@
.. ..
chyyuu有一个ascii图画出我们做的OS在本节的部分。 chyyuu有一个ascii图画出我们做的OS在本节的部分。
多道程序放置 多道程序放置
---------------------------- ----------------------------
与第二章相同,所有应用的 ELF 都经过 strip 丢掉所有 ELF header 和符号变为二进制镜像文件,随后以同样的格式通过 ``link_user.S`` 在编译的时候直接链接到内核的数据段中。不同的是,我们对相关模块进行了调整:在第二章中应用的加载和进度控制都交给 ``batch`` 子模块,而在第三章中我们将应用的加载这部分功能分离出来在 ``loader`` 子模块中实现,应用的执行和切换则交给 ``task`` 子模块。 我们仍然使用pack.py以及kernel.py来生成链接测例bin文件的link_app.S以及kernel_app.ld两个文件。这些内容相较第二章并没有任何改变。主要改变的是对多道程序的加载上要求同时加载多个程序了。
注意,我们需要调整每个应用被构建时候使用的链接脚本 ``linker.ld`` 中的起始地址 ``BASE_ADDRESS`` 为它实际会被内核加载并运行的地址。也就是要做到:应用知道自己会被加载到某个地址运行,而内核也确实能做到将它加载到那个地址。这算是应用和内核在某种意义上达成的一种协议。之所以要有这么苛刻的条件,是因为应用和内核的能力都很弱,通用性很低。事实上,目前应用程序的编址方式是基于绝对位置的而并没做到与位置无关,内核也没有提供相应的重定位机制。
.. note::
对于编址方式,需要再回顾一下编译原理课讲解的后端代码生成技术,以及计算机组成原理课的指令寻址方式的内容。可以在 `这里 <https://nju-projectn.github.io/ics-pa-gitbook/ics2020/4.2.html>`_ 找到更多有关
位置无关和重定位的说明。
由于每个应用被加载到的位置都不同,也就导致它们的链接脚本 ``linker.ld`` 中的 ``BASE_ADDRESS`` 都是不同的。实际上,
我们写了一个脚本 ``build.py`` 而不是直接用 ``cargo build`` 构建应用的链接脚本:
.. code-block:: python
:linenos:
# user/build.py
import os
base_address = 0x80400000
step = 0x20000
linker = 'src/linker.ld'
app_id = 0
apps = os.listdir('src/bin')
apps.sort()
for app in apps:
app = app[:app.find('.')]
lines = []
lines_before = []
with open(linker, 'r') as f:
for line in f.readlines():
lines_before.append(line)
line = line.replace(hex(base_address), hex(base_address+step*app_id))
lines.append(line)
with open(linker, 'w+') as f:
f.writelines(lines)
os.system('cargo build --bin %s --release' % app)
print('[build.py] application %s start with address %s' %(app, hex(base_address+step*app_id)))
with open(linker, 'w+') as f:
f.writelines(lines_before)
app_id = app_id + 1
它的思路很简单,在遍历 ``app`` 的大循环里面只做了这样几件事情:
- 第 16~22 行,找到 ``src/linker.ld`` 中的 ``BASE_ADDRESS = 0x80400000;`` 这一行,并将后面的地址
替换为和当前应用对应的一个地址;
- 第 23 行,使用 ``cargo build`` 构建当前的应用,注意我们可以使用 ``--bin`` 参数来只构建某一个应用;
- 第 25~26 行,将 ``src/linker.ld`` 还原。
多道程序加载 多道程序加载
---------------------------- ----------------------------
应用的加载方式也和上一章的有所不同。上一章中讲解的加载方法是让所有应用都共享同一个固定的加载物理地址。也是因为这个原因,内存中同时最多只能驻留一个应用,当它运行完毕或者出错退出的时候由操作系统的 ``batch`` 子模块加载一个新的应用来替换掉它。本章中,所有的应用在内核初始化的时候就一并被加载到内存中。为了避免覆盖,它们自然需要被加载到不同的物理地址。这是通过调用 ``loader`` 子模块的 ``load_apps`` 函数实现的: 从本章开始不再使用上一章批处理操作系统的run_next_app函数。让我们看看batch.c文件之中修改了什么。
.. code-block:: rust .. code-block:: c
:linenos: :linenos:
// os/src/loader.rs // kernel/batch.c
pub fn load_apps() { int load_app(int n, uint64* info) {
extern "C" { fn _num_app(); } uint64 start = info[n], end = info[n+1], length = end - start;
let num_app_ptr = _num_app as usize as *const usize; memset((void*)BASE_ADDRESS + n * MAX_APP_SIZE, 0, MAX_APP_SIZE);
let num_app = get_num_app(); memmove((void*)BASE_ADDRESS + n * MAX_APP_SIZE, (void*)start, length);
let app_start = unsafe { return length;
core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1) }
};
// clear i-cache first int run_all_app() {
unsafe { llvm_asm!("fence.i" :::: "volatile"); } for(int i = 0; i < app_num; ++i) {
// load apps struct proc* p = allocproc(); // 分配一个进程控制块
for i in 0..num_app { struct trapframe* trapframe = p->trapframe;
let base_i = get_base_i(i); printf("run app %d\n", i);
// clear region load_app(i, app_info_ptr);
(base_i..base_i + APP_SIZE_LIMIT).for_each(|addr| unsafe { uint64 entry = BASE_ADDRESS + i*MAX_APP_SIZE;
(addr as *mut u8).write_volatile(0) trapframe->epc = entry;
}); trapframe->sp = (uint64) p->ustack + PAGE_SIZE;
// load app from data section to memory p->state = RUNNABLE;
let src = unsafe {
core::slice::from_raw_parts(
app_start[i] as *const u8,
app_start[i + 1] - app_start[i]
)
};
let dst = unsafe {
core::slice::from_raw_parts_mut(base_i as *mut u8, src.len())
};
dst.copy_from_slice(src);
} }
return 0;
} }
可以看出,第 :math:`i` 个应用被加载到以物理地址 ``base_i`` 开头的一段物理内存上,而 ``base_i`` 的计算方式如下: 可以看到,进程 load 的逻辑其实没有变化。但是我们在run_all_app函数之中一次性加载了所有的测例程序。具体的方式是遍历每一个app获取其放置的位置并根据其序号i设置相对于BASE_ADDRESS的偏移量作为程序的起始位置。我们认为设定每个进程所使用的空间是 [0x80400000 + i*0x20000, 0x80400000 + (i+1)*0x20000),每个进程的最大 size 为 0x20000i 即为进程编号。需要注意此时同时完成了每一个程序对应的进程的初始化以及状态的设置(对进程还不熟悉的同学可以阅读下一节)。
.. code-block:: rust 现在应用程序加载完毕了。不同于批处理操作系统,我们该如何执行它们呢?
:linenos:
// os/src/loader.rs
fn get_base_i(app_id: usize) -> usize {
APP_BASE_ADDRESS + app_id * APP_SIZE_LIMIT
}
我们可以在 ``config`` 子模块中找到这两个常数。从这一章开始, ``config`` 子模块用来存放内核中所有的常数。看到 ``APP_BASE_ADDRESS`` 被设置为 ``0x80400000`` ,而 ``APP_SIZE_LIMIT`` 和上一章一样被设置为 ``0x20000`` ,也就是每个应用二进制镜像的大小限制。因此,应用的内存布局就很明朗了——就是从 ``APP_BASE_ADDRESS`` 开始依次为每个应用预留一段空间。
这样,我们就说明了多个应用是如何被构建和加载的。
执行应用程序
----------------------------
当多道程序的初始化放置工作完成,或者是某个应用程序运行结束或出错的时候,我们要调用 run_next_app 函数切换到下一个应用程序。此时 CPU 运行在 S 特权级的操作系统中,而操作系统希望能够切换到 U 特权级去运行应用程序。这一过程与上章的 :ref:`执行应用程序 <ch2-app-execution>` 一节的描述类似。相对不同的是,操作系统知道每个应用程序预先加载在内存中的位置,这就需要设置应用程序返回的不同 Trap 上下文Trap上下文中保存了 放置程序起始地址的``epc`` 寄存器内容):
- 跳转到应用程序(编号 :math:`i` )的入口点 :math:`\text{entry}_i`
- 将使用的栈切换到用户栈 :math:`\text{stack}_i`
二叠纪“锯齿螈”操作系统
------------------------
这样,我们的二叠纪“锯齿螈”操作系统就算是实现完毕了。
..
chyyuu有一个ascii图画出我们做的OS。

View File

@ -1,74 +1,89 @@
任务切换 进程和进程切换
================================ ================================
本节导读 本节导读
-------------------------- --------------------------
在上一节实现的二叠纪“锯齿螈”操作系统还是比较原始,一个应用会独占 CPU 直到它出错或主动退出。操作系统还是以程序的一次执行过程(从开始到结束)作为处理器切换程序的时间段。为了提高效率,我们需要引入新的操作系统概念 **任务****任务切换****任务上下文** 本节会介绍进程的调度方式。这是本章的重点之一
如果把应用程序执行的整个过程进行进一步分析,可以看到,如果程序访问 IO 或睡眠等待时,其实是不需要占用处理器的,于是我们可以把应用程序的不同时间段的执行过程分为两类,占用处理器执行有效任务的计算阶段和不必占用处理器的等待阶段。这些按时间流连接在一起的不同类型的多个阶段形成了一个我们熟悉的“暂停-继续...”组合的 :ref:`执行流或执行历史 <term-execution-flow>` 。从开始到结束的整个执行流就是应用程序的整个执行过程。 进程的概念
本节的重点是操作系统的核心机制—— **任务切换** 。 任务切换支持的场景是:一个应用在运行途中便会主动交出 CPU 的使用权,此时它只能暂停执行,等到内核重新给它分配处理器资源之后才能恢复并继续执行。
任务的概念形成
--------------------------------- ---------------------------------
.. 导语中提到了,进程就是运行的程序。既然是程序,那么它就需要程序执行的一切资源,包括栈、寄存器等等。不同于用户线程,用户进程有着自己独立的用户栈和内核栈。但是无论如何寄存器是只有一套的,因此进程切换时对于寄存器的保存以及恢复是我们需要关心的问题。
chyyuu程序执行过程的图示。
如果操作系统能够在某个应用程序处于等待阶段的时候,把处理器转给另外一个处于计算阶段的应用程序,那么只要转换的开销不大,那么处理器的执行效率就会大大提高。当然,这需要应用程序在运行途中能主动交出 CPU 的使用权,此时它处于等待阶段,等到操作系统让它再次执行后,那它就可以继续执行了。 本章中新增的proc.h定义了我们OS的进程的PCB进程管理块和进程一一对应结构体
.. _term-task:
.. _term-task-switch:
到这里,我们就把应用程序的一个计算阶段的执行过程(也是一段执行流)称为一个 **任务** ,所有的任务都完成后,应用程序也就完成了。从一个程序的任务切换到另外一个程序的任务称为 **任务切换** 。为了确保切换后的任务能够正确继续执行,操作系统需要支持让任务的执行“暂停”和“继续”。
.. _term-task-context:
我们又看到了熟悉的“暂停-继续”组合。一旦一条执行流需要支持“暂停-继续”,就需要提供一种执行流切换的机制,而且需要保证执行流被切换出去之前和切换回来之后,它的状态,也就是在执行过程中同步变化的资源(如寄存器、栈等)需要保持不变,或者变化在它的预期之内。而不是所有的资源都需要被保存,事实上只有那些对于执行流接下来的进行仍然有用,且在它被切换出去的时候有被覆盖的风险的那些资源才有被保存的价值。这些物理资源被称为 **任务上下文 (Task Context)**
这里,大家开始在具体的操作系统中接触到了一些抽象的概念,其实这些概念都是具体代码的结构和代码动态执行过程的文字表述而已。
不同类型的上下文与切换
---------------------------------
在执行流切换过程中,我们需要结合硬件机制和软件实现来保存和恢复任务上下文。任务的一次切换涉及到被换出和即将被换入的两条执行流(分属两个任务),通常它们都需要共同遵循某些约定来合作完成这一过程。在前两章,我们已经看到了两种上下文保存/恢复的实例。让我们再来回顾一下它们:
- 第一章《RV64 裸机应用》中,我们介绍了 :ref:`函数调用与栈 <function-call-and-stack>` 。当时提到过,为了支持嵌套函数调用,不仅需要硬件平台提供特殊的跳转指令,还需要保存和恢复 :ref:`函数调用上下文 <term-function-context>` 。注意在 *我们* 的定义中,函数调用包含在普通控制流(与异常控制流相对)之内,且始终用一个固定的栈来保存执行的历史记录,因此函数调用并不涉及执行流的切换。但是我们依然可以将其看成调用者和被调用者两个执行过程的“切换”,二者的协作体现在它们都遵循调用规范,分别保存一部分通用寄存器,这样的好处是编译器能够有足够的信息来尽可能减少需要保存的寄存器的数目。虽然当时用了很大的篇幅来说明,但其实整个过程都是编译器负责完成的,我们只需设置好栈就行了。
- 第二章《批处理系统》中第一次涉及到了某种异常Trap控制流即两条执行流的切换需要保存和恢复 :ref:`系统调用Trap上下文 <term-trap-context>` 。当时,为了让内核能够 *完全掌控* 应用的执行,且不会被应用破坏整个系统,我们必须利用硬件
提供的特权级机制,让应用和内核运行在不同的特权级。应用运行在 U 特权级,它所被允许的操作进一步受限,处处被内核监督管理;而内核运行在 S 特权级,有能力处理应用执行过程中提出的请求或遇到的状况。
应用程序与操作系统打交道的核心在于硬件提供的 Trap 机制,也就是在 U 特权级运行的应用执行流和在 S 特权级运行的 Trap 执行流操作系统的陷入处理部分之间的切换。Trap 执行流是在 Trap 触发的一瞬间生成的,它和原应用执行流有着很密切的联系,因为它唯一的目标就是处理 Trap 并恢复到原应用执行流。而且,由于 Trap 机制对于应用来说几乎是透明的,所以基本上都是 Trap 执行流在“负重前行”。Trap 执行流需要把 **Trap 上下文** 保存在自己的
内核栈上,里面包含几乎所有的通用寄存器,因为在 Trap 处理过程中它们都可能被用到。如果有需要的话,可以回看
:ref:`Trap 上下文保存与恢复 <trap-context-save-restore>` 小节。
任务切换的设计与实现
---------------------------------
本节的任务切换的执行过程是第二章的 Trap 之后的另一种异常控制流,都是描述两条执行流之间的切换,如果将它和 Trap 切换进行比较,会有如下异同:
- 与 Trap 切换不同,它不涉及特权级切换;
- 与 Trap 切换不同,它的一部分是由编译器帮忙完成的;
- 与 Trap 切换相同,它对应用是透明的。
事实上,它是来自两个不同应用的 Trap 执行流之间的切换。当一个应用 Trap 到 S 模式的操作系统中进行进一步处理即进入了操作系统的Trap执行流的时候其 Trap 执行流可以调用一个特殊的 ``__switch`` 函数。这个函数表面上就是一个普通的函数调用:在 ``__switch`` 返回之后,将继续从调用该函数的位置继续向下执行。但是其间却隐藏着复杂的执行流切换过程。具体来说,调用 ``__switch`` 之后直到它返回前的这段时间,原 Trap 执行流会先被暂停并被切换出去, CPU 转而运行另一个应用的 Trap 执行流。之后在时机合适的时候,原 Trap 执行流才会从某一条 Trap 执行流(很有可能不是它之前切换到的那一条)切换回来继续执行并最终返回。不过,从实现的角度讲, ``__switch`` 和一个普通的函数之间的差别仅仅是它会换栈。
.. image:: task_context.png
当 Trap 执行流准备调用 ``__switch`` 函数并进入暂停状态的时候,让我们考察一下它内核栈上的情况。如上图所示,在准备调用 ``__switch`` 函数之前,内核栈上从栈底到栈顶分别是保存了应用执行状态的 Trap 上下文以及内核在对 Trap 处理的过程中留下的调用栈信息。由于之后还要恢复回来执行,我们必须保存 CPU 当前的某些寄存器,我们称它们为 **任务上下文** (Task Context)。我们会在稍后介绍里面需要包含哪些寄存器。至于保存的位置,我们将任务上下文直接压入内核栈的栈顶,从这一点上来说它和函数调用一样。
这样需要保存的信息就已经确实的保存在内核栈上了,而恢复的时候我们要从任务上下文的位置——也就是这一时刻内核栈栈顶的位置找到被保存的寄存器快照进行恢复,这个位置也需要被保存下来。对于每一条被暂停的 Trap 执行流,我们都用一个名为 ``task_cx_ptr`` 的变量来保存它栈顶的任务上下文的地址。利用 C 语言的引用来描述的话就是:
.. code-block:: C .. code-block:: C
:linenos:
TaskContext *task_cx_ptr = &task_cx; // kernel/trap.h
struct proc {
enum procstate state; // 进程状态
int pid; // 进程ID
uint64 ustack;
uint64 kstack;
struct trapframe *trapframe;
struct context context; // 用于保存进程内核态的寄存器信息,进程切换时使用
};
由于我们要用 ``task_cx_ptr`` 这个变量来进行保存任务上下文的地址,自然也要对任务上下文的地址进行修改。于是我们还需要指向 ``task_cx_ptr`` 这个变量的指针 ``task_cx_ptr2`` 可以看到每一个进程的PCB都保存了它当前的状态以及它的PID每个进程的PID不同。同时记录了其用户栈和内核栈的起始地址。trapframe和context在异常中断的切换以及进程之间的切换起到了保存的重要作用。
进程的状态是大家比较熟悉的问题了。OS课程上将进程的状态分为创建、就绪、执行、等待以及结束5大阶段未来还会有挂起。在我们的OS之中对状态的分类略有不同。我们一般用RUNNABLE代表就绪的进程RUNNING代表正在执行的进程UNUSED代表池中未分配或已经结束的进程USED代表已经分配好但是还未加载完毕可以就绪执行的进程。
OS的进程结构
---------------------------------
在我们的OS之中我们采用了非常朴素的进程池方式来存放进程
.. code-block:: C .. code-block:: C
:linenos:
// kernel/trap.c
struct proc pool[NPROC];
struct proc idle; // boot proc始终存在
struct proc* current_proc; // 指示当前进程
TaskContext **task_cx_ptr2 = &task_cx_ptr; char kstack[NPROC][PAGE_SIZE];
char ustack[NPROC][PAGE_SIZE];
char trapframe[NPROC][PAGE_SIZE];
extern char boot_stack_top[]; // bootstack用作 idle proc kernel stack
可以看到我们最多同时有NPROC个进程并且进程池的下标对应着进程的PID。这样使得我们操作进程十分方便。每一个进程的用户栈、内核栈以及trapframe所需的空间已经预先分配好了。当然缺点是进程池空间有限不过直到lab8之前大家都无需担心这个问题。
比较重要的是current_proc它代表着当前正在执行的进程。因此这个变量在进程切换时也需要维护来保证其正确性。活用此变量能大大方便我们的编程。
进程的分配
---------------------------------
进程的分配实际上本质就是从进程池中挑选一个还未使用状态为UNUSED的位置分配给进程。具体代码如下:
.. code-block:: C
:linenos:
// kernel/proc.c
struct proc* allocproc(void)
{
struct proc *p;
for(p = pool; p < &pool[NPROC]; p++) {
if(p->state == UNUSED) {
goto found;
}
}
return 0;
found:
p->pid = allocpid(); // 分配一个没有被使用过的 id
p->state = USED; // 标记该控制块被使用
memset(&p->context, 0, sizeof(p->context));
memset(p->trapframe, 0, PAGE_SIZE);
memset((void*)p->kstack, 0, PAGE_SIZE);
// 初始化第一次运行的上下文信息
p->context.ra = (uint64)usertrapret;
p->context.sp = p->kstack + PAGE_SIZE;
return p;
}
分配进程需要初始化其PID以及清空其栈空间并设置第一次运行的上下文信息为usertrapret使得进程能够从内核的S态返回U态并执行自己的代码。
接下来我们同样从栈上内容的角度来看 ``__switch`` 的整体流程: 接下来我们同样从栈上内容的角度来看 ``__switch`` 的整体流程: