# Xen:PV 前后端 / grant table / event channel / XenStore **不要把 Xen 的 PV 设备当普通 PCI/MMIO 驱动。** 典型 PV I/O 没有寄存器窗口,只有四件套: ``` frontend ├─ XenStore :交换配置(含 ring-ref / event-channel) ├─ grant table :共享 page ├─ shared ring :请求/响应 └─ event channel :通知 │ backend ``` 由此产生两类高频误判:**把 grant ref 当地址**、**把 event port 当 IRQ 号**。两者都只是**索引/句柄**,不是地址空间里的东西。 ## 一、XenStore:配置交换处,值以文本存放 - 前端在 XenStore 里记录后端域(`backend-id`,通常为 0),并把**共享 ring 的 grant 引用**写进去:`ring-ref`、`tx-ring-ref`、`rx-ring-ref`、`ctrl-ring-ref` 等 - 通知端口同样写进 XenStore:`event-channel`(网络控制环用 `event-channel-ctrl`) - **值是字符串**(XenStore 是文本式的)——"ring-ref=42" 存的是文本 `"42"`,不是二进制字段 - **前端把内存共享给后端**(不是反过来):前端用类似 `xenbus_grant_ring()` 的方式把本地分配的 ring 共享出去,再把引用告诉后端;后端拿到引用后在自己内核地址空间里分配页并映射 **RE 含义**:拿到一个 PV 前端,**先读它在 XenStore 里写了什么**——那里有后端域、ring 引用、事件端口,是恢复拓扑的现成入口。 ## 二、grant reference 是**条目索引**,不是物理地址 ``` ring-ref = 42 ≠ page 0x2a / 物理地址 0x2a000 ring-ref = 42 → 前端 grant table 的第 42 号条目 → { flags, domid, frame(MFN) } → 后端据条目去映射那一页 ``` - 每个域有自己的 grant table(一块与 hypervisor 共享的内存,由 guest 分配/初始化后经 hypercall 交给 Xen) - 条目三个字段:**`flags`(操作与权限)、`domid`(被授权的远端域)、`frame`(被授权页的 MFN,注意是 MFN 而非虚拟地址)** - **grant reference 就是条目下标**;下标被传给远端域,远端用它"激活"该条目 - 引用可以内嵌在 ring 消息里传递(如块设备的 `blkif_request_segment.gref`、网络的 `netif_tx_request.gref`)——**在 ring 里看到一个小整数,很可能是 gref,不是偏移或长度** **权限与状态位(决定"能不能撤销")**: | 名称 | 含义 | |---|---| | `GTF_permit_access` | 允许远端域访问本域某页 | | `GTF_accept_transfer` | 接受把一页转移进本域 | | `GTF_readonly` | 只读导出/导入 | | `GTF_reading` / `GTF_writing` | hypervisor 置位,表示该页**当前正被远端映射**(读/写) | | `GTF_transfer_committed` | 转移已完成 | - **映射期间 Xen 不支持撤销**:结束外来访问的调用**只是阻止后续映射**,不会撤销已建立的映射;发送方必须**等 reading/writing 标志清零**(用查询接口确认)之后才能真正回收 - 两种用法要分清:**保留所有权的 grant access**(块前端/后端:A 授权、B 映射、B 用完 unmap、A 再撤权)与**所有权的 transfer**(只适用于 PV;较老的网络前后端用它,新设计改为经 hypervisor 拷贝以避开 TLB 开销) ## 三、event-channel port 不是 IRQ number ``` event-channel = 17 ≠ hardware IRQ 17 ``` - event channel 是 Xen 的通知原语(`event_channel_op` hypercall),类型包括 **VCPU/IPI、VIRQ、PIRQ、域间(interdomain)** - 域间通道常用 `alloc_unbound` / `bind_interdomain` 建立,端口号通常经 XenStore 传给对端 - **端口整数绑定到每 guest `shared_info` 里的全局位掩码**(32 位 guest 为 1024 位,64 位 guest 为 4096 位)——这是"端口"在这个系统里的真实表示 - 用户态接口另有 `/dev/xen/evtchn` **所以 trace 里出现 `port 17` 时,不要去 IOAPIC/GIC 里找 IRQ 17。** ## 四、迁移:**本地端口不稳定,远端端口才是稳定标识** - 域间 event channel 本质是**一对端口**:本地端口 + 远端端口 - 持有者退出会关闭 `/dev/xen/evtchn`,内核随之关闭本地端口;**重新连接时是用原来的远端端口去绑定一个全新的本地端口**,新本地端口号**可能与旧值不同** - 因此**要记录/对齐的是 remote port**;把本地端口号当作持久 object identity 会出错 - 更危险的一点:**本地端口号在解绑后可被重用**,残留引用会指向完全不同的通道 **这与 seL4 的 CPtr、Zircon 的 handle 是同一类坑**(见 [[re-kernel/sel4-kernel]]、[[re-kernel/zircon-kernel]]):**整数句柄不是全局标识**。 ## 五、shared ring 不是普通共享内存 典型流程: ``` 前端拥有页 → grant 给后端 → gref 写入 XenStore → 双方各自映射同一批页 → event channel 只负责 kick ``` 所以看到 **ring 内存持续变化、却找不到 `memcpy`/socket IPC** 是正常的——这是共享页 + 生产者/消费者索引的模型,通知只是"踢一下"。每个 ring 配一个 event channel:环变非空时唤醒接收方,变非满时唤醒发送方。 ## 六、XenStore 是控制面,不是数据面 - **`state = Connected` 只表示协议协商完成**,不代表数据经 XenStore 传 - 真正的数据面可以是 **grant 共享 ring + event channel**: ``` XenStore 几乎没流量,设备 I/O 却很大 → 正常 ``` - **backend 不一定在 Dom0**:不要硬编码 `/local/domain/0/...`;前端里提供 **`backend` 与 `backend-id`** 用于明确定位 service domain——**正常 backend 区域不应当靠假设推导** ## 七、通知抑制:ring 前进却没有 kick 是正常的 - 生产侧的推送宏会**判断对端是否真的需要通知**:**只有新推进的请求越过了对端设置的 `req_event` 阈值时才发通知**(响应侧对称,用 `rsp_event`) - 所以: ``` ring producer 前进 + 没有 event 发送 → 完全正常 → 对端已有工作在飞,它之后会自己再查 ring ``` - 顺序要求:写请求 → 写屏障(对端先看到请求)→ 更新 producer 索引 → 全屏障(对端先看到新请求)→ 再判断 `req_event` ## 八、"睡前再检查一次"是协议核心,不是奇怪 busy-loop 消费侧在准备睡眠时的标准形态: ``` 发现无工作 → 设置"下一个事件通知我"(req_event = req_cons + 1) → 全屏障 → 再查一次 ring ``` - 这个屏障覆盖的竞态是:**producer 已经读过 `req_event`(因此决定不通知),而 consumer 随后才更新它**——不重新检查就会**丢唤醒** - 已知副作用:按原样使用会带来"每 2^32 次事件一次多余唤醒"(索引回绕),实现里常见把事件指针推到过去的修法 - 另一个副作用:该宏**会推进 ring 的事件指针**,在 NAPI 类场景里可能"提前招来下一次中断",所以有些路径只在真正切回中断模式时才用它 **所以不要把"双重检查"优化掉,也别把相关的强屏障当冗余。** ## 九、event channel 的掩码/待决语义:不是"使能位置 1" - event channel 是**"一位事件状态"**模型;**unmask 不是简单地打开使能位** - FIFO ABI 的 unmask 路径:清除 MASKED(连带 BUSY),**若 PENDING 置位则发起 unmask 调用**;该调用的语义要求:**若事件处于 pending,必须向对应 VCPU 投递通知** - 因此"masked 期间事件到达、unmask 之后行为诡异"应从 **pending / masked / FIFO 语义**解释,而不是按普通中断使能理解 ## 十、FIFO event ABI:优先级,以及两种 ABI 不能混 - **优先级 0 最高、15 最低**,默认 7;**每 VCPU 有 16 个事件队列** - 每个事件是带标志的字:**PENDING / MASKED / LINKED / BUSY** 加上指向队列下一项的 LINK 字段;每 VCPU 控制块含 ready 字与 16 个队头 - 含义:**不同 event 的投递延迟可能来自 FIFO 优先级,而不是 vCPU 调度** - **同一个 Xen build 里,事件记账布局可能完全不同**——存在 **2-level ABI 与 FIFO ABI**: ``` 拿 shared_info 的 pending 位图解析器去解释 FIFO 控制块 → 全错 ``` - 一个具体陷阱:**初始化 FIFO 控制块的操作不会重新投递当前 pending 的事件(它们会丢)**,因此应当在绑定任何事件之前完成 ## 十一、迁移后的身份变化 迁移可能要求前后端**重新协商协议**,因为 **domid 与 event channel 状态不一定保留**: ``` 迁移前:domid 7 / port 10 迁移后:domid 12 / 新连接 ``` **不应当判设备劫持。(与 [[re-kernel/zircon-kernel]]、[[re-kernel/sel4-kernel]] 的句柄本地性同源。)** ## 十二、内存屏障是协议的一部分 ring 生产者索引更新的前后都有屏障,保证请求/响应数据**先对另一端可见**。**这些屏障不是编译器噪声,删除即破坏协议。** ## 决策树 ``` 拿到 Xen 目标 ├─ 先判:PV 前端 / 后端 / 全虚拟化(HVM)设备? │ ├─ PV → 找四件套:XenStore 条目 + grant ref + shared ring + event channel │ └─ 找不到寄存器窗口/MMIO 循环 → 正常,PV 本来就没有 ├─ 看到 `ring-ref = N` │ └─ N 是 grant table 条目索引 → 去读条目 {flags, domid, frame},不是地址 ├─ 看到 `event-channel = N` │ └─ N 是 event channel 端口(绑定到 shared_info 位掩码),不是 IRQ 号 ├─ 页被远端访问 / 想回收页 │ ├─ 有 GTF_reading / GTF_writing → 映射仍在,撤销未生效 │ └─ 结束外来访问只阻止后续映射,必须等标志清零 └─ 迁移 / 重连后端口号变了 └─ 正常:本地端口会重新分配,稳定的标识是 remote port ``` ## 工具与验证 - 配置来源:XenStore 树(`~/device/...` 与 `~/backend/TYPE/DOMID/DEVID`)里的 ring 引用与事件端口;前端树里的 `backend-id` - 内存路径:grant table 条目(flags / domid / frame)与 reading/writing 状态位 - 通知路径:event channel 类型(IPI / VIRQ / PIRQ / 域间)与端口号;用户态经 `/dev/xen/evtchn` - 验证:能同时说清「拓扑(谁在前、谁在后、经哪个 XenStore 路径连接)+ 内存靠哪组 gref + 通知靠哪个端口」 ## 该平台的坑(汇总) - **把 grant ref 当物理地址/页码**:它只是本域 grant table 的条目下标,条目里才有 MFN - **把 event-channel port 当 IRQ 号**:它是 Xen 的通知端口,绑定在 `shared_info` 的位掩码上 - **以为结束外来访问就能立刻回收页**:映射期间不支持撤销,必须等 `reading`/`writing` 清零 - **把 event port 整数当持久对象标识**:迁移/重连后本地端口会变;解绑后同一号码还会被重用 - **拿本地端口(而非远端端口)做对齐**:重连时稳定的是 remote port - **在 ring 消息里把小整数当偏移或长度**:很可能是内嵌的 gref - **看到 ring 内存变化却无 IPC 调用就认为分析有漏**:PV 的数据面本来就走共享页,通知只负责 kick - **在前端/后端之间搞反共享方向**:是前端把 ring 共享给后端,再由后端映射 - **在 XenStore 里期待二进制字段**:它是文本式的,引用与端口都以字符串存放 - **把 XenStore 的 `Connected` 当数据面在传**:它只表示协商完成;数据在 grant 共享 ring 上 - **硬编码 backend 在 Dom0**:应读前端的 `backend` / `backend-id` - **把"ring 前进却没通知"当丢事件**:通知抑制是协议设计 - **删掉消费侧的双重检查或强屏障**:那正是防丢唤醒的部分 - **按"中断使能位"理解 event unmask**:masked/pending 语义不同,pending 必须投递 - **用一套事件解析器解释两种 ABI**:2-level 与 FIFO 的记账布局不同 - **在绑定事件之后才初始化 FIFO 控制块**:当前 pending 的事件会丢 - **把 FIFO 优先级造成的延迟归因于 vCPU 调度** - **把迁移后的 domid/端口变化判为劫持**:需要重新协商属正常