实验二:将 LLM 推理封装为内核 AI Service¶
实验说明¶
本实验默认你已经完成 HW2,并且已经在 HW2 中读过配套材料、跑通过最小推理链路。
本实验的重点不是“重新实现一次模型推理”,而是把 HW2 中已经能运行的用户态推理后端,封装成一个更像操作系统服务的接口。完成后,你应当能够让上层程序通过 ai_call、ai_submit、ai_query、ai_wait 使用这条服务链路,而不是像Hw2的AI分支一样直接操作 llmrun_*。
关于AI辅助编程(Vibe Coding)的说明:
-
本课程旨在鼓励大家拥抱新技术,我们不主张将古法编程与AI辅助编程对立。培养大家使用AI工具解决实际开发问题的能力,是今年实验课改革的重要目标。如果你现在还没有接触过AI辅助编程,建议查看ai编程发展;如果你现在不知道如何开始,可以查看助教推荐,该文档会不定时更新。至少,你应该有Github的学生会员,并且已经学会使用
Copliot。 -
在代码提交方面,课程不设代码查重机制,允许提交由AI生成或辅助编写的代码。但这同时也对大家的
代码掌控力提出了更高要求:课程的最终区分度将重点通过线下问答(答辩)来体现,所以如果你对代码并不理解,线下问答扣分是正常的,但是我们线下检查控制在30%,也不会特别影响大家的成绩,主要还是为了督促大家理解实验在做什么,培养基础。在使用AI生成代码时,大家需要深入理解代码的运行逻辑与设计意图。能够透彻解释AI为何如此实现,才是顺利通过考核的关键。
实验给分说明¶
本实验的基础部分总分为 20 分,按与 lab1 相同的方式折算计入最终成绩:
- 代码测试结果:按 70% 比例计入基础部分总分
- 线下提问检查:按 30% 比例计入基础部分总分
此外,本实验还有一个选做 Bonus,满分 5 分。
- Bonus 不计入平台自动测试;
- Bonus 需要找助教进行人工检查;
- 建议先完成基础部分,再考虑 Bonus。
实验代码提交说明¶
本实验采用希冀平台作为测试平台,网址为 https://cscourse.ustc.edu.cn。
在在线作业 lab2 的代码提交界面,需要粘贴你自己的 GitLab 代码仓库链接。由于本次实验使用的是 lab2 分支,因此需要额外指定待测分支,可使用下面两种格式:
https://cscourse.ustc.edu.cn/vdir/Gitlab/username/yourproj.git --branch=lab2https://cscourse.ustc.edu.cn/vdir/Gitlab/username/yourproj.git lab2
平台上的自动测试主要覆盖基础部分的主线功能。若你发现线上测试结果和本地结果存在明显出入,请保留本地运行截图与线上结果截图,并联系助教确认是代码问题还是测试脚本问题。
本次测评方法,会直接拉取你的仓库中的kernel/core/ai_service.c文件,替换到原先的仓库中,所以一般不会出现编译问题。线上测评包含了一些仓库中没有提供的测评样例,所以手动测评通过,但是线上测评不是满分是正常的,请检查你的实验代码(尤其关注下面实验文档中的函数说明)。
实验时间安排¶
注:此处为实验发布时的安排计划,请以课程主页和课程群内最新公告为准
- 4.16 晚实验课,讲解实验二并检查实验
- 4.23 晚实验课,检查实验
- 4.30 晚实验课,检查实验
- 5.7 晚实验课,检查实验
- 5.14 晚及之后实验课,补检查实验
补检查分数照常给分,但会记录此次检查未按时完成,此记录在最后综合分数时作为一种参考。
本次实验难度较高,建议不要在最后几天完成!
检查时间、地点:周四晚 18:30-22:00,电三楼 406/408。
基础实验周期为三周,之后为补检查。
获取实验代码(切换到 lab2 分支)¶
本实验假设你已经在本机 clone 过课程仓库。接下来请拉取远端 lab2 分支,并切换到该分支完成实验。
在仓库根目录执行:
当你完成实验后,push代码时,请注意指定远端仓库:- 若提示找不到
upstream/lab2,请先确认远端是否已经发布该分支。 - 切换分支前请先用
git status检查当前工作区,避免未提交改动被分支切换打断。
开始前你需要完成的前置准备¶
本实验默认你已经完成 HW2 的准备工作,因此这里不再重复展开 HW2 中已经讲过的原理。
开始之前,请先确认下面几件事
- 你已经完成
HW2的环境准备,并能启动带有模型文件的实验镜像。 - 你已经理解
HW2中“请求队列 + 后台 worker”的基本思路。 - 你已经理解为什么
wait()类语义会自然引出“所有权检查”“状态查询”“一次性结果消费”等约束。 - 你当前使用的是只包含一个活跃模型目录的镜像,不要同时把
Smol和Qwen都打进同一个运行镜像里。
如果你需要回顾背景知识,建议先重新阅读下面几页:
- Hw2 作业说明
- Hw2 环境准备与最小检查
- Part 2 阅读材料:从
llmrun --stdin到“请求队列 + 后台 worker” - Part 3 阅读材料:从
wait()推演所有权、状态查询与一次性结果消费语义
初步启动:先把带模型资产的镜像跑起来¶
开始写 ai_service 之前,仍然需要先确认仓库根目录已经准备好了模型资产,并且能够成功启动带 LLM 文件的镜像。
如果你已经做过 HW2
若你是在同一个本地仓库里先完成 HW2,再切换到 lab2 分支,那么工作区里通常已经保留了 models/ 目录。此时一般不需要重新创建目录,只需要确认里面的模型压缩包还在即可。
思考:为什么切换分支后models文件夹还保留着?
如果你当前还没有 models/ 目录,请在仓库根目录手动创建该目录,并把助教提供的模型压缩包放进去。文件名仍与 HW2 保持一致,例如:
如果你本机还没有这些压缩包,请回看 Hw2 环境准备与最小检查 中的下载说明。
不要使用普通的 make qemu。后者不会把 LLM 模型文件打进镜像,因此 init 也无法正确检查模型目录并自动拉起 ai_daemon。
请在仓库根目录执行:
$ make qemu-llm
xxxxxxx #编译打包信息
[NexOS] Mounting filesystem...
[ai_daemon] loading LLM assets
[ai_daemon] cfg kind=0 dim=576 hidden=1536 layers=30 heads=9 kv_heads=3 head_dim=64 vocab=49152 runtime_seq=256
[ai_daemon] EMB.BIN loaded (28508160 bytes)
[ai_daemon] layers 0..4 loaded
[ai_daemon] layers 0..9 loaded
[ai_daemon] layers 0..14 loaded
[ai_daemon] layers 0..19 loaded
[ai_daemon] layers 0..24 loaded
[ai_daemon] layers 0..29 loaded
[ai_daemon] layer mix full=30 linear=0
[ai_daemon] model loaded and cached in memory
ai_daemon: ai_worker_register failed # 显示failed是正常的,是由于没有完成实验
[init] ai_daemon did not signal ready
[init] disabling ai_daemon restarts after startup failure
[init] ai_daemon exited status=1
若启动后没有看到:
请优先回头检查模型压缩包是否已经放到 models/ 下,以及镜像是否确实是通过 make qemu-llm 生成的。
若看到了[init] ai_daemon xxx建议先在 shell 中运行一次:
这是课程已经提供的最小同步 ai_call() 检查,用来快速确认镜像、模型资产和 syscall 链路没有明显问题。
代码架构¶
本实验真正需要你重点关注的文件不多。建议你先把下面几处文件的职责看清楚,再开始写代码。
kernel/core/ai_service.c¶
这是本实验的核心文件,也是必做部分唯一需要完成的地方。
当前代码中的 TODO 基本都集中在这里。建议你也把主要精力放在这里。
user/ai_daemon.c¶
这是用户态常驻 worker。它的大致工作流程是:
- 选择当前活跃模型目录;
- 初始化已有的 LLM runtime;
- 调用
ai_worker_register()把自己注册为唯一 worker; - 在循环中不断执行
ai_worker_get()、真实推理、ai_worker_complete()。
Bonus实验的主要完成部分。
user/aitest.c¶
这是本实验最重要的测试入口。它已经封装了同步、异步、状态查询、非法等待、二次等待、所有权隔离、批量提交等多种测试模式。
你不需要把 aitest.c 里的所有模式都记住,但至少要知道:
aitest可以测同步路径;aitest --async可以测异步路径;aitest --async --query可以测状态查询;- 其它参数可以帮助你检查边界行为。
注意,该文件只用于手动验证,其中的测试是线上测评的子集!!!
user/init.c¶
init 会在系统启动时检查模型资产,并在满足条件时自动拉起 ai_daemon。如果一切正常,你通常会在启动日志里看到:
如果你始终看不到这行输出,那么本实验两个正式 Part 的异步 service 主线通常都不会正常工作;但课程已经提供的同步 ai_call() 快速检查仍然可以先帮助你判断环境和 syscall 链路有没有明显问题。
syscall 相关文件¶
下面这些文件说明 service 接口已经被接进系统调用链路(Lab1的内容我们Lab2就不再做重复工作了):
kernel/include/syscall.hkernel/core/syscall.ckernel/include/defs.huser/user.huser/syscall.c
也就是说,syscall 编号、分发和用户态封装基本都已经准备好了。你在必做部分真正要补的是 ai_service.c 中的控制逻辑。
代码执行流程¶
本次实验本质上是一个生产者消费者问题(理论课 ch5)。

本次实验的生产者消费者架构图等助教有空了更新,先用理论课的顶一下/(ㄒoㄒ)/~~
1. 流程总览¶
- 用户程序(
aitest.c)通过ai_submit()或ai_call()提交请求。 - 内核在
ai_service_submit()->ai_service_enqueue_tokens()中创建请求对象、保存输入并把请求放入队列。 ai_daemon注册为 worker 后,通过ai_service_worker_get()从队列中取任务。- worker 在完成推理后,调用
ai_worker_complete()把执行结果交还给内核。 - 前台程序可以通过
ai_query()查询状态,也可以通过ai_wait()等待完成并取回结果。
2. 提交请求¶
-
用户态程序(例如
aitest.c)调用ai_submit()或ai_call()提交请求。例如: -
以内核如何处理
ai_submit()为例。用户态程序发起系统调用后,会进入sys_ai_submit(),再调用内核态的ai_service_submit(): -
ai_service_submit()本质上会继续调用ai_service_enqueue_tokens(),来将请求的具体内容放到请求队列中,具体说明见Part1部分。 -
到这里为止,这次请求已经从“前台的一次函数调用”变成了“内核队列中的一个受管对象”。
3. worker 取任务并完成请求¶
-
ai_daemon启动后,会先调用ai_worker_register();其内核侧对应的函数是ai_service_worker_register()(具体说明见Part1部分)。 -
注册成功后,worker 会不断调用
ai_worker_get();其内核侧对应的函数是ai_service_worker_get()。 -
如果此时队列为空,worker 不应该一直轮询抢占 CPU,而是先阻塞等待;等到有新请求入队后,再继续取任务。
-
成功取到任务时,内核会把当前请求中的
tokens、reqid和predict_count回写给用户态 worker。于是ai_daemon就真正拿到了这次要执行的任务内容。 -
worker 在用户态完成模型推理后,需要调用下面这个接口把结果交还给内核:
-
ai_worker_complete()是一个系统调用,其内核侧对应的函数是ai_service_worker_complete()。这个函数可以简单理解成“worker 向内核提交本次任务的结算结果”。 -
ai_service_worker_complete()会先检查两件事(具体说明见Part1部分):- 当前调用者是不是已经注册成功的合法 worker;
reqid对应的请求是否真的存在,并且当前处于RUNNING状态。
-
检查通过后,内核会把这次执行结果正式写回请求对象,并把状态改成
DONE或FAILED;随后再唤醒可能阻塞在ai_wait()中的前台进程。到这里为止,异步主线才真正闭环。
4. 前台查询与等待结果¶
-
如果前台只想知道某个请求目前是什么状态,可以调用:
-
如果前台需要真正等待完成并拿到结果,可以调用:
-
ai_query()和ai_wait()进入内核后,并不会只凭一个reqid就直接访问请求对象,而是都会先调用ai_find_req_locked(reqid, p->pid)。 -
ai_find_req_locked()的作用可以简单理解成:先按reqid找到请求,再进一步确认这个请求是不是当前进程自己的请求。 -
如果这一步检查失败,
query()/wait()都应直接返回-1;否则才可以继续读取状态或等待结果。 -
ai_query()的职责比较简单:把当前请求的状态、错误码、结果长度等信息整理后返回给用户态。 -
ai_wait()则更进一步:如果请求尚未完成,就阻塞等待;如果请求已经完成,就返回结果或错误,并在成功消费后回收请求槽位。
5. 辅助函数1:copyin() / copyout()¶
这两个函数可以先把它们理解成“内核帮你做的一次数据拷贝”:
copyin():把用户态程序给出的数据复制到内核自己的缓冲区里;copyout():把内核里已经整理好的数据复制回用户态程序提供的缓冲区里。
原理上的直观理解可以先记到这里;写代码时,重点关注它们在代码里怎么写、关键变量分别填什么。
常见写法如下:
copyin(p->pagetable, (char *)kbuf, user_uva, nbytes);
copyout(p->pagetable, user_uva, (char *)kbuf, nbytes);
可以先按下面这对应关系来记:
p->pagetable:通常直接传当前进程的页表,实验里一般写成p->pagetable(不用管,就这么写╮(╯▽╰)╭)kbuf:内核里的缓冲区变量,也就是你准备读入或写出的那块内存user_uva:用户程序传进来的地址变量,通常名字里会带uva(理论课第七章的内容)nbytes:本次要处理的字节数,通常写成“元素个数 × 每个元素大小”
在本实验里,几个典型用法:
copyin(p->pagetable, (char *)tokens, token_uva, token_count * sizeof(uint32))这里的tokens是内核里的 token 缓冲区,token_uva是用户传进来的 token 地址copyout(p->pagetable, token_uva, (char *)tokens, token_count * sizeof(uint32))这里通常表示把内核里的tokens内容按相同长度写回某个用户缓冲区copyout(p->pagetable, reqid_uva, (char *)&reqid, sizeof(reqid))这里的reqid_uva是用户提供的整型输出地址,&reqid是内核里的整型变量地址copyout(p->pagetable, predict_uva, (char *)&predict_count, sizeof(predict_count))predict_uva和&predict_count的对应关系与上面相同copyin(p->pagetable, result, out_uva, out_len)这里的result是内核临时结果缓冲区,out_uva是 worker 传进来的结果地址copyout(p->pagetable, st_uva, (char *)&st, sizeof(st))这里的st_uva是用户传进来的状态结构地址,&st是内核整理好的状态结构
写的时候最容易看乱的,其实就是两件事:
- 第一个地址参数和第二个地址参数,哪个是内核变量,哪个是用户地址
- 长度到底该写
sizeof(x),还是count * sizeof(type)
6. 辅助函数2:sleep() / wakeup()¶
它们的原型大致如下:
// kernel/core/proc.c
void sleep(void *chan, struct spinlock *lk){
...
acquire(&p->lock);
release(lk);
p->chan = chan;
p->state = SLEEPING;
sched();
...
}
void wakeup(void *chan){
...
if (p->state == SLEEPING && p->chan == chan) {
p->state = RUNNABLE;
}
...
}
可以先按下面这种方式理解这两个函数的用法:
sleep(chan, lk):表示“当前进程先挂到chan这个等待条件上,然后进入睡眠”;其中lk是当前正在保护共享状态的那把锁wakeup(chan):表示“唤醒所有睡在这个chan上的进程”,让它们重新参与调度并再次检查条件
这两个函数在代码里最需要看清的是两个参数:
chan:你到底在等哪件事发生。sleep()和wakeup()两边必须使用同一个chanlk:当前保护共享状态的锁;在本实验里通常就是&aisvc.lock
先把 chan 理解成什么?
chan 可以先把它理解成“等待条件的标识”。内核在 sleep(chan, lk) 时,会把当前进程记到这个 chan 上;之后只有别人对同一个 chan 调用 wakeup(chan),这个进程才会被唤醒。
在本实验里,chan 不需要理解成某种复杂的数据结构;更直接地说,它通常就是“你正在等哪件事发生”的那个稳定地址。只要地址稳定、sleep() 和 wakeup() 两边使用的是同一个地址,并且它能清楚表示“你在等什么事情”,就可以作为 chan。
例如:
- 在
ai_service_worker_get()中,worker 等待的是“消息队列从空变成非空”,所以chan可以选为队列对象或 service 对象本身的地址; - 在
ai_service_wait()中,前台进程等待的是“某个具体请求完成”,所以chan更适合选为该请求对象自己的地址。
可以先把常见写法理解成下面这样:
7. 辅助函数3:acquire() / release()¶
为什么锁总和阻塞一起出现?
acquire(&aisvc.lock) 和 release(&aisvc.lock) 不是“固定代码”,而是用来保护共享状态的。
- 在进入队列、取出队列、修改
qcount/qhead/qtail、推进请求状态时,都应该先加锁;(回想一下Hw2) - 完成这一轮共享状态修改后,再释放锁;
- 如果已经决定让当前进程阻塞,不要自己手工先
release()再sleep(),而应直接写成sleep(chan, &aisvc.lock); - 更稳妥的顺序通常是:先在持锁状态下把条件真的改好,再调用
wakeup(),最后release()。
Part 1:实现异步 AI Service 主路径¶
1.1 目标¶
第一部分需要让系统具备下面这条结构:
- 用户程序提交请求;
- 内核把请求组织成队列中的受管对象;
ai_daemon注册为唯一 worker;- worker 取任务、执行推理并回写结果。
第一部分结束后,Kernel才第一次具有“后台 service”结构。
1.2 你要实现的函数¶
这一部分需要完成四个函数:
ai_service_enqueue_tokens()ai_service_worker_register()ai_service_worker_get()ai_service_worker_complete()
1.3 建议按三个任务推进¶
如果你觉得补全代码有些抽象,可以参考下面三个小任务推进,而不是四个函数各写各的。
- 先让
ai_daemon能注册成功- 重点完成
ai_service_worker_register(); - 先把“唯一 worker”这件事建立起来。
- 重点完成
- 再打通“提交 -> 入队 -> 取任务”
- 重点完成
ai_service_enqueue_tokens()和ai_service_worker_get(); - 先让请求真的能从前台走到 worker。
- 重点完成
- 最后补全“完成回写 -> 唤醒等待者”
- 重点完成
ai_service_worker_complete(); - 到这里为止,异步主路径才算真正闭环。
- 重点完成
1.4 功能要求¶
请至少保证下面这些要求成立(几乎每一点要求都会有线上测评的corner case,当你的线上测评有问题时,不妨回头看看这里):
ai_service_enqueue_tokens()- 当系统中还没有合法 worker 注册时,应拒绝新请求;
- 当请求槽位耗尽时,应等待可用槽位;
- 需要把用户态传入的 token 数据
copyin到内核请求对象; - 请求入队后,应能唤醒等待任务的 worker。
思考: 为什么token id需要拷贝到内核,而不是直接用指针传递?
ai_service_worker_register()- 当前系统中只能有一个合法 worker;
- 如果已经有另一个 worker 在线,新注册应失败;
- 如果当前调用者就是已经登记的 worker,则重复注册应视为合法的幂等操作,而不是失败;
- 注册成功后,需要记录 worker 身份和在线状态。
思考:允许同一个调用者(worker)重复注册能带来什么好处?
ai_service_worker_get()- 只有当前已注册的合法 worker 才能取任务;否则直接返回
-1。 - 队列为空时,worker 应阻塞等待,直到有新请求入队后再继续。
- 这里可把“等待队列非空”这件事对应到一个固定的
chan上,例如 service/队列对象本身的地址;sleep(chan, &lock)和生产者入队后的wakeup(chan)必须使用同一个chan。 - 检查或修改
qcount、qhead、队列槽位内容以及请求状态时,都应在aisvc.lock的保护下进行。 - 成功取到任务时,需要从队列中弹出一个请求,并将该请求状态推进到“正在执行”。
- 成功时需要把请求的 token 数据、
reqid和predict_count回写给用户态 worker,并返回本次请求的token_count。 - 如果 worker 提供的
token_cap小于该请求实际的token_count,本次取任务应失败;同时不能把请求永久留在RUNNING状态。 - 如果向用户态 worker 回写 token、
reqid或predict_count的任一步失败,也不能把请求永久卡在运行中。 - 上面两类失败都应把该请求正确收尾为失败态,例如设置负错误码、清空结果长度,并唤醒可能在等待该请求结果的前台进程,而不是直接返回
-1。
- 只有当前已注册的合法 worker 才能取任务;否则直接返回
思考,为什么不同的失败的处理方式不一样?为什么有些需要回收请求,有些直接返回
-1?
ai_service_worker_complete()- 只有当前已注册的合法 worker 才能完成任务;否则直接返回
-1。 - 只能完成当前处于
RUNNING状态的请求;如果reqid无效、请求不存在、请求不在运行态,或同一请求被重复完成,也应直接返回-1。 - 在处理用户态结果指针前,不要长时间持有锁;应先确认身份与请求状态,再在锁外完成必要的用户态数据复制。
- 当
status == 0时,表示 worker 认为本次推理成功:- 需要检查输出长度是否合法;
- 需要把
out_uva指向的结果复制到内核临时缓冲区; - 如果长度非法或复制失败,本次完成应被视为“请求失败”,而不是把请求继续留在
RUNNING状态。
- 重新加锁后,需要再次按
reqid查找请求,并重新确认它仍处于RUNNING状态,再写回最终结果。 - 成功完成时,需要:
- 设置
err = 0;
- 设置
- 失败完成时,需要:
- 将
err设为负值; - 将
result_len清零;
- 将
- 无论最终是
DONE还是FAILED,只要这是一次对合法RUNNING请求的完成操作,都应唤醒睡眠在ai_wait()中的调用者。
- 只有当前已注册的合法 worker 才能完成任务;否则直接返回
梳理一下 ai_service_worker_complete() 的返回情况
这个函数最容易让人看乱的地方在于:函数返回值和请求最终状态不是同一个维度。先分开看
-
return -1表示“这次complete调用本身不合法”,而不是“请求执行失败”。 常见情况包括:- 当前调用者根本不是合法 worker;
reqid无效;- 根本找不到这个请求;
- 这个请求已经不在
RUNNING状态; - 重新加锁后发现该请求已经被别人改动,不再是当前这次合法的完成对象。
-
return 0且请求被写成DONE表示“这次complete调用合法,并且 worker 提交的是一个成功结果”。 这时通常要求:status == 0;- 输出长度合法;
- 从用户态复制结果到内核临时缓冲区成功;
- 重新加锁后请求仍然处于
RUNNING状态。
-
return 0且请求被写成FAILED表示“这次complete调用本身合法,但是这次请求应被判定为执行失败”。 常见情况包括:- worker 主动上报失败,即
status != 0; status == 0,但输出长度非法;status == 0,但从用户态复制结果失败。
- worker 主动上报失败,即
因此,可以用一句话记这个函数:
return -1:说明“这次完成操作不成立”;return 0 + DONE:说明“这次完成操作成立,而且请求成功完成”;return 0 + FAILED:说明“这次完成操作成立,但请求被正式收尾为失败态”。
最后再特别注意一点:只要是“对一个合法 RUNNING 请求做了正式收尾”,无论最后写成 DONE 还是 FAILED,都应该 wakeup(req);但如果这次调用一开始就是非法的、直接 return -1,则不应把它当成一次合法完成来唤醒等待者。
1.6 测试方法¶
建议先确认启动日志中已经出现:
再运行下面这些主线检查:
你可以这样理解这些命令分别在检查什么:
aitest --async检查请求是否真的经过“提交 -> 队列 -> worker -> 完成回写”。aitest --async --batch 4检查多个请求是否能共享同一个 service 主干,而不是只对单次调用生效。
注意,上述检查只是线上测评的子集!!!
Part 2:实现查询、等待与所有权语义¶
2.1 目标¶
在 Part 1 完成后,Part2是为了把这条 service 补成一个真正像操作系统服务的接口:
- 请求只能被拥有者查询和等待;
wait需要正确阻塞;- 结果只能成功消费一次;
- 请求完成后,槽位需要被回收。
2.2 你要实现的函数¶
ai_find_req_locked()ai_service_query()ai_service_wait()
2.3 功能要求¶
请至少保证下面这些语义成立:
ai_find_req_locked()- 不能只看
reqid; - 还要检查当前请求是否属于当前进程;
- 这是后续
query/wait隔离语义的基础。
- 不能只看
ai_service_query()- 只能查询当前进程自己提交的请求;
- 需要把请求状态、错误码、结果长度等信息整理后返回给用户态;
- 查询本身不应消费结果。
ai_service_wait()- 只能等待当前进程自己提交的请求;
- 请求未完成时应阻塞等待;
- 这里更适合把
chan设计成“这个请求对象自己的地址”,表示等待“该请求完成”;随后在ai_service_worker_complete()中把请求写成DONE或FAILED后,再对同一个chan调用wakeup()。 - 查找请求、检查请求状态以及最终回收请求槽位时,都应在
aisvc.lock的保护下完成;不要在无锁状态下直接读写共享请求对象。 - 请求失败时应返回错误;
- 成功取回结果后,要把请求槽位回收;
- 同一个请求结果不能被成功取走两次。
2.4 提示¶
这一部分容易写错的地方
- 只检查
reqid,忘记检查owner_pid,会导致跨进程信息泄露。 wait成功后不回收槽位,跑一阵子就会发现系统再也没有空位。- 过早回收请求对象,可能导致结果还没
copyout完就被清空。 - 第二次
wait还能成功,通常说明 one-shot 语义没有真正建立起来。
这一部分结束前,做一次同步接口一致性检查
starter code 里已经给你提供了一条最小同步 ai_call() 路径。当前面的 query/wait 语义都稳定之后,需要再回头修改:
也就是说,用户态看到的同步接口可以保留,但底层不应再绕开异步 service 主线。
2.5 测试方法¶
建议在 Part 1 通过后,再做下面这些主线检查:
aitest --async --query
aitest --badwait
aitest --async --doublewait
aitest --async --fork-unauth
aitest
你可以这样理解这些命令分别在检查什么:
aitest --async --query检查ai_query()是否能返回合法状态。aitest --badwait检查非法reqid是否会被正确拒绝。aitest --async --doublewait检查结果是否只能成功消费一次。aitest --async --fork-unauth检查子进程是否会被错误地允许等待父进程的请求。aitest作为最后的同步一致性检查,确认ai_call()已经和异步路径使用同一套底层语义。
测试与验收建议¶
测试范围说明
下面列的是建议你自己先做的检查,目的是帮助你尽快常见的问题。它不是完整的线上测评用例。
建议按下面顺序做自测:
-
先做一次课程提供的同步快速检查:
-
再确认后台 worker 已经拉起:
-
再检查
Part 1的异步主路径: -
再检查
Part 2的查询与边界语义: -
最后再做一次同步一致性检查:
如果上面这些命令都能稳定通过,你的实现通常已经完成了实验的主线。
Bonus¶
本实验的 Bonus 为选做内容,满分 5 分,需要助教进行人工检查。
Bonus 的详细要求、代码入口和建议测试方式见 实验二 Bonus:在 ai_daemon 中实现最小前缀缓存。
实验结论¶
从课程结构上看,可以把这句话作为本实验的最终认识:
HW2让模型能在用户态跑起来;Lab2让这条能力开始像一个操作系统服务。