diff --git a/Assets/3-exception.png b/Assets/3-exception.png new file mode 100644 index 00000000..b9dfecdc Binary files /dev/null and b/Assets/3-exception.png differ diff --git a/Assets/IPC-overview.png b/Assets/IPC-overview.png new file mode 100644 index 00000000..e2f9ad5e Binary files /dev/null and b/Assets/IPC-overview.png differ diff --git a/Assets/capability.jpg b/Assets/capability.jpg new file mode 100644 index 00000000..84aa507f Binary files /dev/null and b/Assets/capability.jpg differ diff --git a/Pages/Lab4.md b/Pages/Lab4.md new file mode 100644 index 00000000..46e9a981 --- /dev/null +++ b/Pages/Lab4.md @@ -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`文件夹删除,并运行以上指令重新拉取代码。 diff --git a/Pages/Lab4/IPC.md b/Pages/Lab4/IPC.md new file mode 100644 index 00000000..38ef38db --- /dev/null +++ b/Pages/Lab4/IPC.md @@ -0,0 +1,54 @@ +# 进程间通信(IPC) + + + +在本部分,我们将实现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的所有内容 \ No newline at end of file diff --git a/Pages/Lab4/multicore.md b/Pages/Lab4/multicore.md new file mode 100644 index 00000000..fa8ee9b4 --- /dev/null +++ b/Pages/Lab4/multicore.md @@ -0,0 +1,27 @@ +# 多核支持 + + + +本部分实验没有代码题,仅有思考题。为了让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 的所有内容 \ No newline at end of file diff --git a/Pages/Lab4/performance.md b/Pages/Lab4/performance.md new file mode 100644 index 00000000..eaa56ef1 --- /dev/null +++ b/Pages/Lab4/performance.md @@ -0,0 +1,35 @@ +# 实机运行与IPC性能优化 + + + +在本部分,你需要对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 的所有内容 \ No newline at end of file diff --git a/Pages/Lab4/scheduler.md b/Pages/Lab4/scheduler.md new file mode 100644 index 00000000..fb7861bf --- /dev/null +++ b/Pages/Lab4/scheduler.md @@ -0,0 +1,105 @@ +# 多核调度 + + + +ChCore已经可以启动多核,但仍然无法对多个线程进行调度。本部分将首先实现协作式调度,从而允许当前在CPU核心上运行的线程主动退出或主动放弃CPU时,CPU核心能够切换到另一个线程继续执行。其后,我们将驱动树莓派上的物理定时器,使其以一定的频率发起中断,使得内核可以在一定时间片后重新获得对CPU核心的控制,并基于此进一步实现抢占式调度。 + +ChCore中与调度相关的函数与数据结构定义在kernel/include/sched/sched.h中。 +sched_ops是用于抽象ChCore中调度器的一系列操作。它存储指向不同调度操作的函数指针,以支持不同的调度策略。 +cur_sched_ops则是一个sched_ops的实例,其在内核初始化过程中(main函数)调用sched_init进行初始化。 +ChCore用在 kernel/include/sched/sched.h 中定义的静态函数封装对cur_sched_ops的调用。sched_ops中定义的调度器操作如下所示: + +* sche_init:初始化调度器。 +* sched:进行一次调度。即将正在运行的线程放回就绪队列,然后在就绪队列中选择下一个需要执行的线程返回。 +* sched_enqueue:将新线程添加到调度器的就绪队列中。 +* sched_dequeue:从调度器的就绪队列中取出一个线程。 +* sched_top:用于debug,打印当前所有核心上的运行线程以及等待线程的函数。 + +在本部分将实现一个基本的Round Robin(时间片轮转)调度器,该程序调度在同一CPU核心上运行的线程,因此内核初始化过程调用sched_init时传入了&rr作为参数。该调度器的调度操作(即对于sched_ops定义的各个函数接口的实现)实现在kernel/sched/policy_rr.c中,这里简要介绍其涉及的数据结构: + +`current_threads`是一个数组,分别指向每个CPU核心上运行的线程。而`current_thread`则利用`smp_get_cpu_id`获取当前运行核心的id,从而找到当前核心上运行的线程。 + +`struct queue_meta`定义了round robin调度器使用的就绪队列,其中`queue_head`字段是连接该就绪队列上所有等待线程的队列,`queue_len`字段是目前该就绪队列的长度,`queue_lock`字段是用于保证该队列并发安全的锁。 kernel/sched/policy_rr.c定义了一个全局变量`rr_ready_queue_meta`,该变量是一个`struct queue_meta`类型的数组,数组大小由`PLAT_CPU_NUM`定义,即代表每个CPU核心都具有一个就绪队列。运行的CPU核心可以通过`smp_get_cpu_id`获取当前运行核心的id,从而在该数组中找到当前核心对应的就绪队列。 + +## 调度队列初始化 +内核初始化过程中会调用`sched_init`初始化调度相关的元数据,`sched_init`定义在kernel/sched/sched.c中,该函数首先初始化idle_thread(每个CPU核心拥有一个idle_thread,当调度器的就绪队列中没有等待线程时会切换到idle_thread运行),然后会初始化`current_threads`数组,最后调用`struct sched_ops rr`中定义的sched_init函数,即`rr_sched_init`。 +> [!CODING] 练习题 1 +> 在 kernel/sched/policy_rr.c 中完善 `rr_sched_init` 函数,对 `rr_ready_queue_meta` 进行初始化。在完成填写之后,你可以看到输出“Scheduler metadata is successfully initialized!”并通过 Scheduler metadata initialization 测试点。 + +> [!HINT] Tip +> sched_init 只会在主 CPU 初始化时调用,因此 rr_sched_init 需要对每个 CPU 核心的就绪队列都进行初始化。 + +## 调度队列入队 +内核初始化过程结束之后会调用`create_root_thread`来创建第一个用户态进程及线程,在`create_root_thread`最后会调用`sched_enqueue`函数将创建的线程加入调度队列之中。`sched_enqueue` +最终会调用kernel/sched/policy_rr.c中定义的`rr_sched_enqueue`函数。该函数首先挑选合适的CPU核心的就绪队列(考虑线程是否绑核以及各个CPU核心之间的负载均衡),然后调用`__rr_sched_enqueue`将线程插入到选中的就绪队列中。 +> [!CODING] 练习 2 +> 在 kernel/sched/policy_rr.c 中完善 `__rr_sched_enqueue` 函数,将`thread`插入到`cpuid`对应的就绪队列中。 + +> [!SUCCESS] +> 在完成填写之后,你可以看到输出“Successfully enqueue root thread”并通过 Schedule Enqueue 测试点。 + +## 调度队列出队 +内核初始化过程结束并调用`create_root_thread`创建好第一个用户态进程及线程之后,在第一次进入用户态之前,会调用`sched`函数来挑选要返回到用户态运行的线程(虽然此时就绪队列中只有root thread一个线程)。`sched`最终会调用kernel/sched/policy_rr.c中定义的`rr_sched`函数。 +该调度函数的操作非常直观,就是将现在正在运行的线程重新加入调度器的就绪队列当中,并从就绪队列中挑选出一个新的线程运行。 +由于内核刚刚完成初始化,我们还没有设置过`current_thread`,所以`rr_sched`函数中`old`为`NULL`,后面的练习中我们会考虑`old`不为`NULL`的情况。紧接着`rr_sched`会调用`rr_sched_choose_thread`函数挑选出下一个运行的线程,并切换到该线程。 + +`rr_sched_choose_thread`内部会调用`find_runnable_thread`从当前CPU核心的就绪队列中选取一个可以运行的线程并调用`__rr_sched_dequeue`将其从就绪队列中移除。 +> [!CODING] 练习 3 +> 在 kernel/sched/sched.c 中完善 `find_runnable_thread` 函数,在就绪队列中找到第一个满足运行条件的线程并返回。 在 kernel/sched/policy_rr.c 中完善 `__rr_sched_dequeue` 函数,将被选中的线程从就绪队列中移除。 + +> [!SUCCESS] +> 在完成填写之后,运行 ChCore 将可以成功进入用户态,你可以看到输出“Enter Procmgr Root thread (userspace)”并通过 Schedule Enqueue 测试点。 + +## 协作式调度 +顾名思义,协作式调度需要线程主动放弃CPU。为了实现该功能,我们提供了`sys_yield`这一个系统调用(syscall)。该syscall可以主动放弃当前CPU核心,并调用上述的`sched`接口完成调度器的调度工作。kernel/sched/policy_rr.c中定义的`rr_sched`函数中,如果当前运行线程的状态为`TS_RUNNING`,即还处于可以运行的状态,我们应该将其重新加入到就绪队列当中,这样该线程在之后才可以被再次调度执行。 + +> [!CODING] 练习 4 +> 在kernel/sched/sched.c中完善系统调用`sys_yield`,使用户态程序可以主动让出CPU核心触发线程调度。 +> 此外,请在kernel/sched/policy_rr.c 中完善`rr_sched`函数,将当前运行的线程重新加入调度队列中。 + +> [!SUCCESS] +> 在完成填写之后,运行 ChCore 将可以成功进入用户态并创建两个线程交替执行,你可以看到输出“Cooperative Schedluing Test Done!”并通过 Cooperative Schedluing 测试点。 + +## 抢占式调度 + +使用刚刚实现的协作式调度器,ChCore能够在线程主动调用`sys_yield`系统调用让出CPU核心的情况下调度线程。然而,若用户线程不想放弃对CPU核心的占据,内核便只能让用户线程继续执行,而无法强制用户线程中止。 因此,在这一部分中,本实验将实现抢占式调度,以帮助内核定期重新获得对CPU核心的控制权。 + +ChCore启动的第一个用户态线程(执行user/system-services/system-servers/procmgr/procmgr.c的`main`函数)将创建一个“自旋线程”,该线程在获得CPU核心的控制权后便会执行无限循环,进而导致无论是该程序的主线程还是ChCore内核都无法重新获得CPU核心的控制权。就保护系统免受用户程序中的错误或恶意代码影响而言,这一情况显然并不理想,任何用户应用线程均可以如该“自旋线程”一样,通过进入无限循环来永久“霸占”整个CPU核心。 + +为了处理“自旋线程”的问题,ChCore内核必须具有强行中断一个正在运行的线程并夺回对CPU核心的控制权的能力,为此我们必须扩展ChCore以支持处理来自物理时钟的外部硬件中断。 + +**物理时钟初始化** + +本部分我们将通过配置ARM提供的Generic Timer来使能物理时钟并使其以固定的频率发起中断。 +我们需要处理的系统寄存器如下([Refer](https://developer.arm.com/documentation/102379/0101/The-processor-timers)): +* CNTPCT_EL0: 它的值代表了当前的 system count。 +* CNTFRQ_EL0: 它的值代表了物理时钟运行的频率,即每秒钟 system count 会增加多少。 +* CNTP_CVAL_EL0: 是一个64位寄存器,操作系统可以向该寄存器写入一个值,当 system count 达到或超过该值时,物理时钟会触发中断。 +* CNTP_TVAL_EL0: 是一个32位寄存器,操作系统可以写入 TVAL,处理器会在内部读取当前的系统计数,加上写入的值,然后填充 CVAL。 +* CNTP_CTL_EL0: 物理时钟的控制寄存器,第0位ENABLE控制时钟是否开启,1代表enble,0代表disable;第1位IMASK代表是否屏蔽时钟中断,0代表不屏蔽,1代表屏蔽。 + +对物理时钟进行初始化的代码位于kernel/arch/aarch64/plat/raspi3/irq/timer.c的`plat_timer_init`函数。 + +> [!CODING] 练习 5 +> 请根据代码中的注释在kernel/arch/aarch64/plat/raspi3/irq/timer.c中完善`plat_timer_init`函数,初始化物理时钟。需要完成的步骤有: +> * 读取 CNTFRQ_EL0 寄存器,为全局变量 cntp_freq 赋值。 +> * 根据 TICK_MS(由ChCore决定的时钟中断的时间间隔,以ms为单位,ChCore默认每10ms触发一次时钟中断)和cntfrq_el0 (即物理时钟的频率)计算每两次时钟中断之间 system count 的增长量,将其赋值给 cntp_tval 全局变量,并将 cntp_tval 写入 CNTP_TVAL_EL0 寄存器! +> * 根据上述说明配置控制寄存器CNTP_CTL_EL0。 + +> [!HINT] +> 由于启用了时钟中断,但目前还没有对中断进行处理,所以会影响评分脚本的评分,你可以通过运行ChCore观察是否有`"[TEST] Physical Timer was successfully initialized!: OK"`输出来判断是否正确对物理时钟进行初始化。 + +**物理时钟中断与抢占** + +我们在lab3中已经为ChCore配置过异常向量表(kernel/arch/aarch64/irq/irq_entry.S),当收到来自物理时钟的外部中断时,内核会进入`handle_irq`中断处理函数,该函数会调用平台相关的`plat_handle_irq`来进行中断处理。`plat_handle_irq`内部如果判断中断源为物理时钟,则调用`handle_timer_irq`。 + +ChCore记录每个线程所拥有的时间片(`thread->thread_ctx->sc->budget`),为了能够让线程之间轮转运行,我们应当在处理时钟中断时递减当前运行线程的时间片,并在当前运行线程的时间片耗尽时进行调度,选取新的线程运行。 + +> [!CODING] 练习 6 +> 请在kernel/arch/aarch64/plat/raspi3/irq/irq.c中完善`plat_handle_irq`函数,当中断号irq为INT_SRC_TIMER1(代表中断源为物理时钟)时调用`handle_timer_irq`并返回。 请在kernel/irq/irq.c中完善`handle_timer_irq`函数,递减当前运行线程的时间片budget,并调用sched函数触发调度。 请在kernel/sched/policy_rr.c中完善`rr_sched`函数,在将当前运行线程重新加入就绪队列之前,恢复其调度时间片budget为DEFAULT_BUDGET。 + +> [!SUCCESS] +> 在完成填写之后,运行 ChCore 将可以成功进入用户态并打断创建的“自旋线程”让内核和主线程可以拿回CPU核心的控制权,你可以看到输出`"Hello, I am thread 3. I'm spinning."`和`“Thread 1 successfully regains the control!”`并通过 `Preemptive Scheduling` 测试点。 + +> [!IMPORTANT] +> 以上为Lab4 Part2的所有内容 diff --git a/Pages/Lab5.md b/Pages/Lab5.md new file mode 100644 index 00000000..5c2021a3 --- /dev/null +++ b/Pages/Lab5.md @@ -0,0 +1,8 @@ +# Lab 5:虚拟文件系统 + +虚拟文件系统(Virtual File System,VFS)提供了一个抽象层,使得不同类型的文件系统可以在应用程序层面以统一的方式进行访问。这个抽象层隐藏了不同文件系统之间的差异,使得应用程序和系统内核可以以一致的方式访问各种不同类型的文件系统,如 ext4、tmpfs、 FAT32 等。在 ChCore 中,我们通过 FSM 系统服务以及 FS_Base 文件系统 wrapper 将不同的文件系统整合起来,给运行在 ChCore 上的应用提供了统一的抽象。 + +> [!CODING] 练习1 +> 阅读 `user/chcore-libc/libchcore/porting/overrides/src/chcore-port/file.c` 的 `chcore_openat` 函数,分析 ChCore 是如何处理 `openat` 系统调用的,关注 IPC 的调用过程以及 IPC 请求的内容。 + +Lab5 的所有代码都运行在用户态,不同应用间通过 IPC 进行通信,可调用的 IPC 相关函数定义在 `user/chcore-libc/libchcore/porting/overrides/include/chcore/ipc.h`。 diff --git a/Pages/Lab5/FSM.md b/Pages/Lab5/FSM.md new file mode 100644 index 00000000..ad5c9f0b --- /dev/null +++ b/Pages/Lab5/FSM.md @@ -0,0 +1,58 @@ +# FSM + + + +只要实现了 FSBase 和 FSWrapper 的接口的 IPC 服务,都可以成为一个文件系统示例。FSM 负责管理文件系统,为用户态建立文件系统连接并创建 IPC 的客户端,由于文件系统与其挂载点密切相关,所以 FSM 会处理以下类型的请求: + +```C +/* Client send fsm_req to FSM */ +enum fsm_req_type { + FSM_REQ_UNDEFINED = 0, + + FSM_REQ_PARSE_PATH, + FSM_REQ_MOUNT, + FSM_REQ_UMOUNT, + + FSM_REQ_SYNC, +}; +``` + +当 FSM 收到 Client 的FSM_REQ_MOUNT 类型的请求时,其会执行挂载文件系统的操作,**增加挂载的文件系统数量**,创建对应的 `mount_info_node` 添加到挂载信息表中,直至最后**与文件系统建立 IPC 连接**,并将创建完的 IPC 客户端保存在挂载信息中,总而言之,FSM 仅负责挂载和文件系统同步有关的工作,剩下的其他功能由每个具体的FS服务进行处理。 + +```c +struct mount_point_info_node { + cap_t fs_cap; + char path[MAX_MOUNT_POINT_LEN + 1]; + int path_len; + ipc_struct_t *_fs_ipc_struct; // fs_client + int refcnt; + struct list_head node; +}; + +``` + +> [!CODING] 练习题 2 +> 实现 `user/system-services/system-servers/fsm/fsm.c` 的 `fsm_mount_fs` 函数。 + + +> [!HINT] 提示: +> 你应当回顾Lab4的代码以查看ChCore是怎么基于IPC服务的cap来创建并维护连接的。 + +当 FSM 收到 Client 的 FSM_REQ_PARSE_PATH 类型的请求时,其首先会尝试解析 IPC 请求中访问文件的路径,通过遍历挂载信息链表,找到对应的最匹配的文件系统以及其挂载点路径。通过匹配的文件系统,获取到该文件系统的客户端 cap。如果 Client 已经获取到了文件系统的 cap,则直接返回解析后的`挂载点路径`;否则 FSM 会把挂载路径以及其对应的文件系统的 cap 也一并返回给 Client,并记录该 Client 已获取的文件系统 cap 的信息(FSM 会记录所有已经发送给某个 Client 的文件系统的 cap,见 `user/system-services/system-servers/fsm/fsm_client_cap.h`)。 + +> [!CODING] 练习题 3 +> 实现 `user/system-services/system-servers/fsm/fsm.c` 的 IPC 请求处理函数。 + + +> [!HINT] 提示: +> * 完成 `user/system-services/system-servers/fsm/fsm_client_cap.c` 中的相关函数。 +> * 所有关于挂载点有关的helper函数都在 `user/system-services/system-servers/fsm/mount_info.c` +> * IPC handler 返回的 IPC msg 的数据类型为 `struct fsm_request`,其有关的含义在 `user/chcore-libc/libchcore/porting/overrides/include/chcore-internal/fs_defs.h` 有详细的解释。 +> * 使用 `user/system-services/system-servers/fsm/mount_info.h` 定义的函数来帮助你实现 IPC handler。 +> * 你应当回顾 Lab4 代码以查看 ChCore 是怎么将 cap 对象在进程间收发的,以及 ChCore 中是怎么使用共享内存完成 IPC 调用的。 +> * 由于 printf 并不经过FS所以你可以放心使用。 + +我们提供了所有需要实现的文件的 Obj 版本,你可以修改 CMakeLists.txt,将编译所需的源文件从未实现的 C 文件替换为包含了正确实现的 Obj 文件,以此验证某一部分练习的正确性。如果你需要调试某一个部分,你可以将 `scripts/grade/cmakelists` 下的CMakeLists对应复制到 `FSM` 以及 `FS_Base` 的目录下覆盖并重新编译,运行 `make qemu` 后你就可以查看到 printf 的调试信息。 + +> [!SUCCESS] +> 完成第一部分后,执行 `make grade`,可以得到 `Scores: 20/100`。 \ No newline at end of file diff --git a/Pages/Lab5/base.md b/Pages/Lab5/base.md new file mode 100644 index 00000000..53ea2525 --- /dev/null +++ b/Pages/Lab5/base.md @@ -0,0 +1,85 @@ +# FS_Base + + + +在 ChCore 中,FS_Base 是文件系统的一层 wrapper,IPC 请求首先被 FS_Base 接收,再由 FS_Base 调用实际的文件系统进行处理。 + +## vnode + +在 FS_Base wrapper 中,ChCore 实现了 vnode 抽象,为文件系统中的对象(如文件、目录、符号链接等)提供一个统一的表示方式。 + +ChCore 中 vnode 的定义为: +```C +struct fs_vnode { + ino_t vnode_id; /* identifier */ + struct rb_node node; /* rbtree node */ + + enum fs_vnode_type type; /* regular or directory */ + int refcnt; /* reference count */ + off_t size; /* file size or directory entry number */ + struct page_cache_entity_of_inode *page_cache; + cap_t pmo_cap; /* fmap fault is handled by this */ + void *private; + + pthread_rwlock_t rwlock; /* vnode rwlock */ +}; +``` + +其中,`private` 表示文件系统特定的私有数据,例如对 inode 的引用,refcnt 代表该 vnode 被引用的次数,在下文的 `server_entry` 中会提到。 + +> [!CODING] 练习4 +> 实现 `user/system-services/system-servers/fs_base/fs_vnode.c` 中 vnode 的 `alloc_fs_vnode`、`get_fs_vnode_by_id`、`inc_ref_fs_vnode`、`dec_ref_fs_vnode` 函数。 + +> [!HINT] Tip +> +> - 你可能需要回顾Lab2中的代码去了解红黑树的操作方法。 + +> [!SUCCESS] +> 完成练习4后,执行 `make grade`,可以得到 `Scores: 35/100`。 + +## server_entry + +文件描述符(File Descriptor,简称 fd)是操作系统用于管理文件和其他输入/输出资源(如管道、网络连接等)的一种抽象标识符。我们来回顾一下计算机系统基础课中学习的unix文件系统抽象。在类 Unix 系统(如 Linux、macOS)中,文件描述符是一个非负整数,它指向一个内核中的文件表项,每个表项包含了文件的各种状态信息和操作方法。ChCore 将进程的 fd 保存在 chcore-libc 当中,同时在文件系统中通过 server_entry 维护了各个 Client 的 fd 的信息,把各个 Client 的 fd 和在文件系统侧的 fid 对应起来((client_badge, fd) -> fid(server_entry)),也就是说 server_entry 对应着每个文件系统实例所对应的文件表项,其包含了对应文件表项的文件 offset 以及 `vnode` 引用。由于一个 `vnode` 可能会对应多个文件表项,所以 `vnode` 的引用数需要进行维护。 + +FS_Base 的 IPC handler 在处理 IPC 请求时,会先把 IPC 消息中包含的文件 fd 转换为 fid,所以我们需要把进程的 fd 和实际所对应的文件表项的映射建立起来,而在 ChCore 中对应的就是 `server_mapping` 链表。每当处理 IPC 请求时,文件系统都会通过进程发起的 badge 号找到与之对应的映射表,最终得到文件表项的 ID。 + +> [!CODING] 练习5 +> 实现 `user/system-services/system-servers/fs_base/fs_wrapper.c` 中的 `fs_wrapper_set_server_entry` 和 `fs_wrapper_get_server_entry` 函数。 + +> [!HINT] Tip +> +> - 通过全局变量 `struct list_head server_entry_mapping` 遍历 `server_entry_node`。 +> - 你可以参考 `fs_wrapper_clear_server_entry` 来理解每一个变量的含义。 + +> [!SUCCESS] +> 完成练习5后,执行 `make grade`,可以得到 `Scores: 50/100`。 + +## fs_wrapper_ops + +当我们拥有了文件表项和VNode抽象后,我们便可以实现真正的文件系统操作了。 + +我们可以将 FS_Base 以及 FS_Wrapper 的所有逻辑看成一个 VFS 的通用接口,其暴露出的接口定义为 `strcut fs_server_ops`。对于每一个文件系统实例,其都需要定义一个全局的名为 `server_ops` 的全局句柄,并将实际的文件系统操作的实现注册到该句柄中。你可以通过查看 `user/system-services/system-servers/tmpfs/tmpfs.c` 中查看 ChCore 的默认 `tmpfs` 文件系统是怎么将其注册到 `FS_Wrapper` 中的。而到了实际处理文件请求时,上层的 FS_Wrapper 在响应 IPC 请求的时候,只需要调用 `server_ops` 中的函数指针即可,不需要实际真正调用每一个文件系统实现的操作函数, 这样便完成了一个统一的文件操作逻辑。例如在 `tmpfs` 中实际的读命令为 `tmpfs_read` 但在上层的 `fs_wrapper` 看来其调用只需要调用 `server_ops->read` 即可而不需要真正知晓 `tmpfs` 中的函数签名。 + +对于本 Lab 你只需要实现最基本的 Posix 文件操作即可,即 Open,Close,Read, Write 以及 LSeek 操作。而其下层每个文件系统除了 `Open` 操作,每当 FS_Base 尝试处理 Posix 文件请求时,其都会调用 `translate_fd_to_fid` 将对应的 `fd` 翻译成 `fid` 并重新写回 `struct fs_request` 中的 `fd`,所以请注意**不需要在实际的fs_wrapper_函数中再次调用该函数**。下面将简述一下每一个函数的语义。 + +对于 Open 以及 Close 来说,其主要的目的就是创建以及回收 Server Entry 即文件表项。由于在 VFS 中 VNode 的创建是动态的,所以当进程尝试发出 `Open` 中,我们需要调用与之对应的 `server_ops` 并同时分配对应的文件表项。对于每一个新增的文件表项,我们需要将其关联到对应的内存 `VNode` 中。由于文件表项所对应的 `VNode` 可能不在内存中,所以当文件系统返回 inode 号时我们需要尝试查找相应的 `vnode`,如果不存在则尝试分配并将其添加至对应的红黑树中。当完成 `VNode` 关联后,我们需要使用上一步实现的映射函数,将 `server_entry` 与用户 `fd` 映射,完成文件表项的创建。对于 `Close`,我们需要采取类似的逻辑,即回退所有的文件表项操作,减少引用计数,并尝试回收对应的系统资源。 + +针对 `Read/Write/Lseek` 操作,你需要参考 `man` 以及对应的 `tests/fs_test` 下的所有测试文件,按照 `Posix` 语义相应地维护 `server_entry` 以及 `vnode` 信息,并将数据返回给用户进程。 + +针对 mmap 操作,我们知道针对文件的 mmap 操作是采取 Demand Paging 的内存映射来实现的,当用户进程调用 `mmap` 时,FS 会首先为用户新增一个 `pmo` 即内存对象,并将其对应的类型设置为 `PMO_FILE`,并为其创建 `Page_Fault` 映射(`user/system-services/system-servers/fs_base/fs_page_fault.c`),最后将该 `pmo` 对象发回用户进程并让其进行映射。当用户尝试访问该内存对象,并发生缺页异常时,内核会根据 pmo 的所有者(badge)将异常地址调用到对应FS处理函数进行处理,处理函数为每一个文件系统中的 `user_fault_handler`,此时 FS 服务器会根据缺页地址分配新的内存页,填充文件内容并完成缺页的处理,最终返回内核态,从而递交控制权到原来的用户进程。 + +> [!CODING] 练习6 +> 实现 `user/system-services/system-servers/fs_base/fs_wrapper_ops.c` 中的 `fs_wrapper_open`、`fs_wrapper_close`、`fs_wrapper_read`、`fs_wrapper_pread`、`fs_wrapper_write`、`fs_wrapper_pwrite`、`fs_wrapper_lseek`、`fs_wrapper_fmap` 函数。 + +> [!HINT] Tip +> * `user/chcore-libc/libchcore/porting/overrides/include/chcore-internal/fs_defs.h` 中定义了 `struct fs_request`,其中定义了文件系统收到的 IPC 信息所包含的数据。 +> * 针对文件表项的helper函数如 `alloc_entry` 和 `free_entry` 在 `user/system-services/system-servers/fs_base/fs_vnode.c` 中定义。 +> * `user/system-services/system-servers/tmpfs/tmpfs.c` 中定义了 tmpfs 文件系统提供的文件操作接口 server_ops,fs_wrapper 接口会调用到 server_ops 进行实际的文件操作。 +> * 用户态的所有针对文件的请求,首先会被路由到 `user/chcore-libc/libchcore/porting/overrides/src/chcore-port/file.c` 中,该文件包含了在调用 `ipc` 前后的预备和收尾工作。 +> * 你应当回顾 Lab2 的代码,去了解针对 PMO_FILE,内核是怎么处理缺页并将其转发到FS中的。同时你需要查看 `user/system-services/system-servers/fs_base/fs_page_fault.c` 中的 `page_fault` 处理函数,了解 FS 是如何处理 mmap 缺页异常的。 + +> [!SUCCESS] +> 完成练习6后,执行 `make grade`,可以得到 `Scores: 100/100`。 + +> [!QUESTION] 练习7 +> 思考 ChCore 当前实现 VFS 的方式有何利弊?如果让你在微内核操作系统上实现 VFS 抽象,你会如何实现? diff --git a/Pages/SUMMARY.md b/Pages/SUMMARY.md index 545b60ae..f43f8167 100644 --- a/Pages/SUMMARY.md +++ b/Pages/SUMMARY.md @@ -22,6 +22,15 @@ - [页表管理](./Lab2/pte.md) - [缺页管理](./Lab2/page_fault.md) +- [Lab4: 多核调度与IPC](./Lab4.md) + - [多核支持](./Lab4/multicore.md) + - [多核调度](./Lab4/scheduler.md) + - [进程间通信(IPC)](./Lab4/IPC.md) + - [实机运行与IPC性能优化](./Lab4/performance.md) + +- [Lab5: 虚拟文件系统](./Lab5.md) + - [FSM](./Lab5/FSM.md) + - [FS_Base](./Lab5/base.md) - [附录](./Appendix.md) - [Bomb: 工具教程](./Appendix/toolchains.md)