Skip to content

Commit

Permalink
docs(Lab4, Lab5): split README.md and integrated with current pages f…
Browse files Browse the repository at this point in the history
…ramework.

Reviewed-by: Yiyang Wu <[email protected]>
Signed-off-by: Yiyang Wu <[email protected]>
  • Loading branch information
Ayanami1314 authored and ToolmanP committed Oct 2, 2024
1 parent 2587f5c commit e57b24e
Show file tree
Hide file tree
Showing 10 changed files with 397 additions and 0 deletions.
16 changes: 16 additions & 0 deletions Pages/Lab4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Lab 4:多核调度与IPC

在本实验中,ChCore将支持在多核处理器上启动(第一部分);实现多核调度器以调度执行多个线程(第二部分);最后实现进程间通信IPC(第三部分)。注释`/* LAB 4 TODO BEGIN (exercise #) */``/* LAB 4 TODO END (exercise #) */`之间代表需要填空的代码部分。

## Preparation

实验 4 与实验 3相同,需要在根目录下拉取 `musl-libc` 代码。

>```bash
> git submodule update --init --recursive
>```
使用 `make build` 检查是否能够正确项目编译。
> [!WARNING]
> 请确保成功拉取`musl-libc`代码后再尝试进行编译。若未成功拉取`musl-libc`就进行编译将会自动创建`musl-libc`文件夹,这可能导致无法成功拉取`musl-libc`代码,也无法编译成功。出现此种情况可以尝试将`user/chcore-libc/musl-libc`文件夹删除,并运行以上指令重新拉取代码。
54 changes: 54 additions & 0 deletions Pages/Lab4/IPC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# 进程间通信(IPC)

<!-- toc -->

在本部分,我们将实现ChCore的进程间通信,从而允许跨地址空间的两个进程可以使用IPC进行信息交换。

## ChCore进程间通讯概览
![](./assets/IPC-overview.png)

ChCore的IPC接口不是传统的send/recv接口。其更像客户端/服务器模型,其中IPC请求接收者是服务器,而IPC请求发送者是客户端。 服务器进程中包含三类线程:
* 主线程:该线程与普通的线程一样,类型为`TYPE_USER`。该线程会调用`ipc_register_server`将自己声明为一个IPC的服务器进程,调用的时候会提供两个参数:服务连接请求的函数client_register_handler和服务真正IPC请求的函数server_handler(即图中的`ipc_dispatcher`),调用该函数会创建一个注册回调线程;

* 注册回调线程:该线程的入口函数为上文提到的client_register_handler,类型为`TYPE_REGISTER`。正常情况下该线程不会被调度执行,仅当有Client发起建立IPC连接的请求时,该线程运行并执行client_register_handler,为请求建立连接的Client创建一个服务线程(即图中的IPC handler thread)并在服务器进程的虚拟地址空间中分配一个可以用来映射共享内存的虚拟地址。

* 服务线程:当Client发起建立IPC连接请求时由注册回调线程创建,入口函数为上文提到的server_handler,类型为`TYPE_SHADOW`。正常情况下该线程不会被调度执行,仅当有Client端线程使用`ipc_call`发起IPC请求时,该线程运行并执行server_handler(即图中的`ipc_dispatcher`),执行结束之后会调用`ipc_return`回到Client端发起IPC请求的线程。

> [!IMPORTANT] 注意
> 注册回调线程和服务线程都不再拥有调度上下文(Scheduling Context),也即不会主动被调度器调度到。其在客户端申请建立IPC连接或者发起IPC请求的时候才会被调度执行。为了实现该功能,这两种类型的线程会继承IPC客户端线程的调度上下文(即调度时间片budget),从而能被调度器正确地调度。
## ChCore IPC具体流程
为了实现ChCore IPC的功能,首先需要在Client与Server端创建起一个一对一的IPC Connection。该Connection保存了IPC Server的服务线程(即上图中IPC handler Thread)、Client与Server的共享内存(用于存放IPC通信的内容)。同一时刻,一个Connection只能有一个Client接入,并使用该Connection切换到Server的处理流程。ChCore提供了一系列机制,用于创建Connection以及创建每个Connection对应的服务线程。下面将以具体的IPC注册到调用的流程,详细介绍ChCore的IPC机制:

1. IPC服务器的主线程调用`ipc_register_server`(user/chcore-libc/musl-libc/src/chcore-port/ipc.c中)来声明自己为IPC的服务器端。

* 参数包括server_handler和client_register_handler,其中server_handler为服务端用于提供服务的回调函数(比如上图中IPC handler Thread的入口函数`ipc_dispatcher`);client_register_handler为服务端提供的用于注册的回调函数,该函数会创建一个注册回调线程。

* 随后调用ChCore提供的的系统调用:`sys_register_server`。该系统调用实现在kernel/ipc/connection.c当中,该系统调用会分配并初始化一个`struct ipc_server_config`和一个`struct ipc_server_register_cb_config`。之后将调用者线程(即主线程)的general_ipc_config字段设置为创建的`struct ipc_server_config`,其中记录了注册回调线程和IPC服务线程的入口函数(即图中的`ipc_dispatcher`)。将注册回调线程的general_ipc_config字段设置为创建的`struct ipc_server_register_cb_config`,其中记录了注册回调线程的入口函数和用户态栈地址等信息。

2. IPC客户端线程调用`ipc_register_client`(定义在user/chcore-libc/musl-libc/src/chcore-port/ipc.c中)来申请建立IPC连接。

* 该函数仅有一个参数,即IPC服务器的主线程在客户端进程cap_group中的capability。该函数会首先通过系统调用申请一块物理内存作为和服务器的共享内存(即图中的Shared Memory)。

* 随后调用`sys_register_client`系统调用。该系统调用实现在kernel/ipc/connection.c当中,该系统调用会将刚才申请的物理内存映射到客户端的虚拟地址空间中,然后调用`create_connection`创建并初始化一个`struct ipc_connection`类型的内核对象,该内核对象中的shm字段会记录共享内存相关的信息(包括大小,分别在客户端进程和服务器进程当中的虚拟地址和capability)。

* 之后会设置注册回调线程的栈地址、入口地址和第一个参数,并切换到注册回调线程运行。
3. 注册回调线程运行的入口函数为主线程调用`ipc_register_server`是提供的client_register_handler参数,一般会使用默认的`DEFAULT_CLIENT_REGISTER_HANDLER`宏定义的入口函数,即定义在user/chcore-libc/musl-libc/src/chcore-port/ipc.c中的`register_cb`

* 该函数首先分配一个用来映射共享内存的虚拟地址,随后创建一个服务线程。

* 随后调用`sys_ipc_register_cb_return`系统调用进入内核,该系统调用将共享内存映射到刚才分配的虚拟地址上,补全`struct ipc_connection`内核对象中的一些元数据之后切换回客户端线程继续运行,客户端线程从`ipc_register_client`返回,完成IPC建立连接的过程。

4. IPC客户端线程调用`ipc_create_msg``ipc_set_msg_data`向IPC共享内存中填充数据,然后调用`ipc_call`(user/chcore-libc/musl-libc/src/chcore-port/ipc.c中)发起IPC请求。

* `ipc_call`中会发起`sys_ipc_call`系统调用(定义在kernel/ipc/connection.c中),该系统调用将设置服务器端的服务线程的栈地址、入口地址、各个参数,然后迁移到该服务器端服务线程继续运行。由于当前的客户端线程需要等待服务器端的服务线程处理完毕,因此需要更新其状态为TS_WAITING,且不要加入等待队列。

5. IPC服务器端的服务线程在处理完IPC请求之后使用`ipc_return`返回。
* `ipc_return`会发起`sys_ipc_return`系统调用,该系统调用会迁移回到IPC客户端线程继续运行,IPC客户端线程从`ipc_call`中返回。

> [!CODING] 练习题 7
> 在user/chcore-libc/musl-libc/src/chcore-port/ipc.c与kernel/ipc/connection.c中实现了大多数IPC相关的代码,请根据注释补全kernel/ipc/connection.c中的代码。之后运行ChCore可以看到 “[TEST] Test IPC finished!” 输出,你可以通过 Test IPC 测试点。

> [!IMPORTANT]
> 以上为Lab4 Part3的所有内容
Binary file added Pages/Lab4/assets/IPC-overview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 27 additions & 0 deletions Pages/Lab4/multicore.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# 多核支持

<!-- toc -->

本部分实验没有代码题,仅有思考题。为了让ChCore支持多核,我们需要考虑如下问题:

- 如何启动多核,让每个核心执行初始化代码并开始执行用户代码?
- 如何区分不同核心在内核中保存的数据结构(比如状态,配置,内核对象等)?
- 如何保证内核中对象并发正确性,确保不会由于多个核心同时访问内核对象导致竞争条件?

在启动多核之前,我们先介绍ChCore如何解决第二个问题。ChCore对于内核中需要每个CPU核心单独存一份的内核对象,都根据核心数量创建了多份(即利用一个数组来保存)。ChCore支持的核心数量为PLAT_CPU_NUM(该宏定义在 kernel/common/machine.h 中,其代表可用CPU核心的数量,根据具体平台而异)。 比如,实验使用的树莓派3平台拥有4个核心,因此该宏定义的值为4。ChCore会CPU核心的核心ID作为数组的索引,在数组中取出对应的CPU核心本地的数据。为了方便确定当前执行该代码的CPU核心ID,我们在 kernel/arch/aarch64/machine/smp.c中提供了smp_get_cpu_id函数。该函数通过访问系统寄存器tpidr_el1来获取调用它的CPU核心的ID,该ID可用作访问上述数组的索引。

## 启动多核
在实验1中我们已经介绍,在QEMU模拟的树莓派中,所有CPU核心在开机时会被同时启动。在引导时这些核心会被分为两种类型。一个指定的CPU核心会引导整个操作系统和初始化自身,被称为主CPU(primary CPU)。其他的CPU核心只初始化自身即可,被称为其他CPU(backup CPU)。CPU核心仅在系统引导时有所区分,在其他阶段,每个CPU核心都是被相同对待的。

> [!QUESTION] 思考题 1
> 阅读汇编代码kernel/arch/aarch64/boot/raspi3/init/start.S。说明ChCore是如何选定主CPU,并阻塞其他其他CPU的执行的。
在树莓派真机中,还需要主CPU手动指定每一个CPU核心的的启动地址。这些CPU核心会读取固定地址的上填写的启动地址,并跳转到该地址启动。在kernel/arch/aarch64/boot/raspi3/init/init_c.c中,我们提供了wakeup_other_cores函数用于实现该功能,并让所有的CPU核心同在QEMU一样开始执行_start函数。

与之前的实验一样,主CPU在第一次返回用户态之前会在kernel/arch/aarch64/main.c中执行main函数,进行操作系统的初始化任务。在本小节中,ChCore将执行enable_smp_cores函数激活各个其他CPU。

> [!QUESTION] 思考题 2
> 阅读汇编代码kernel/arch/aarch64/boot/raspi3/init/start.S, init_c.c以及kernel/arch/aarch64/main.c,解释用于阻塞其他CPU核心的secondary_boot_flag是物理地址还是虚拟地址?是如何传入函数enable_smp_cores中,又是如何赋值的(考虑虚拟地址/物理地址)?
> [!IMPORTANT]
> 以上为Lab4 part1 的所有内容
35 changes: 35 additions & 0 deletions Pages/Lab4/performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# 实机运行与IPC性能优化

<!-- toc -->

在本部分,你需要对IPC的性能进行优化。为此,你首先需要在树莓派3B实机上运行ChCore。

> [!CODING] 练习题 8
> 请在树莓派3B上运行ChCore,并确保此前实现的所有功能都能正确运行。
在ChCore启动并通过测试后,在命令行运行
```shell
$ ./test_ipc_perf.bin
```

你会得到如下输出结果
```shell
[TEST] test ipc with 32 threads, time: xxx cycles
[TEST] test ipc with send cap, loop: 100, time: xxx cycles
[TEST] test ipc with send cap and return cap, loop: 100, time: xxx cycles
[TEST] Test IPC Perf finished!
```

> [!CODING] 练习题 9
> 尝试优化在第三部分实现的IPC的性能,降低test_ipc_perf.bin的三个测试所消耗的cycle数
IPC性能测试程序的测试用例包括:
1. 创建多个线程发起IPC请求(不传递cap),Server收到IPC后直接返回。记录从创建线程到所有线程运行结束的时间。
2. Client创建多个PMO对象,并发起IPC请求(传递PMO);Server收到IPC后读取PMO,并依据读出的值算出结果,将结果写回随IPC传递的PMO中并返回;Client在IPC返回后读取PMO中的结果。将上述过程循环多次并记录运行时间。
3. Client创建多个PMO对象,并发起IPC请求(传递PMO);Server收到IPC后读取PMO,并依据读出的值算出结果,然后创建新的PMO对象,将结果写入新创建的PMO中,并通过`ipc_return_with_cap`返回;Client在IPC返回后读取返回的PMO中的结果。将上述过程循环多次并记录运行时间。

在测试能够顺利通过的前提下,你可以修改任意代码。(测试程序所调用的函数位于 `user/chcore-libc/libchcore/porting/overrides/src/chcore-port/ipc.c`


> [!SUCCESS]
> 以上为Lab4 的所有内容
Loading

0 comments on commit e57b24e

Please sign in to comment.