跳转至

实验一:NexOS 用户态编程与系统调用

实验说明

本实验基于 NexOS 源码,要求同学完成三个部分的功能实现,覆盖从用户态程序执行、Shell 命令解释到系统调用打通的完整链路。

实现范围

  • 第一部分:实现 runscript 命令执行脚本(不要求支持内置命令)。
  • 第二部分:在 sh.c 中实现重定向多指令(;
  • 第三部分:添加 ioctl 系统调用并打通调用路径,同时新增 gpu_stats.c,通过 ioctl 获取 GPU 矩阵乘次数并按指定格式输出。

总分说明

本实验总分 20 分,按比例折算计入最终成绩:

  • 代码测试结果:按 70% 比例计入总分(最高分折算为 14 分
  • 线下提问检查:按 30% 比例计入总分(最高分折算为 6 分

请严格按照“功能要求 + 评分点”完成,未通过的功能点会影响代码测试得分。

测试平台代码提交说明

本实验采用希冀平台作为测试平台,网址为https://cscourse.ustc.edu.cn

在在线作业lab1的代码提交界面,需要粘贴你自己的gitlab代码仓库链接,由于本次实验所使用的是 lab1 分支的代码,因此需要额外指定需要测评的分支,有如下两种指定的格式:

  • https://cscourse.ustc.edu.cn/vdir/Gitlab/username/yourproj.git --branch=lab1
  • https://cscourse.ustc.edu.cn/vdir/Gitlab/username/yourproj.git lab1

本次实验的平台测试脚本已经经过助教团队的大量测试,但是仍不排除有测试不完善的地方。因此如果你发现线上测试结果与你本地测试的结果有出入,请将你的线上测试结果截图和线下测试截图发给助教,助教会根据情况判断是学生代码问题还是测试脚本的bug。

实验时间安排

注:此处为实验发布时的安排计划,请以课程主页和课程群内最新公告为准

  • 4.2 晚实验课,讲解实验一,检查实验
  • 4.9 晚实验课,检查实验
  • 4.16 晚实验课,检查实验
  • 4.23 晚及之后实验课,补检查实验

补检查分数照常给分,但会记录此次检查未按时完成,此记录在最后综合分数时作为一种参考(即:最终分数可能会低于当前分数)。

检查时间、地点:周四晚18: 30~22: 00,电三楼406/408。


获取实验代码(切换到 lab1 分支)

本实验假设你已经在本机 clone 过课程代码仓库。接下来你需要把远端的 lab1 分支拉取到本地,并切换到该分支进行实验。

推荐操作(已 clone 的本地仓库)

在仓库根目录执行:

# 获取远端最新分支信息
git fetch --all

# 首次切换:创建本地分支并跟踪远端 lab1
git checkout -b lab1 origin/lab1
# 或者(新版 git)
# git switch -c lab1 --track origin/lab1

如果你本地已经有 lab1 分支

直接切换即可:

git checkout lab1
# 或者
# git switch lab1

常见问题

  • 若提示找不到 origin/lab1:先确认远端分支名是否为 lab1(可用 git branch -r 查看)。
  • 切换分支会改变工作区文件;若你在其他分支有未提交改动,请先 git status 检查并自行处理(提交或暂存)。

一、实现 runscript 命令(6 分)

1.1 目标

在用户态新增一个命令 runscript,可从脚本文件中逐行读取命令并执行。
本部分不要求支持内置命令(如 cdexit),仅要求执行外部命令。
助教已在源码目录下 user/runscript.c 中提供了部分辅助函数,请各位同学补全main函数的主流程。

1.2 基础知识

本小节按 runscript 实际用到的系统调用逐个说明:做什么、参数是什么、返回值怎么判定

系统调用 核心功能 参数含义 返回值含义 在本实验中的典型用途
open(const char *path, int omode) 打开文件并分配一个文件描述符 fd path:文件路径;omode:打开模式(如 O_RDONLY >=0:成功,值即 fd-1:失败 打开脚本文件:open(script, O_RDONLY)
read(int fd, void *buf, int n) fd 指向文件读取最多 n 字节到 buf fd:文件描述符;buf:接收缓冲区;n:最多读取字节数 >0:本次实际读取字节数;0:读到 EOF;-1:失败 循环读取脚本内容并按行拆分
close(int fd) 关闭文件描述符,释放进程内的 fd 槽位 fd:要关闭的描述符 0:成功;-1:失败 脚本执行结束后关闭脚本文件
fork(void) 创建当前进程的子进程 无参数 父进程返回子进程 pid(>0);子进程返回 0;失败返回 -1 每条外部命令先 fork 一个子进程
exec(const char *path, char *const argv[]) 用新程序替换当前进程映像 path:可执行文件路径;argv:参数数组, 由用户输入的命令按空格切分得到,最后一个元素必须为 0 成功不返回;返回则表示失败(通常 -1 在子进程中执行目标命令,如 /ls
wait(int *status) 等待任一子进程退出并回收资源 status:用于接收退出码的用户缓冲区地址,可传指针 >=0:返回已退出子进程 pid-1:失败/无子进程 父进程串行等待,保证脚本按顺序执行
exit(int status) 立即终止当前进程并上报退出状态 status:退出码(通常 0 表示成功,非 0 表示失败) 不返回(noreturn 参数错误、打开/读取/执行失败时终止 runscript 或子进程

exit 在第一部分中的使用建议

  • 主流程中遇到不可恢复错误(如脚本打开失败、读取失败),建议 exit(1)
  • 子进程中 exec 失败时必须立即 exit(1),避免子进程落回父流程代码。
  • 正常执行结束使用 exit(0),便于 wait(&status) 在父进程判断执行结果。

1.3 功能要求

请按照下述要求补全 user/runscript.cmain 函数:

  1. 基本调用方式:
    • runscript <script_path>
  2. 脚本读取与逐行执行:
    • 以只读方式打开脚本文件;
    • 脚本不存在时,以错误码1调用exit退出;
    • 按行解析并顺序执行。
  3. 行过滤:
    • 跳过空行;
    • 跳过注释行(例如以 # 开头)。
  4. 外部命令执行:
    • 将行按空白切分为 argv
    • 使用 fork + exec + wait 串行执行每一行命令;
    • 任意一行执行失败时,runscript 应立即退出并返回错误码1。
  5. 错误处理:
    • 当指令执行报错时,以错误码1调用exit退出。

功能要求样例

样例 1:基本按行执行 + 跳过注释/空行

脚本 demo1.sh

# 这是注释
echo start

ls
执行:runscript /demo1.sh
预期:按顺序执行 echo startls;注释行和空行被忽略。

样例 2:最后一行无换行符

脚本内容(末尾没有 \n):

echo only_one_line
执行:runscript /demo2.sh
预期:最后一行依然会被执行,不应被漏掉。

样例 3:错误路径

执行:runscript /not_exist.sh
预期:打印打开失败信息并返回非零错误码。
若脚本中某行命令不存在(如 no_such_cmd),应提示执行失败并返回非零。

样例 4:中间命令失败时的返回行为

脚本 demo_err_mid.sh

echo before
no_such_cmd
echo after
执行:runscript /demo_err_mid.sh
预期:

  • echo before 正常执行;
  • 第二行执行失败后,runscript 立即 exit(1)
  • 第三行 echo after 不再执行。

1.4 评分细则(6 分)

  • 2 分runscript 命令可正常编译、打包到 fs.img
  • 2 分:可按行顺序执行脚本。
  • 1 分:脚本文件路径错误时返回错误码1。
  • 1 分:命令执行错误时返回错误码1。

1.5 提示

  • 助教在 user/runscript.c 中提供了很多功能函数,注意灵活使用。
  • exec 失败只会在子进程返回,记得在子进程中 exit(1)
  • 命令执行错误的处理涉及两处处理:一是 exec 报错返回;二是 exec 执行成功,但指令运行过程中报错退出。
  • 为了方便同学们测试,我们预先创建了一个测试脚本 user/test.sh ,该测试脚本会打包进qemu的fsimg中,你可以自由修改这个文件中的内容来构造测试样例,然后进入qemu执行 runscript test.sh 来进行测试。

1.6 测试方法

建议测试步骤(第一部分)

  • 编译与打包:确认 runscript 已加入用户程序编译与 fs.img 打包列表(能在 shell 中直接运行 runscript)。
  • 基本功能:在 QEMU 内执行 runscript test.sh,确认能逐行执行、跳过注释/空行。
  • 错误路径
    • 助教提供了一个辅助检查返回错误码的用户态程序 rs_status ,该程序会调用你实现的 runscript 并获取其返回值;
    • 执行 rs_status /not_exist.sh:应输出 RS_RC: 1
    • 脚本中间插入不存在命令,然后执行 rs_status test.sh ,应输出 RS_RC: 1

二、在 sh.c 中实现重定向与多指令(6 分)

2.1 目标

user/sh.c 中扩展 shell 解析能力,支持:

  • 使用 ; 分隔的多条命令;
  • 输入输出重定向:<>>>

2.2 基础知识

下面是本实验可能用到的系统调用:

系统调用 核心功能 参数含义 返回值含义 在本实验中的典型用途
open(const char *path, int omode) 打开文件 path:文件路径;omode:打开模式 >=0:新 fd-1:失败 灵活使用不同的模式打开重定向的目标
dup2(int oldfd, int newfd) newfd 指向 oldfd 相同的打开文件 oldfd:已有有效 fdnewfd:要覆盖的目标 fd(常用 0/1 >=0:返回 newfd-1:失败 子进程中执行 dup2(infd, 0)dup2(outfd, 1) 实现重定向
close(int fd) 关闭不再使用的描述符 fd:要关闭的描述符 0:成功;-1:失败 dup2 后关闭临时 fd;父进程也要关闭它持有的重定向 fd

2.2.1 文件打开模式详解(openomode

这些模式常量定义在头文件 kernel/include/fcntl.h(源码中可见具体宏值);用户程序在 sh.c 中通过 #include "fcntl.h" 使用这些常量

打开模式 宏值 含义
O_RDONLY 0x000 只读打开
O_WRONLY 0x001 只写打开
O_RDWR 0x002 读写打开
O_CREATE 0x200 文件不存在时创建
O_TRUNC 0x400 打开时清空原文件内容
O_APPEND 0x800 每次写入追加到文件末尾

2.2.2 多指令与三个重定向符详解

  1. 多指令分隔符 ;

    • 含义:把一行命令切成多个“子命令”,按从左到右顺序依次执行。
    • 典型输入:echo A; echo B; echo C
    • 实现要点:
      • 先按 ; 切分,再对每个子命令做参数解析与执行;
      • 某个子命令失败时,shell 不应退出,后续子命令继续尝试执行。
  2. 输入重定向 <

    • 语义:把文件内容作为标准输入(fd 0)喂给命令。若文件不存在,open 失败并报错。
  3. 覆盖写重定向 >

    • 语义:把标准输出(fd 1)写入目标文件,并从文件开头开始覆盖。若文件已存在,会先清空原内容再写入;若文件不存在,则会创建文件
  4. 追加写重定向 >>

    • 语义:把标准输出追加到目标文件末尾,不覆盖已有内容。若文件不存在,则会创建文件

2.3 功能要求

请按照下述要求,在 user/sh.cTODO 注释标记的地方补全相关功能:

  1. 多指令分割(;):
    • 一行输入可包含多条子命令;
    • 子命令按顺序执行;
    • 单个子命令的执行错误不能干扰其他子命令。
  2. 重定向解析:
    • 在参数列表中识别 <>>> 及其目标文件;
    • 从最终 argv 中移除重定向符号与文件名;
    • 若符号后缺失文件名,应给出提示并安全返回。
  3. 文件描述符重绑定:
    • <:将文件绑定到标准输入(fd 0);
    • >:将文件绑定到标准输出(fd 1,覆盖写);
    • >>:将文件绑定到标准输出(追加写);
    • 在子进程中通过 dup2 实施重定向。
  4. 资源回收与健壮性:
    • 父子进程中不再使用的 fd 要及时关闭;
    • open/fork/wait 失败时应提示错误并不中断 shell 主循环。

功能要求样例

样例 1:多指令 ; 顺序执行

输入:

echo A; echo B; echo C
预期:按顺序输出 ABC,中间不丢命令。
输入 echo A;;echo B 时,应跳过空命令且不崩溃。

样例 2:多指令中存在出错指令

输入:

echo ok1; no_such_cmd; echo ok2
预期: - echo ok1 正常输出; - no_such_cmd 执行失败时打印错误信息,但 shell 不退出; - 后续 echo ok2 仍会继续执行并输出。

样例 3:输出重定向覆盖与追加

输入:

echo hello > out.txt
echo world >> out.txt
cat < out.txt
预期:cat 输出两行,依次为 helloworld
其中 > 覆盖写,>> 追加写。

2.4 评分细则(6 分)

  • 2 分:多指令 ; 能正确解析并顺序执行(含空命令边界处理)。
  • 1 分:错误子指令不能影响其他子指令运行。
  • 1 分:重定向 > 功能正确。
  • 1 分:重定向 < 功能正确。
  • 1 分:重定向 >> 功能正确。

2.5 提示

  • 助教在 user/sh.c 中提供了辅助函数,请注意灵活使用,比如:
    • split_commands():处理 ;
    • process_redirect():处理 </>/>>

2.6 测试方法

建议测试步骤(第二部分)

  • 多指令顺序
    • echo A; echo B; echo C:应依次输出 A/B/C
    • echo ok1; no_such_cmd; echo ok2:应报错但 shell 不退出,ok2 仍会输出。
  • 重定向
    • echo hello > out.txt 后执行 cat < out.txt:应输出 hello
    • echo world >> out.txt 后再 cat < out.txt:应能看到追加效果。

三、添加 ioctl 系统调用与实现 gpu_stats 程序(8 分)

3.1 目标

本部分目标包含两项:

  1. 在 NexOS 中新增并打通 ioctl 系统调用链路:
    用户程序 -> 用户态封装 -> ecall -> 内核 syscall 分发 -> 文件层分发(如 GPU 设备)。
  2. 新增用户态程序 gpu_stats.c
    通过 ioctl 获取 GPU 自启动以来矩阵乘执行次数,并按固定格式输出:gpu matmul_ops: %llu

3.2 基础知识

3.2.1 背景:为什么需要 ioctl

在操作系统里,read/write 这类通用文件接口适合“按字节流读写”的场景,但对 GPU、网卡、显示设备这类复杂设备来说,常常需要“发送控制命令 + 传递结构化参数 + 获取状态结果”。
ioctl(input/output control)正是为这种需求设计的统一控制入口:用户态通过一个系统调用,把“命令字 + 参数地址”交给内核,再由内核转发给具体设备驱动处理。

在大模型推理场景中,这个机制尤其重要:推理程序通常需要频繁与 GPU 交互(例如查询能力、申请资源、提交计算、获取统计信息)。从 OS 视角看,这些动作本质上都属于“设备控制请求”,而 ioctl 就是用户态与 GPU 驱动之间的关键桥梁。

在本实验中,为了简化,助教在根目录虚拟了一个GPU设备,路径为 /gpu 。本实验后续与GPU交互均是和这个虚拟GPU设备进行交互。

ioctl 在本实验中的角色

本实验里,ioctl 承担的核心职责是:把用户程序的 GPU 控制请求安全、统一地送达设备驱动,并把处理结果返回用户态(例如返回矩阵乘累计次数)。

3.2.2 ioctl 端到端流程(用户态 -> 系统调用 -> 设备)

建议把整条链路理解为 5 步:

  1. 用户程序发起调用(用户态)
    用户程序先拿到设备文件描述符 fd(例如本实验需要使用 open() 打开 /gpu),再调用: ioctl(fd, cmd, arg)
  2. 用户态封装触发 ecall(用户库)
    user/syscall.cioctl() 会调用统一的 __syscall
    • a7 = SYS_ioctl
    • a0 = fd, a1 = cmd, a2 = arg
    • 执行 ecall 陷入内核。
  3. 内核系统调用总入口分发(syscall()
    内核根据 trapframe->a7syscalls[],命中 SYS_ioctl 后跳转到 sys_ioctl()
  4. sys_ioctl() 做参数校验并转给文件层
    sys_ioctl() 读取 a0/a1/a2,检查 fdofile[fd],拿到 struct file *f 后,调用: fileioctl(f, cmd, arg)
  5. 文件层按类型路由到具体设备驱动
    fileioctl 根据 f->type 分发(如 FD_GPU -> gpu_ioctl),驱动执行后返回结果,再沿原路返回用户态。

3.2.3 关键接口说明

接口/系统调用 核心功能 参数含义 返回值含义 在本实验中的典型用途
ioctl(int fd, int cmd, uint64 arg)(用户态) 向指定设备/文件发送控制命令 fd:目标设备文件描述符;cmd:控制命令号;arg:命令参数(常为用户缓冲区地址) 由内核具体实现决定;通常 0/非负 成功,-1 失败 用户程序控制设备(例如 GPU)
fileioctl(struct file *f, int cmd, uint64 arg)(内核文件层) f->type 转发控制请求 f:已打开文件对象;cmd:控制命令;arg:附加参数 设备处理结果;不支持类型返回 -1 f->type==FD_GPU 时转发给 gpu_ioctl

如何理解 cmdarg

  • cmd 决定“要做什么动作”(如查询信息、分配显存、执行计算)。
  • arg 是该动作的参数载体,通常是一个结构体在用户态的地址,内核按 cmd 解释其布局。

3.2.4 ioctl 系统调用函数功能逻辑

本小节描述 ioctl 系统调用内部核心处理逻辑,也是添加ioctl系统调用的核心,请认真阅读。

  1. 使用 myproc() 获取进程结构体 struct proc *
  2. sys_ioctl() 读取参数寄存器

    • struct proc * 中的 trapframe 取三个参数:
      • a0 -> fd
      • a1 -> cmd
      • a2 -> arg
  3. sys_ioctl() 做文件描述符合法性检查

    • 检查 fd 是否落在 [0, NOFILE)NOFILE 表示单进程文件描述符上限);
    • 检查失败立即返回 -1
  4. sys_ioctl() 获取结构体 struct file *f

    • 该结构体保存在 struct proc * 中的 ofile 数组中;
    • 该数组中 fd 下标对应的结构体,即是我们需要的;
    • 检查 struct proc * 是否非空,空则返回 -1
  5. 调用 fileioctl() 继续将指令和参数转交给下层分发

    • 我们实现的 sys_ioctl 直接将 fileioctl() 的返回值作为自己的返回值。

本小节涉及的两个关键结构体

  1. struct proc(进程控制块)

    • 作用:描述当前进程的运行上下文与资源。
    • 获取方式:myproc() 的返回值。
    • sys_ioctl 中主要用到:
      • trapframe:保存系统调用参数寄存器(a0/a1/a2)。
      • ofile[]:当前进程打开文件表,ofile[fd] 可取到对应 struct file *
  2. struct file(内核打开文件对象)

    • 作用:抽象“一个已打开的文件/设备”,统一承载文件类型和读写状态。
    • 获取方式:通过进程控制块 struct procofile[] 数组获取。
    • 本实验中作为参数传递给 fileioctl 即可。

这一小节对应的源码定位

  • kernel/core/syscall.csyscall()sys_ioctl()syscalls[] 注册。
  • kernel/core/file.cfileioctl()
  • kernel/drivers/gpu.cgpu_ioctl()(设备侧命令处理示例)。

3.2.5 本实验实现边界(重点)

只需要打通到 fileioctl

对同学来说,sys_ioctl 的核心任务是:正确取参 + 正确校验 + 调用 fileioctl(f, cmd, arg) + 返回结果
也就是说,你只需要把系统调用层打通到 fileioctl,不需要在 sys_ioctl 里直接处理具体 GPU 命令细节。

3.2.6 ioctl用户态使用方法:获取 GPU 矩阵乘次数

下面内容对应参考源码中的定义:

  • 指令定义位置:kernel/include/gpu.h
  • 处理逻辑位置:kernel/drivers/gpu.ccase GPU_IOC_GET_STATS

  • ioctl 指令(cmd)是什么

    • 指令名:GPU_IOC_GET_STATS
    • 指令值:5
    • 语义:查询 GPU 驱动统计信息(当前实验中关注 matmul_ops
  • 参数结构体是什么

    • 结构体名:struct gpu_stats
    • 定义:
      struct gpu_stats {
          uint64 matmul_ops;
      };
      
    • 字段含义:matmul_ops 表示“自系统启动以来,GPU_IOC_MATMUL 成功执行的次数”。
  • ioctl(fd, cmd, arg) 三个参数该怎么传

    • fd:由 open("/gpu", O_RDWR) 返回的设备文件描述符;
    • cmd:传 GPU_IOC_GET_STATS
    • arg:传 struct gpu_stats 变量地址,即 (uint64)&st
  • 返回值与判定规则

    • ioctl(...) == 0:调用成功,st.matmul_ops 已由内核写回;
    • ioctl(...) < 0:调用失败(例如 fd 非法、copyout 失败),应打印错误并返回非零退出码。

需要包含的头文件

  • #include "user.h":提供 open/close/ioctl/printf/exit 等系统调用的用户态封装
  • #include "fcntl.h":提供 O_RDWR 等文件打开方式描述符
  • #include "gpu.h":提供指令 GPU_IOC_GET_STATS 与指令对应参数结构体 struct gpu_stats

3.3 功能要求

本部分由两个连续任务组成:先完成添加 ioctl 系统调用,再编写用户态统计程序。

  1. 系统调用号注册:
    • kernel/include/syscall.h 中新增 SYS_ioctl 编号。
  2. 内核态实现与分发:
    • kernel/core/syscall.c 中实现 sys_ioctl
    • trapframe 读取参数(fdcmdarg);
    • 校验 fd 合法性并获取 struct file *
    • 调用 fileioctl(f, cmd, arg) 返回结果;
    • syscalls[] 分发表中注册 SYS_ioctl -> sys_ioctl
  3. 用户态封装:
    • user/syscall.c 添加 ioctl 封装;
    • user/user.h 声明 int ioctl(int fd, int cmd, uint64 arg);
  4. 新增用户态程序 gpu_stats.c
    • user/ 目录新增 gpu_stats.c,通过 open("/gpu", O_RDWR) 打开 GPU 设备;
    • 准备 struct gpu_stats st,调用 ioctl(fd, GPU_IOC_GET_STATS, (uint64)&st) 获取统计信息;
    • 调用成功后,按固定格式输出:
      • gpu matmul_ops: %llu
    • 程序结束前关闭 fd,异常路径返回非零退出码。
  5. 修改 Makefile ,将用户态程序 gpu_stats.c 加入编译目标,并打包进fsimg。

程序样例(gpu_stats

一个最小流程如下: 1. 先运行若干次矩阵乘(例如运行 gpudemo)。 2. 再执行 gpu_stats。 3. 预期输出形如:

gpu matmul_ops: 3
其中数值会随 GPU_IOC_MATMUL 调用次数增加而变化。

3.4 评分细则(8 分)

  • 4 分ioctl 系统调用链路完整(编号、分发、sys_ioctl、用户态封装均正确)。
  • 4 分gpu_stats.c 实现正确,能通过 ioctl 获取矩阵乘次数并按 gpu matmul_ops: %llu 格式输出。

自动测试命名与输出格式强约束

为确保通过自动测试,以下内容必须与要求完全一致,不得自行改名或改格式:

  • 系统调用名:ioctl
  • 用户程序文件名:gpu_stats.c
  • 输出格式:gpu matmul_ops: %llu

若上述任意一项被修改(包括大小写、下划线、空格、冒号位置等),将导致自动测试匹配失败,无法通过测试。

3.5 提示

  • 参数寄存器约定:系统调用号在 a7,前三个参数在 a0/a1/a2
  • sys_ioctl 中常见错误是未检查 fd 越界或 ofile[fd] == 0
  • gpu_stats.c 建议复用 gpu.h 中的 struct gpu_statsGPU_IOC_GET_STATS 常量,避免魔法数字。
  • 输出格式请严格对齐题目要求:gpu matmul_ops: %llu(包含空格与冒号位置)。

3.6 测试方法

建议测试步骤(第三部分)

  • 先验证 ioctl 是否成功
    • 修改 Makefile ,将用户态程序 gpudemo.c 添加进 UPROGSfsimg(参考NexOS 用户态程序创建指南);
    • 启动 qemu 以后, 在命令行中输入并执行指令 gpudemo 2 2 2 1 2 3 4 5 6 7 8 ,检查是否得到正确结果 19 22 43 50
  • 联动验证 GPU 统计
    • 先运行一次或多次 gpudemo 2 2 2 1 2 3 4 5 6 7 8
    • 再运行 gpu_stats,应输出 gpu matmul_ops: %llu,且数值等于最近一次执行 gpudemo 输出的 STATS: 后的结果。
  • 格式核对:输出必须严格匹配 gpu matmul_ops: %llu,否则自动测试无法通过。