Skip to content

Lab 3: Process (User Part)

字数
3405 字
阅读时间
14 分钟

负责助教:孔令宇

在本实验中,我们将完善 Lab2 中实现的进程概念,在系统中运行简单的用户态进程,同时利用系统调用实现用户态程序主动向内核态转换的功能。此外,用户态程序需要能够被均匀调度。

1. 服务器操作

运行以下命令进行代码的拉取与合并

shell
# 拉取远端仓库
git fetch --all

# 提交你的更改
git add .
git commit -m "your commit message"

# 切换到新lab的分支
git checkout lab3

# 新建一个分支,用于开发
git checkout -b lab3-dev

# 引入你在上个lab的更改
git merge lab2-dev

如果合并发生冲突,请参考错误信息自行解决。

2. Codebase 更新说明

  • 更新了信号量的规则,将信号量的锁定和解锁剥离出来,便于借助信号量实现更丰富的同步功能。详见 common/sem.ccommon/sem.h 。这个改动不影响你原有的代码,但是你依然需要注意一些使用信号量和锁带来的并发问题。
  • 在通过 Lab2 的测试后,请将activate_proc中 「若p->state==ZOMBIE则panic」 的规则去除(如果你是这样写的话)。如果p->state==ZOMBIE,请不做任何操作,并返回 false。这项改动有助于你编写kill函数。
  • 在时钟中断上抽象出了 CPU 定时器的概念。详见 kernel/cpu.ckernel/cpu.h
  • sched 函数中添加了 attach_pgdir(&next->pgdir) ,用于在进入用户态时设置页表。
  • 修复了 common/list.cqueue_push 没有增加 x->sz 的问题。

3. 页表

AArch64 将 64 位虚拟地址分为 0xffff 开头的高地址和 0x0000 开头的低地址两部分。在我们的实验中,内核代码使用的内存被映射到高地址,用户代码使用的内存被映射到低地址。

高地址的页表基地址寄存器为ttbr1,低地址的页表基地址寄存器为ttbr0。这两个寄存器都只能在内核模式下访问,他们保存相应页表的物理地址。在我们的实验中,默认使用 4KB 页大小的 4 级页表。你可以参考ARM Manual 了解此类页表的具体结构,页表中的大多数特性我们的实验并没有用到,但了解详细情况有助于你后续扩展功能。

IMPORTANT

任务1

完成页表配置的相关代码。我们在Proc中添加了pgdir项,存储进程的用户态内存空间的相关信息。pgdir是定义在pt.h中的结构体,pgdir.pt指向进程的用户页表。如果进程是像 Lab2 一样运行在内核态中,则pgdir.pt可以为空。

  • 完成pt.c中的get_pte函数。该函数遍历给定的pgdir,从中找到对应于虚拟地址va的页表项,并返回指向页表项的指针。如果页表项不存在(即其所在的页表未创建),若 alloc 标记为 true,则创建和配置页表项所在的页表及其上级页表(如果上级页表也不存在),然后返回有效的页表项指针;否则返回 NULL。注意:返回的指针指向的页表项可以是无效的。请注意区分页表项和页表项所描述的物理页。
  • 完成pt.c中的free_pgdir函数。该函数释放给定的pgdir。请注意只要释放页表本身所占的空间,不要释放页表所引用的物理页。(页表所引用的物理页目前由测试代码直接管理。在后续实验中,我们会逐渐完成相关的代码,本次实验只要求大家完成页表本身的操作。)
  • 你可能需要在proc.cinit_proc中加入init_pgdir,在exit中加入free_pgdir

4. 系统调用

大多数系统级别的操作需要在内核态执行,如设备IO或者进程间通信。当用户态程序要执行这些操作时,就需要系统调用(system call)。系统调用提供用户程序与操作系统之间的接口,允许运行在用户空间的程序向操作系统内核请求需要更高权限运行的服务。

系统调用类似于一种特殊的异常,用户程序执行系统调用指令(svc)后,CPU 会将其当作一个 trap 跳转到内核态处理。内核在trap_global_handler中识别出类型,然后执行相应操作。

我们的实验采用通用的系统调用约定:x8 寄存器存放请求的系统调用 id,x0-x5 寄存器存放系统调用的六个参数。系统调用返回时,设置 x0 寄存器为系统调用的返回值。

IMPORTANT

任务2

完成系统调用的相关代码。我们在trap.c中已经提供了在系统调用时调用syscall_entry的代码,你需要完成syscall.c中的syscall_entry函数。该函数传入 UserContext 作为参数,请从中提取出相应的寄存器,查询syscall_table,执行指定的系统调用,并将系统调用的返回值保存到指定的寄存器。

IMPORTANT

任务3

修改你的 UserContext。你需要保证 UserContext 中有 spsr、elr、sp(sp_el0)寄存器,并结合你的 UserContext 添加 test/user_proc.c 中的 TODO 部分。

TIP

思考:spsr、elr、sp(sp_el0)寄存器的作用都是什么?

5. 使用kill方法结束进程

在内核代码中,kill系统调用主要在以下场景使用:

  1. 系统调用处理:用户程序通过kill()系统调用请求终止指定进程
  2. 异常恢复:当进程执行中发生严重错误(如访问非法内存、除零错误)时,内核通过kill终止异常进程
  3. 资源限制:进程超出资源限制(如CPU时间限制、内存使用上限)时,内核自动调用kill终止进程
  4. 信号处理:接收到终止信号(如SIGKILL、SIGTERM)时,在信号处理函数中调用kill终止进程
  5. 调试支持:内核调试器或ptrace机制需要终止被调试进程时使用kill
  6. 进程组操作:终止整个进程组时,遍历进程树对每个成员进程调用kill
  7. 系统清理:系统关机或重启时,内核按特定顺序调用kill终止所有用户进程

kill 机制通过设置进程的killed标记实现延迟终止:标记设置后,进程在下次返回用户态时检查标记并调用exit()退出,避免了在内核态直接终止可能导致的资源管理问题。

在本 lab 中,我们先实现 kill 函数。

IMPORTANT

任务4

完成结束进程相关的代码。我们在proc.c中添加了一个函数kill。结束进程的逻辑参考了 xv6 和 Linux 的设计,调用 kill 会设置指定进程的 killed 标记,并唤醒进程,阻止进程睡眠。进程在返回用户态时会检查 killed 标记,若有则调用 exit 退出。

  • 实现proc.c中的kill函数。该函数遍历进程树,搜索指定 PID 且状态不为 UNUSED (可参考sched.c中给出的is_unused,访问调度信息时加锁是个好习惯,但这里其实不加锁也没啥事)的进程,如果进程不存在,返回 -1。对于找到的进程,设置struct prockilled标记,并调用activate_proc唤醒进程。完成之后,返回 0。请注意使用进程树的锁。
  • 修改你的sched代码,使其能够保证,如果当前进程带有 killed 标记,且 new state 不为 ZOMBIE,则调度器直接返回,不做任何操作。(为什么?)
  • aarch64/trap.ctrap_global_handler函数的末尾加上检查,如果当前进程有 killed 标记且即将返回到用户态,则调用exit(-1)

6. 调度算法

在本 Lab 中,我们还将实现一个更实用的调度算法。

调度的意义

进程调度是操作系统中的核心功能,其主要意义包括:

  1. 提高CPU利用率:通过合理分配CPU时间,确保CPU始终处于忙碌状态,避免浪费计算资源
  2. 提高系统吞吐量:合理调度可以使更多进程在单位时间内完成,提高系统的整体处理能力
  3. 减少响应时间:对于交互式应用,通过适当调度可以减少用户等待时间,提升用户体验
  4. 提供公平性:确保所有进程都能获得合理的CPU时间,避免某些进程长时间占用CPU
  5. 支持多任务并发:允许多个进程看似同时运行,实现多任务处理

常见调度算法

操作系统中常见的调度算法包括:

  1. 先来先服务 (First Come First Served, FCFS):按照进程到达的顺序进行调度,简单但可能导致短进程等待长进程
  2. 时间片轮转 (Round Robin, RR):每个进程获得固定时间片,时间片用完后切换到下一个进程,实现公平调度
  3. 完全公平调度 (Completely Fair Scheduler, CFS):Linux 内核使用的现代调度算法,通过虚拟运行时间(VRUNTIME)实现公平性。每个进程获得与其权重成比例的 CPU 时间,使用红黑树维护进程队列,选择 VRUNTIME 最小的进程运行。CFS 通过动态调整时间片确保所有进程都能获得公平的 CPU 份额,同时考虑进程优先级和 nice 值。

IMPORTANT

任务5

改进你的调度器。Lab2 的内核进程调度是非抢占式的,Lab3 要求大家进行抢占式的调度。请注意调度的公平性问题,选择一个合适的调度算法。你可能需要在调度器中加入时钟中断相关的代码。另请注意:我们现在在时钟中断的基础上封装了一层 CPU 定时器的抽象,请使用CPU定时器

我们在user_proc.c中编写了用户页表和用户进程相关的测试代码,在cpu.c中通过 CPU 定时器添加了 CPU 定时输出消息的代码。如果一切正常,你将看到vm_test PASSuser_proc_test PASS

测试还会输出 4 个 CPU 和 22 个进程的工作量,请确认 CPU 和进程间的相对工作量是否分别基本平衡。

7. PID 回收

进程会频繁地创建和退出,简单地使用一个自增的全局 int 来管理 PID 是不合理的。更好的办法是使用 Bitmap 等支持快速标记使用、回收的数据结构。

IMPORTANT

任务6

改进你的 PID 分配方式,要求支持进程退出后 PID 的回收利用。

8. 提交

提交方式:将实验报告提交到 elearning 上,格式为 学号-lab3.pdf

注意:从lab1开始,用于评分的代码以实验报告提交时为准。如果需要使用新的代码版本,请重新提交实验报告。

截止时间11月9日23:59

DANGER

逾期提交将扣除部分分数

计算方式为 scorefinal=score(1n20%),其中 n 为迟交天数,不满一天按一天计算)。

报告中可以包括下面内容

  • 代码运行效果展示

  • 实现思路和创新点

  • 对后续实验的建议

  • 其他任何你想写的内容

    你甚至可以再放一只可爱猫猫

报告中不应有大段代码的复制。如有使用本地环境进行实验的同学,请联系助教提交代码(最好可以给个git仓库)。使用服务器进行实验的同学,助教会在服务器上检查,不需要另外提交代码。

在服务器上操作的同学,此次实验完成后请提交(或者说创建一个新分支)到 lab3-submission 分支,助教会使用你在此分支上提交记录来批作业。如果此分支最后提交时间晚于实验报告提交时间,助教会选择此分支上在实验报告提交时间前的最后一个提交作为批改代码。

提交操作

shell
# 提交最后的代码
git add .
git commit -m "your final commit message"

# 新建一个分支,用于提交
git checkout -b lab3-submission
The avatar of contributor named as tangxiaoxia0336 tangxiaoxia0336
The avatar of contributor named as kooWZ kooWZ