在 C/C++ MPI 应用程序中获取终端 Window 大小

Get Terminal Window size in an C/C++ MPI application

从 C/C++ 程序获取终端宽度已经在 SO 上进行了广泛的解释,例如here.

但是,当使用 MPI 启动相同的代码时,行数和列数 returns 0(让 foo.c 是包含上述答案代码的源文件):

# gcc foo.c
# mpirun -n 1 ./a.out
lines 0
columns 0

有没有办法获取连接到 MPI stdout 的终端的宽度?

我想避免为此拖入像 ncurses 这样的额外库。此外,我主要对 Linux.

的答案感兴趣

总结:

增强您的并行程序,以便您可以传递终端大小(如 rows=cols= 命令行参数,或终端路径(如 tty= 命令行参数) .

example.c:

// SPDX-License-Identifier: CC0-1.0
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>

static int  rows = 0;
static int  cols = 0;

int main(int argc, char *argv[])
{
    /*
     * Beginning of added code
    */
    if (isatty(STDERR_FILENO)) {
        struct winsize  w;
        if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == 0 && w.ws_row > 0 && w.ws_col > 0) {
            rows = w.ws_row;
            cols = w.ws_col;
        }
    }
    for (int arg = 1; arg < argc; arg++) {
        if (!strncmp(argv[arg], "tty=", 4)) {
            const int  fd = open(argv[arg] + 4, O_RDWR | O_NOCTTY);
            if (fd != -1) {
                struct winsize  w = { .ws_row = 0, .ws_col = 0 };
                if (ioctl(fd, TIOCGWINSZ, &w) == 0 && w.ws_row > 0 && w.ws_col > 0) {
                    rows = w.ws_row;
                    cols = w.ws_col;
                    if (isatty(STDOUT_FILENO)) {
                        ioctl(STDOUT_FILENO, TIOCSWINSZ, &w);
                    }
                }
                close(fd);
            }
        } else
        if (!strncmp(argv[arg], "rows=", 5)) {
            int  val;
            char dummy;
            if (sscanf(argv[arg] + 5, "%d %c", &val, &dummy) == 2 && val > 0) {
                rows = val;
                if (isatty(STDOUT_FILENO) && rows > 0 && cols > 0) {
                    struct winsize  w = { .ws_row = rows, .ws_col = cols };
                    ioctl(STDOUT_FILENO, TIOCSWINSZ, &w);
                }
            }
        } else
        if (!strncmp(argv[arg], "cols=", 5)) {
            int  val;
            char dummy;
            if (sscanf(argv[arg] + 5, "%d %c", &val, &dummy) == 2 && val > 0) {
                cols = val;
                if (isatty(STDOUT_FILENO) && rows > 0 && cols > 0) {
                    struct winsize  w = { .ws_row = rows, .ws_col = cols };
                    ioctl(STDOUT_FILENO, TIOCSWINSZ, &w);
                }
            }
        }
    }
    /*
     * End of added code
    */

    if (cols > 0 && rows > 0) {
        printf("Assuming terminal has %d columns and %d rows.\n", cols, rows);
    } else {
        printf("No assumptions about terminal size.\n");
    }

    if (isatty(STDOUT_FILENO)) {
        printf("Standard output is terminal %s", ttyname(STDOUT_FILENO));

        struct winsize  w;
        if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == 0) {
            printf(" reporting %d columns and %d rows.\n", w.ws_col, w.ws_row);
        } else {
            printf(".\n");
        }
    }

    sleep(5);

    return EXIT_SUCCESS;
}

编译并运行使用例如

mpicc -Wall -O2 example.c -o example
mpirun -n 1 ./example tty=$(tty)
mpirun -n 1 ./example rows=$(tput lines) cols=$(tput cols)

上面的程序有每个并行进程(上面只有一个,-n 2,更多的可以正常工作)报告它认为输出到的内容。

tty=$(tty) 形式很好 shorthand 当所有进程 运行 在此节点上(同一台计算机,当前 OS)。 rows=$(tput lines) cols=$(tput cols) 将当前终端尺寸记录到参数中,而不引用任何特定的 tty 设备,即使并行进程分散在许多机器上也能正常工作。


情况描述:

mpirun 是许多不同软件包可以提供的二进制文件之一。 运行 realpath $(which mpirun) 查看它当前在您的系统上引用的实际二进制文件。在基于 Debian 的系统上,您可以使用 dpkg-query -S $(realpath $(which mpirun)) 来查找当前提供它的软件包。我假设它是 openmpi,或者与 openmpi 兼容。

openmpi-bin 版本 2.1.1-8 为每个并行进程的标准输出流创建一个伪终端,但它将它们的行数和列数设置为零。尽管它将这些伪终端的内容转发到父进程的输出,但它不处理或转发 window 大小更改通知(SIGWINCH 信号)。它也不响应设备状态报告 ("3[6n") 等查询。

本质上,openmpi 的 mpi运行 (orte运行) 提供了一个你可以使用的伪终端,但它并没有正确地“维护”它;只是把它当作一个花哨的管道。不是问题,真的,但确实会导致 OP 和其他人观察到的副作用。

由于许多实际和历史原因,并且这是 MPI,可以安全地假设 ANSI 转义码(CSI 控制序列和扩展,例如光标移动、颜色、清除行等)工作.不,他们没有 quarantees,我只是从来没有在他们没有的环境中并行 运行 MPI 程序。如果输出到文件,则可以通过将文件发送到类似 xterm 的终端仿真器来“回放”它们,或者使用一些精心设计的 sed 表达式将其删除。


建议的解决方案:

  1. 运行 每个并行进程在单独的 xterm 中 window 使用 mpirun -n 1 -xterm -1 ./example

    因为每个并行进程都连接到它自己的 xterm 实例,所以它们将正确响应 window 大小更改。并且您可以假设 xterm 转义序列(包括大多数 ANSI 转义序列,如颜色、光标移动、滚动等)将起作用,即使您根本不使用 curses 或 terminfo。

  2. 运行 每个并行进程使用你自己的伪终端或 shim 使用 PATH=$PWD:$PATH mpirun -n 1 -xterm -1 ./example

    mpirun 使用 PATH 中的第一个 xterm 可执行文件,将其执行为 xterm -T WindowTitle -e command args。在上面的例子中,如果当前目录中有一个名为xterm的可执行文件,它将被使用。

    如果您编写自己的 shim 或启动器脚本,则可以忽略所有命令行参数,包括 -e。剩下的就是要执行的命令(您在该实例上的并行进程。)

    您可以使用它来使用您更喜欢 xterm 的终端仿真器,甚至可能是纯文本仿真器,例如 screen,或者您可以将标准输出(甚至标准输入)连接到您自己的伪终端复用器。话又说回来,您可以直接将这种多路复用器魔法集成到您自己的 MPI 程序中。

    xterm 垫片 运行 与您的并行进程完全相同:零等级,标准输入连接到连接到 mpi运行 标准输入的管道命令,以及所有其他带有连接到 /dev/null 的标准输入的等级;连接到由 mpi运行 父进程维护的伪终端(每个并行进程一个)的标准输出;和标准错误通过连接到 mpi运行 命令的标准错误的另一个管道连接。

    (如果您真的不喜欢 xterm,但有一个最喜欢的,请在评论中告诉我,我将在您自己的程序,您可以 运行 您的程序并行使用,每个程序都有自己的终端 window。)

  3. 通过命令行参数将用于输出的预期终端的信息传递给每个并行进程。

    example.c 上面支持两种方法:通过 rows=cols= 命令行参数直接指定它,或者通过指定将用于显示结果的终端 tty=。表单 tty=$(tty) 使用 tty 实用程序发出到该会话的控制终端的路径,因此最接近 "this terminal" 你可以通过,但只有在同一台机器和同一 OS 实例上并行处理 运行 时才有效。

    如果您的进程分布在多个节点(计算机)上,前者很有用,因为它实际上只传递终端大小而没有其他。

    因为 mpi运行 不会将 window 大小更改事件(SIGWINCH 信号)传递给并行进程,并行进程会停留在原始终端大小,如果终端大小发生变化, mpi运行 懒得告诉并行进程了。

    这并不意味着您的并行进程不能使用 MPI 广播通信器来更新他们对输出终端维度的共同理解。您甚至可以连接 SIGWINCH 信号处理程序,以便使用单个 long 作为有效负载(通过 sigqueue() 发送)的用户发起的 SIGWINCH 信号提取新的 window 大小并将其广播到所有并行进程.然后,您可以 运行 在同一终端上运行一个简单的后台程序,它将任何 SIGWINCH 信号反映给 mpi运行 进程的所有本地子进程 运行ning 作为与您相同的用户(my-signal-reflector ./program & mpirun -n 1 ./program ; wait).

我立即想到的大多数其他方案都可以或多或少地融入这三个方案之一。

如果您运行通过 SSH 连接执行这些操作,请考虑 运行在 screen 会话中执行 mpirun 命令,设置虚拟屏幕终端大小(使用屏幕 width <cols> <rows> 命令),并将这些尺寸作为命令行参数传递,类似于 example.c 中的 cols=rows= .这样你就可以离开并行进程 运行ning,但从 mpi运行 会话中分离出来;甚至关闭 SSH 连接,稍后再返回。当您连接到该屏幕会话时,如果您的实际 window 大小不同,输出不会出现乱码,您只能看到屏幕会话的一部分。开销很小,并且 if 用户被允许以交互方式 运行 MPI 会话(请询问您的系统管理员;不要只是 假设 它在集群前端这样做是可以的,因为通常有一个单独的“不是那个前端”用于交互式会话,因此前端可以保留用于编译和 data/result 传输,而不是陷入困境减少多余的并行进程 运行 交互),通常强烈建议在 screen 下这样做。

如果您只对某一具体案例感兴趣,请描述清楚。

最低 MPI 供应商和版本。

无论如何,如果您使用的是 Open MPI v4。0.x,您可以应用下面的补丁并重建(在 Open MPI 存储库的最新 v4.0.x 分支上测试。

diff --git a/orte/mca/iof/base/iof_base_setup.c b/orte/mca/iof/base/iof_base_setup.c
index 01fda21..29786d0 100644
--- a/orte/mca/iof/base/iof_base_setup.c
+++ b/orte/mca/iof/base/iof_base_setup.c
@@ -93,8 +93,14 @@ orte_iof_base_setup_prefork(orte_iof_base_io_conf_t *opts)
          * pty exactly as we use the pipes.
          * This comment is here as a reminder.
          */
+        struct winsize w, *wp;
+        if (0 > ioctl(STDOUT_FILENO, TIOCGWINSZ, &w)) {
+            wp = NULL;
+        } else {
+            wp = &w;
+        }
         ret = opal_openpty(&(opts->p_stdout[0]), &(opts->p_stdout[1]),
-                           (char*)NULL, (struct termios*)NULL, (struct winsize*)NULL);
+                           (char*)NULL, (struct termios*)NULL, wp);
     }
 #else
     opts->usepty = 0;