# OpenVMS:AST / $QIO / 条件处理 / 镜像激活与共享镜像 OpenVMS 有**两条异步/异常控制流**,逆向时都要补进图里: ``` AST(Asynchronous System Trap) → 外部事件驱动的异步控制流 条件处理(Condition Handling) → 异常/状态传播的控制流 ``` 再加一层**镜像激活**语义(逻辑名、共享镜像、全局节)——三者叠起来,才解释得了"这段代码谁在调、这份映像是哪来的"。 ## 一、$QIO 与事件标志 `SYS$QIO` 的关键参数: | 参数 | 作用 | |---|---| | `efn` | 完成时**置位**的事件标志号 | | `astadr` | 完成时执行的 **AST 例程地址** | | `astprm` | 传给 AST 例程的参数 | | `iosb` | 接收最终状态的 I/O 状态块 | - **发起时事件标志会被清除**;完成时置位 - `iosb` 技术上可选,但**不应当作可选**——并发的异步事件要各用一个独立的 IOSB ## 二、完成顺序与控制流 ``` ① 写 IOSB → ② 置事件标志 → ③ 触发 AST ``` - 指定了 `astadr` 时,系统在完成时**中断进程并把控制交给 AST 例程**;例程结束**控制返回到被中断的那个点** - **事件标志置位 ≠ 这个 I/O 完成**:等待服务等的是标志,别的操作也可能置位它;判断完成要用同步等待 + IOSB - **`W` 后缀 = 同步版本** ## 三、AST 的语义 - AST 像子程序调用,但**无视主流程上下文异步投递**;主线与 AST 共享的代码**必须可重入** - **AST 不会中止进行中的系统调用** - 进程休眠或在等待服务上等待时:AST 投递会**把进程暂时移出等待**去执行 AST,完成后**原来的等待调用会被再次执行** **所以**:`wait 看起来"返回后又自动等待"不是 loop corruption**——这是该模型的正常行为。跳出这个循环只能靠唤醒或置位事件标志。 - 同一访问模式下,AST **不能被同访问模式的另一个 AST 抢占**,但**可被更高访问模式的 AST 抢占** - **特殊内核模式 AST** 是获得进程上下文的标准机制:在下一次该进程被调度时以该进程最高优先级执行 ## 四、屏蔽 AST - 专门的系统服务可关闭/开启**当前及所有更低特权访问模式**的 AST 投递 - 极短临界区可用**每进程 P1 空间里的两个标志位**代替,避免服务调用开销:投递代码置"已禁用"位,若第一位已置则再置第二位并**不投递**;用户代码事后检查第二位并重新开启 ## 五、条件处理:异常的控制流 **信号与处理器**: - 例程可以**返回完成码**(在 R0 里、按 bit 0 判断)或**显式 signal** 一个条件 - **条件处理设施(CHF)按顺序调用已启用的条件处理器**;signal 会复制参数列表并构造传给处理器的信号参数向量 - **处理器链的行为细节**(做控制流恢复时要记住): - 当处理器仍在活动时又发生第二次异常,**CHF 会跳过已搜索过的帧**,并调整传给处理器的深度值 - **经异常向量建立的处理器每次异常都会被调用(不被跳过)**——多次信号并发时用它建立处理器要谨慎 - **处理器返回后 CHF 依据返回状态动作**:再 signal 一个条件 / **继续** / **重新 signal(继续搜索下一个处理器)** / **unwind** **所以**:异常流不是"抛出—捕获"两段,而是**一条可以继续、可以重新投递、可以展开的链**。 ## 六、建立处理器的两种方式(含语言差异) - 一种方式把处理器地址**移到调用者栈帧的 longword 0**(即 `SF$A_HANDLER`),并返回原值;**直到显式恢复或控制返回到调用该建立例程的例程之前都有效** - 它**修改调用者的栈帧**,主要为**没有内建错误处理的语言**提供;**不该与有预定义处理器的语言混用**(会得到不可预期的结果);手工汇编里等价于把处理器地址写进帧指针偏移 0 - **在 Alpha 与 I64 的原生程序上不可用**,但多数语言编译器会**识别并映射**它: - 每个过程可用**过程描述符**里的处理器字段标识处理器(Alpha),I64 上则由**展开信息块**的处理器字段标识 - I64 的运行时里没有它;各语言分别处理(有的当内建、有的转成运行时入口点、有的提供自己的建立/恢复内建) - **32 位信号数组格式在 VAX 与 I64 相同,但条件码与机制数组格式不同**(四字元素、额外寄存器、深度值) - **C 程序必须用 C 运行时专有的建立接口**:它会捕获与 C 运行时相关的异常并把你不想处理的转给你的处理器;**直接用通用接口配 `setjmp`/`longjmp` 是未定义行为** - 要让某个异常生成 UNIX 风格信号,用户处理器必须返回"重新投递";返回"正常"则**阻止**生成 UNIX 风格信号 - 用空指针调用可取消该例程中已有的处理器 ## 七、镜像启动时建立的默认处理器 **每次启动新映像时,操作系统都会建立默认条件处理器**(三件套,顺序有意义): 1. **traceback 处理器**——在 catchall **之后**建立,因此**先拿到控制**;经消息输出与格式化服务给出错误消息与符号化回溯,并按条件严重性决定继续或退出 2. **catchall 处理器**——建在**第一个栈帧**,**最后被调用** 3. **last-chance 处理器**——经系统异常向量建立;**仅当栈无效、或所有栈处理器都重新投递时**才被调用;**若调试器在场,调试器自己的 last-chance 处理器会替换系统那个** **只有这三个会输出错误消息。** ## 八、镜像激活与共享镜像 - **重定向共享镜像**:**用与该共享镜像文件名相同的逻辑名**即可——镜像激活器会用这个逻辑名去找目标文件,于是**无需重新链接**就能替换实现 - 共享镜像的默认目录是 **`SYS$SHARE:`**(不是 `SYS$LIBRARY:`,尽管当前二者指向同一批目录) - 逻辑名必须与"链接时使用的输入文件规格"以及"安装时使用的名字"一致 - 有**动态激活把一个共享镜像装进进程的 P0 空间**的例程 - **特权/仅执行(execute-only)镜像的限制**(安全分析常撞到): - 激活特权镜像时,**只考虑系统逻辑名表里带执行属性的逻辑名** - **被激活的镜像、以及它引用的共享镜像与消息节,都必须作为"已知镜像"安装** - 仅执行(有执行权无读权)的镜像受**与特权镜像相同**的限制;专用限定符(只对主程序有意义)允许激活"用户有执行权但无读权"的共享镜像 - 这种受限模式下,**RMS 只使用"可信"逻辑名**(exec/内核模式创建的);**用户模式与超级visor模式的逻辑名不被信任**;进程/组逻辑名表里的重定向被忽略 ## 九、节(sections)与调用标准 - 安装镜像时指定**共享**会为其建立**全局节**(global sections);否则以**私有节**映射 - 共享镜像可安装为让全局节**驻留多端口内存**(需要相应特权) - **已知镜像(及其全局节)的删除,要等所有使用它的进程释放之后才发生**;可写全局节在移除时写回磁盘 - 调用标准体现在参数与返回值约定上:**返回值是条件值**;参数可以是**按描述符**、按引用或按值;描述符有类别要求(VAX 上某些必须是特定类别);符号查找一类的例程**返回符号的虚拟地址时会加上共享镜像的起始地址** ## 十、RMS 与文件规格解析 - 很多功能**把文件名与默认名分开解析**(而不是拼成一条完整规格) - 默认设备/目录、默认文件类型(如 `SYS$SYSTEM` / `EXE`)由调用方或例程提供 - **已知镜像的查找忽略版本号**,总是用现存最高版本;**安装命令的规格里不允许指定版本号** ## 决策树 ``` 拿到 OpenVMS 目标 ├─ 例程没有调用者却有行为 │ ├─ 查它是否作为 astadr 传给异步服务 → 补 AST 边 │ └─ 查它是否是条件处理器 → 补条件处理边 ├─ 完成路径不完整 │ └─ 补:写 IOSB → 置事件标志 → 触发 AST ├─ 异常处理不完整 │ ├─ 处理器链:继续 / 重新投递 / 展开 三种去向 │ ├─ 第二次异常时 CHF 跳过已搜索帧 │ └─ 三个默认处理器(traceback / catchall / last-chance) ├─ 等待"返回后又继续等待" │ └─ AST 投递后原等待被重新执行,属正常 ├─ 共享镜像行为与预期不符 │ ├─ 逻辑名是否与文件名一致(重定向机制) │ ├─ 默认目录 SYS$SHARE │ └─ 特权/仅执行镜像下:必须安装为已知镜像,且只认可信逻辑名 ├─ 参数/返回值解析错位 │ └─ 按调用标准:条件值 + 描述符类别 + 按引用/按值 └─ 文件打不开 / 找到的不是那份 └─ RMS 的文件名与默认名分开解析;已知镜像查找忽略版本号 ``` ## 工具与验证 - 异步登记点:异步服务的 `astadr`/`astprm`/`efn`/`iosb` 参数 - 条件处理:处理器建立点(栈帧字段 / 过程描述符 / 展开信息块)、处理器返回值、默认三件套 - 镜像:镜像激活器与逻辑名(默认目录、重定向规则)、已知镜像安装状态、全局/私有/消息节 - 调用标准:描述符类别、参数传递方式、返回条件值 - 验证:能同时说清「这条 I/O 的完成经哪条 AST 边回来 + 异常会走哪一层处理器 + 这个映像从哪里被激活、节是全局还是私有」 ## 该平台的坑(汇总) - **把 AST 例程当无调用者的死代码** - **用同步调用图解释完成路径**:必须补 AST 边 - **把"等待返回后又自动等待"当循环被破坏** - **把事件标志置位当该 I/O 完成** - **忽略 IOSB**:并发异步事件必须各用一个 - **以为 AST 会中止正在进行的系统调用** - **把异常当"抛出—捕获"两段**:实际是可继续、可重投、可展开的链 - **忽略"第二次异常跳过已搜索帧"**:处理器的 depth 值会变 - **把经异常向量建立的处理器当只调用一次**:每次异常都会调用 - **在有内建错误处理的语言里混用栈帧式处理器建立方式**:行为不可预期 - **在 C 里用通用接口配 setjmp/longjmp**:未定义行为,必须用 C 运行时专有接口 - **按文件名找共享镜像却忽略逻辑名重定向**:看到的可能不是你以为的那份 - **在特权/仅执行镜像下期待普通逻辑名生效**:只认可信逻辑名,且被引用的共享镜像必须安装 - **按 ASCII/字节流理解参数与记录**:调用标准与 RMS 规格解析各有自己的规则