USE_BUFFER 和 RUN_PROG 的用法与实现

· 预计阅读 9 分钟

USE_BUFFER 和 RUN_PROG 是阅读 pSolve 源代码时经常能见到的两个子程序。前者负责在程序之间传递缓冲区数据,后者负责找到并启动指定的可执行程序,传递运行状态,并决定后续执行是否返回调用者。两者配合,完成程序之间的数据交接和执行调度。这里简要记录它们的用法和实现。

在 enter.f 中,enter → batch 调用链包含下面两行代码:

CALL USE_BUFFER ( NEWBUF, INT2(64), 'OWC' )
CALL RUN_PROG ( 'BATCH', 'PASS', INT2(0) )

这是在干什么呢?第一行把 enter 准备好的“运行字符串”写入管道,其中包含控制文件名、处理进程编号等信息;第二行调度 batch 执行。batch 启动后,再从管道中读取这份数据。

1 USE_BUFFER

USE_BUFFER 的源代码位于 psolve/libs/cutil/use_buffer.f。它是一个 Fortran 子程序,通过 CALL 调用,接口如下:

SUBROUTINE USE_BUFFER ( IBUFF, IWDS, STRING )
参数 含义
IBUFF 存放待发送或已接收数据的 INTEGER*2 数组
IWDS 传输长度,以双字节整数为单位,每个 word 为 2 字节
STRING 操作指令,例如 'OWC' 或 'ORC'

第三个参数由几个字母组成,程序按照字母出现的顺序执行操作:

  • O:打开缓冲区访问
  • W:写入数据
  • R:读取数据
  • C:结束缓冲区访问

因此,'OWC' 表示执行“打开→写入→关闭”,用于发送数据;'ORC' 表示执行“打开→读取→关闭”,用于接收数据。这里的“打开”和“关闭”只涉及接口的逻辑访问状态,底层管道的建立另有专门的子程序,后面再说明。STRING 保存的是操作指令,与 batch_main.f 中保存运行字符串的同名变量不是一回事。USE_BUFFER 本身不解析传入的数据,也不限定其内容必须是字符串。

2 RUN_PROG

RUN_PROG 的源代码位于 psolve/libs/cutil/run_prog.f。它是 pSolve 的外部程序调度接口,负责确定程序位置、传递启动状态、启动程序,以及处理程序结束后的调度信息。接口如下:

SUBROUTINE RUN_PROG ( INPROG, STATE, PATH )

以 CALL RUN_PROG ( 'BATCH', 'PASS', INT2(0) ) 为例,各参数的含义如下:

参数 含义 当前调用中的值
INPROG 要执行的程序名称或路径 'BATCH'
STATE 是否在目标程序完成后返回调用者,可取 'WAIT' 或 'PASS' 'PASS'
PATH 是否直接使用给定的可执行文件路径,是整数开关,并不是路径字符串 INT2(0)

其中,WAIT 和 PASS 的区别,在于是否回到当前调用点:

  • 'WAIT':等待目标程序完成,接收返回状态,再继续执行调用语句后面的代码
  • 'PASS':将后续执行交给指定程序,不再返回当前调用点

例如:

CALL RUN_PROG ( 'PROC', 'WAIT', INT2(0) )
! proc 正常结束后,继续执行这里的代码

而下面这个调用正常情况下不会返回:

CALL RUN_PROG ( 'BATCH', 'PASS', INT2(0) )
! 正常情况下,不会继续执行这里的代码

PASS 描述的是执行控制如何交接。当前进程可能留在 RUN_PROG 内部承担调度工作,也可能把接续请求交给父进程后退出。

3 从 enter 到 batch

发送端 enter 先准备好运行字符串,再调用:

CALL USE_BUFFER ( NEWBUF, INT2(64), 'OWC' )

这表示打开缓冲区访问,写入 NEWBUF 中的 64 个双字节整数,然后结束本次访问。每个元素占 2 字节,所以一次发送的数据长度是 128 字节。INT2(64) 则把长度参数明确转换为双字节整数,与子程序的参数声明一致。发送端的运行字符串保存在 NEWSTR 中,NEWSTR 和 NEWBUF 通过 EQUIVALENCE 共用存储。因此,写入 NEWBUF,就是把 NEWSTR 所占的那 128 字节写入管道。

随后执行 RUN_PROG。对于这次由 enter 发起的调用,主要过程如下。

  • 首先,确定可执行文件的位置,并准备启动参数。

RUN_PROG 先确定 batch 可执行文件的位置,检查文件是否存在。由于本次使用 'PASS',RUN_PROG 会在运行状态中设置一个标志,表示 batch 是通过 PASS 方式启动的:

CALL SBIT ( PRE_IP(2), INT2(4), INT2(1) )

这个标志保存在 PRE_IP(2) 的第 4 个二进制位中,供后续程序调度使用。

  • 随后,将可执行文件路径、四个状态字的十进制文本和两字符的用户标识拼成启动命令。其结构可以表示为:
<batch可执行文件路径> <状态字1> <状态字2> <状态字3> <状态字4> <用户标识>

五个参数依次来自 PRE_IP(1)、PRE_IP(2)、PRE_IP(3)、PRE_IP(4) 和 PRE_LETRS。其中尖括号里的文字只是说明占位符。运行字符串已经写入管道,不在这五个命令行参数中。

  • 接着,启动子进程,并等待它结束。

RUN_PROG 调用底层函数:

IERR = EXECUTE_D ( PTR_CH(CMD), ARGC, ARGV, STATUS, PTRDEV )

EXECUTE_D 的实现位于 psolve/libs/fclib/execute_d.c。它使用 vfork() 创建子进程,再通过 execvp() 执行目标程序;父进程等待子进程结束。因此,在这条调用链中,enter 仍然存在,只是停留在 RUN_PROG 内部等待 batch。

  • batch 启动后,先恢复运行状态,再读取缓冲区数据。

batch_main.f 调用:

CALL PRE_PROG()

PRE_PROG 通过 RMPAR → UNPACK_RMPAR 读取上述命令行参数,初始化当前进程中的 PRE_IP 等状态。

随后,在由 enter 启动的分支中执行:

CALL USE_BUFFER ( ISTRNG, INT2(64), 'ORC' )

这次执行的是打开、读取、关闭,将收到的 128 字节写入 ISTRNG。发送端和接收端必须约定相同的数据长度。这两个调用中的 64 指的是双字节整数的个数,实际传输的是 128 字节。USE_BUFFER 按指定长度搬运数据,不会根据字符串的有效长度自动缩短传输内容。

到这里,很自然地就产生了一个疑问:传输接口使用整数数组,为什么 batch 最后得到的却是字符串?

关键在 psolve/include/ba2cm.i 中的这组声明:

CHARACTER STRING*128
INTEGER*2 ISTRNG(64)
EQUIVALENCE (ISTRNG, STRING)

EQUIVALENCE 让 ISTRNG 和 STRING 共用同一块内存。前者把这块内存看作 64 个双字节整数,后者把它看作 128 个字符。两种表示的总长度正好相同。因此,数据读入 ISTRNG 后,程序就可以通过 STRING 访问其中的字符。这里改变的是访问同一批字节的方式,没有逐个进行整数到字符的转换,也没有另外复制一份字符串。这种写法的作用是让同一份数据既能传给要求整数数组的 USE_BUFFER,又能被后面的代码作为字符串解析,同时省去中间复制。

随后,batch 内部的 BATCH 子程序调用 CTRLS,后者从 STRING 中拆出处理进程编号、控制文件名等字段,再继续读取控制文件。需要区分的是,这次传递的运行字符串包含控制文件的路径;控制文件的具体内容由 CTRLS 等后续代码从文件中读取。

batch 完成后,还要向父进程报告结束状态。batch_main.f 正常执行到结尾时调用 END_PROG()。它把结束记录写入状态管道,然后退出。父进程中的 RUN_PROG 在等待结束后读取这条记录;在这次正常完成、没有后续程序的 PASS 调度中,它最终退出,而不会返回 enter 中调用语句后面的位置。

4 管道通信与程序调度

EQUIVALENCE 让同一进程中的变量共用内存;enter 和 batch 之间的数据,则通过操作系统提供的管道传递。管道可以暂存数据,所以 enter 能先写入运行字符串,再启动 batch 来读取。MAKE_PIPES 建立了两条管道,分别承担不同的任务:

管道 作用
数据管道 由 USE_BUFFER 读写,传递运行字符串等数据
状态管道 由 RUN_PROG、END_PROG 使用,报告程序是否结束、是否需要继续运行其他程序

USE_BUFFER 中的 O 和 C 只表示开始和结束本次缓冲区访问,不会创建或关闭底层管道。实际的数据读写由 FC_WRITE 和 FC_READ 完成。

程序调度可以结合前面的例子来理解:enter 启动 batch 后,在 RUN_PROG 内部等待;batch 正常完成时调用 END_PROG(),把结束状态交回。enter 收到结束记录后,也结束本次运行。如果 batch 要通过 PASS 接着运行另一个程序,就把下一程序名和运行状态交给父进程,然后退出,由父进程继续启动那个程序。这就是前面所说的“交出后续执行”。

5 代码中的小瑕疵

原代码在发送前还计算了:

NWORDS = (TRIMLEN(NEWSTR)+1)/2

这行计算的是容纳字符串有效内容所需的 word 数,奇数长度时向上取整。但随后的调用使用固定的 INT2(64),没有使用 NWORDS,所以实际发送长度仍然是 128 字节。这里的长度计算并没有参与后续传输,也许是历史遗留的代码。

此外,psolve/libs/fclib/execute_d.c 中有一处条件写反了:

else if ( WIFSIGNALED(status_exec) == 0 )
    *status = WTERMSIG(status_exec) + 10000;

WIFSIGNALED 用于判断子进程是否被信号终止,例如被 kill 命令结束。满足这一条件时,它应当为真,因此这里应去掉 == 0:

else if ( WIFSIGNALED(status_exec) )
    *status = WTERMSIG(status_exec) + 10000;

原代码会把这种结束情况的 STATUS 设为 0,使 RUN_PROG 漏掉“子进程被信号终止”的提示。修改后,可分别用正常退出和被 SIGTERM 终止的子进程验证:前者应保留原退出码,后者应返回信号编号加 10000 的状态值。