Part 3 阅读材料:从 wait() 推演所有权、状态查询与一次性结果消费语义¶
阅读目标¶
- 从
wait()的真实语义出发,理解异步 AI service 为什么必须补上所有权检查、状态查询以及一次性结果消费; - 能够把“谁能等待、谁能查询、结果何时回收”这些语义边界映射到最小可行的请求接口设计上;
- 为完成
Hw2中与ai_request、ai_status、ai_wait、ai_query相关的语义设计题做好准备。
阅读范围
本部分主要围绕以下内容展开:
kernel/core/proc.c中的wait()user/sh.c中的fork + exec + wait- “长期驻留 worker”这一类异步服务场景
为了完成 Part 3 题目,下文会在这些机制的基础上做一层合理抽象,使用 reqid、ai_wait、ai_query、ai_status 等记号来说明异步请求语义。你给出的答案不要求与后续实验最终代码完全一致,但必须做到字段选择和状态语义自洽。
阅读建议
回答 Part 3 时,只需要抓住“所有权、状态查询、一次性消费”这三条主线,不需要覆盖本页列出的每一个边界场景。
与前文的分工
Part 1 阅读材料的 4.4 / 4.5 已经介绍了当前 base 的运行路径与 llmrun_* 的初始化方式。本页不再重复这些背景,只把“长期驻留 worker”当作一个应用场景,用来说明为什么异步请求必须补上所有权、状态查询和一次性结果消费语义。
1. 先看 wait() 的核心语义¶
请阅读 kernel/core/proc.c 中的 wait()。关键片段如下:
int wait(uint64 addr) {
struct proc *p = myproc();
...
acquire(&wait_lock);
for (;;) {
int havekids = 0;
for (int i = 0; i < NPROC; i++) {
struct proc *pp = &procs[i];
if (pp->parent != p) {
continue;
}
havekids = 1;
acquire(&pp->lock);
if (pp->state == ZOMBIE) {
int pid = pp->pid;
int status = pp->xstate;
if (addr != 0 &&
copyout(p->pagetable, addr, (char *)&status, sizeof(status)) < 0) {
release(&pp->lock);
release(&wait_lock);
return -1;
}
freeproc(pp);
release(&pp->lock);
release(&wait_lock);
return pid;
}
release(&pp->lock);
}
if (!havekids || p->killed) {
release(&wait_lock);
return -1;
}
sleep(p, &wait_lock);
}
}
这段代码里,至少有三条可以直接迁移到异步 AI service 的关键语义:
- 只能等待“属于自己关系范围内”的对象
- 对象未完成时,可以阻塞等待
- 成功取走结果后,对象会被回收,而不是永久保留
2. 第一条语义:不是所有人都能等待任何对象¶
wait() 中最关键的一句是:
它表示:
当前进程只能等待自己的子进程,而不能拿任意一个
pid去回收别人的退出状态。
这个边界在 user/sh.c 里非常清楚:
这里的所有权关系是:
- shell 创建子进程;
- 子进程执行用户程序;
- shell 作为父进程负责
wait()回收它。
把这个思想迁移到 AI service,就会得到一个自然要求:
- 每个请求都必须知道“是谁提交的”;
- 后续
wait/query时,系统必须检查当前调用者是否就是这个请求的拥有者。
也就是说:
知道
reqid不是权限;拥有这个请求,才是权限。
3. 第二条语义:wait() 是阻塞等待,但它只等“完成”¶
wait() 做的事情并不是查询各种中间进度,而是:
- 子进程还没结束时就睡眠;
- 子进程结束后取走退出状态;
- 然后完成回收。
因此,wait() 很适合表达“一个对象什么时候整体结束”,但不适合表达“中间进度走到了哪一步”。
如果把这个逻辑迁移到 AI 请求,就会发现一个差别:
- 子进程状态通常很简单,重点是“活着”还是“退出”
- AI 请求的生命周期更细,至少可能经历“排队中”“运行中”“完成”“失败”等阶段
这正是异步 AI service 为什么通常还需要一个 ai_query():
因为仅有 ai_wait() 并不足以表达中间状态。
4. 第三条语义:结果成功取走后,应当一次性消费并回收¶
在 wait() 的成功路径里,取走退出状态之后会执行:
这说明:
- 退出结果不是永久快照;
- 父进程成功消费一次后,子进程对象就应该被回收;
- 第二次再对同一个对象
wait(),不应继续返回旧结果。
把这个思想迁移到 AI service,会得到完全平行的语义:
- 请求完成后,拥有者可以成功
ai_wait(reqid, out, cap)一次; - 一旦结果被成功取走,请求槽就应该回收;
- 同一个
reqid不能被无限次重复消费。
为什么这一点很重要
如果成功 wait 后还永久保留旧请求,就会带来三类问题:
- 请求槽迟迟不释放,系统资源会被耗尽
- 同一结果可能被重复取走,语义变得混乱
- 旧
reqid和新请求更容易发生混淆
5. 把 wait() 映射到异步 AI request¶
从系统设计角度,可以把两者做一个很直接的类比:
wait() / 进程语义 |
AI request 语义 |
|---|---|
子进程 pid |
请求 reqid |
| 父子关系 | owner_pid 或等价的所有权信息 |
ZOMBIE |
DONE / FAILED |
xstate |
错误码、结果长度等完成信息 |
wait() |
ai_wait(reqid, ...) |
freeproc() |
回收请求槽 |
这张表想说明的核心点是:
wait()不是凭空工作的;- 它依赖“对象归属”“对象状态”“成功消费后回收”这三件事同时成立;
- 异步 AI service 也必须补齐这三件事,接口语义才会完整。
6. 为什么异步 AI service 还必须提供 ai_query()¶
如果一个请求只是“有”和“没有”两种状态,那么也许只靠 submit + wait 就勉强够用。
但 AI 请求往往更像这样一条生命周期:
此时,调用者经常需要知道的并不是“立刻取结果”,而是:
- 还在排队,还是已经开始运行?
- 是已经成功完成,还是已经失败?
- 当前是否已经有结果可以等待取回?
这就是 ai_query() 的职责:
ai_query是观察 只返回状态,不消费结果ai_wait是消费 等待完成,并在成功时取走结果
把这两者区分开,接口语义才会清楚。
7. 一种最小可行的字段设计¶
Part 3 任务 A 不是要你抄写唯一标准答案,而是要你给出一组最小但自洽的字段。下面是一种压缩到较少字段的设计示意。
7.1 ai_request 元数据字段¶
| 字段名 | 用途 |
|---|---|
reqid |
请求的唯一标识,供后续 wait/query 查找 |
owner_pid |
记录这个请求属于谁,用于所有权检查 |
state |
记录请求当前处于排队、运行、完成还是失败 |
err |
失败时保存错误码,成功时可为 0 |
result_len |
结果长度,便于 wait/query 判断输出规模 |
7.2 ai_status 对外可见字段¶
| 字段名 | 用途 |
|---|---|
state |
告诉调用者当前生命周期阶段 |
err |
如果失败,告诉调用者失败原因 |
result_len |
如果已完成,告诉调用者结果规模 |
为什么这里没有再放 reqid
ai_query(reqid, st) 的调用者本来就已经持有 reqid,因此在“最小设计”里,ai_status 不再重复携带它也是合理的。如果你希望保留 reqid 作为调试字段,也可以,只要能解释其必要性。
8. 一组自洽的 query / wait 语义表¶
下面这张表几乎可以直接作为 Part 3 任务 C 的参考骨架。
| 场景 | 期望行为 | 理由 |
|---|---|---|
提交者在请求尚未完成时调用 ai_query(reqid, st) |
返回成功,并在 st 中写入 QUEUED 或 RUNNING 等状态;不消费结果 |
query 的职责是观察状态,而不是取走结果 |
提交者在请求完成后第一次调用 ai_wait(reqid, out, cap) |
阻塞结束后成功返回,复制结果,并回收请求槽 | 第一次 wait 才是合法的结果消费时刻 |
同一个提交者第二次对同一 reqid 调用 ai_wait(...) |
返回失败 | 结果已经被成功消费,请求对象已经回收或标记失效 |
其他进程或不具有所有权的子进程拿到 reqid 后调用 ai_query(...) |
返回失败 | 知道 reqid 不等于拥有访问权限 |
请求已经被成功 wait 取走后,再次 ai_query(reqid, st) |
返回失败 | 请求对象已经不再有效,旧状态不应被继续暴露 |
这张表背后的核心原则其实只有三条:
- 有所有权边界
query只观察,wait才消费- 成功消费后,请求对象应被回收
9. 回到 Hw2,应该抓住哪三个核心问题¶
如果你要回答 Part 3,最重要的不是记住某个固定结构体,而是先把下面三件事想清楚:
- 为什么异步请求必须记录“属于谁”
- 为什么异步请求不能只有
wait,还需要query - 为什么结果成功取走一次后,请求对象应当被回收
只要这三件事讲清楚,你给出的字段和语义表通常就会比较稳固。
10. 读到这里,你应该已经能回答的四个问题¶
- 为什么一个 AI 请求必须记录所有权信息
- 为什么“知道
reqid”不等于“有权限wait/query” - 为什么
ai_query和ai_wait不能混成同一种操作 - 为什么成功
wait一次后,请求对象通常应被回收
如果你已经能说清这些问题,就可以进入 Part 3 题目了。