输出到 stdout 有效,但重定向到文件或 fifo 时失败

输出到 stdout 有效,但重定向到文件或 fifo 时失败

我有一个 Wifi 天线,我想优化它的位置。对于这个建议,我想实时可视化当前的延迟。

我编写了一行脚本来输出 ping 延迟:

纬度:

ping 8.8.8.8|gawk '/64 bytes/{ match($7,/[0-9.]+/,arr); print((i++),"  ",arr[0]);}'

我想用 gnuplot 绘制它:

plot "<lat.sh"

那是行不通的。我发现 lat.sh 在终端上显示它应该显示的内容,例如:

$ ./lat.sh 
0    47.7
1    25.5
2    15.8
3    16.7

但是,将其输出到管道是行不通的。./lat.sh > outfile./lat.sh|tee outfile./lat.sh &> outfile不向终端打印任何内容,但文件(或管道)保持为空。 gnuplot 管道也是如此。我真的很困惑。

答案1

作为麦克塞夫解释下面,这是一个缓冲问题。解决这个问题的一种方法是强制awk立即使用fflush()(其余的只是您正在做的事情的简化版本):

ping 8.8.8.8 | gawk -F'[= ]' '/^64/{print i++,$(NF-1); fflush()}'

fflush()调用将强制awk在输出可用时立即打印输出,而无需等待其缓冲区填满。

或者,您可以尝试运行ping特定次数。例如,10:

ping  -c 10 8.8.8.8 | gawk ...

答案2

看来问题出在这里awk。您的程序省略了输出 - 修剪它。这可能是一件非常有用的事情,但是当您这样做时,您会减少排队的输出awk。大多数 UNIX 程序使用stdio.h (这是大多数 UNIX 程序)将根据输出设备的类型缓冲其输出。当输出设备是终端时,大多数 UNIX 程序都会对其输出进行行缓冲 - 并且write()每行执行一次。几时不是尽管,(例如当它是管道时)所述程序将缓冲每次写入的一些定义的字节数。

这是标准行为,通常是好的事物。每次调用write()都是系统调用 - 它是系统内核核心和最基本功能的挂钩。 UNIX 系统是分时的- 内核根据需要将 CPU 的处理时间分配给每个请求进程。所以它支付为了提高效率并将您的 I/O 分成可管理的块。这就是应用程序缓冲的原因。

这就是awk我们正在做的事情。这个问题是混合在一起的,因为它对输入的过滤将其自身的输出减少到每秒大约 8 或 10 字节左右。如果awk将输出缓冲成 4kb 块,(这是常见的),并且它每秒仅生成大约 10 个字节的输出,因此最终每 6 分钟只写入一次。除非您正在检查大陆漂移规模的某些频率,否则它不会成为测量延迟的非常有用的应用程序。

有一些方法可以处理这个问题。例如,您可以将管道包装在 pty 中 -screen可以为您和其他几个应用程序执行此操作。可能有awk-native 选项来处理这个问题 - 但我不知道(即使特登肯定会。 GNU 的工具可以stdbuf注入 libc 调用,以便在程序执行时设置输出缓冲区。这通常非常有效,但如果稍后执行的程序在执行过程中调整其缓冲区,则不会很有用。

现在我责备awkping也可能缓冲输出。然而 - 至少在我的 Linux 系统上 - 当我这样称呼它时:

ping -O -n 8.8.8.8 | ...

...不是这种情况。如果您好奇的话,我设法让它在每个输出行可用时一致地写入每个输出行,仅使用标准 UNIX 命令的非常基本的 POSIX 选项。

最简单:

ping -On 8.8.8.8 |
sed -u 's/^64.*=\(.*\) ttl.*=/\1  /' |
more pipeline

...将与 GNU、BSD 或 AST 一起使用sed。另外,还有...

ping -On 8.8.8.8 |
sed 's/^64.*=\(.*\) ttl.*=/\1  /w target_file' |
more pipeline

...它将在任何可能的处理之后将所有输入行复制到标准输出 - 这很可能是块缓冲的。然而,它会立即将每次成功替换w的结果写入s///目标文件w任何/所有指定仪式文件的立即仪式w是 POSIX 规范的sed行为 - 也是如此(几乎)对于read 文件也是如此。作为该主题的细微变化:

ping -On 8.8.8.8 |
sed 's/^64.*=\(.*\) ttl.*=/\1  /w /dev/fd/1' |
more pipeline

在支持使用链接寻址文件描述符的系统上/dev,上述内容应该会通过标准 shell 管道产生行缓冲输出 - 至少就目前而言sed是这样。对于那些不直接支持/dev/fd链接的系统,它仍然是一个流行的sed处理功能/dev/std(in|out|err)- 即使是旧系统也是minised如此。用于-n完全禁用默认的 stdout 输出。

这是基于该构造的更灵活且更复杂的解决方案:

mkfifo  /tmp/ipipe    /tmp/opipe
exec 9<>/tmp/opipe 8<>/tmp/ipipe
trap '  printf \\nTTY:STOP\\n >&8' INT
sed -n 's/^64.*=\(.*\) ttl.*=/\1\t/
        /^TTY:START$/,/^TTY:STOP$/{
                /^TTY/d;w /dev/tty
};      /^PIP:START$/,/^PIP:STOP$/{
                /^PIP/d;w /tmp/opipe
}'      <&8     >/dev/null 2>&1 & SEDPID=$!
ping -On 8.8.8.8       >&8 2>&8 & PNGPID=$!
printf \\nPIP:START\\n >&8;rm /tmp/?pipe

现在设置了一个后台调度程序进程,该进程将始终以行缓冲方式写入其输出。它将写入第一次调用时保存的文件描述符,该描述符对应于父 shell 的<>&9描述符,或者写入 tty,具体取决于您是否向其发送命令:

PIP:START
TTY:START

您可以发送这些信息,例如:

printf \\nPIP:START\\n >&8
printf \\nTTY:START\\n >&8

它将根据您的需要同时处理两者。父 shell 已trap配置为向其发送命令TTY:停止当它收到一个INT信号 - 所以只需按下CTRL+C您键盘上的内容应该足以使其在任何给定时间停止写入您的终端。不过,您需要明确告诉它停止写入管道,trap如果您愿意,也可以在 a 中处理。你可以告诉它停止,如下所示:

printf \\nPIP:STOP\\n >&8

sed将要总是 w写入行缓冲区 - 无论它如何缓冲其标准输出。这样,您就可以在ping写入后立即读取其写入的每一行。您可以通过调用另一个进程来读取文件描述符 9 来完成此操作 - 这是画中画 sed当指示这样做时写入 - 或者您可以将其直接打印到终端。调用另一个进程来读取画中画做就是了:

cat <&9

...或类似的。另请注意,一旦可能需要的所有进程在这些管道上明确建立了其文件句柄,我就会明确为所使用的管道创建文件系统rm链接。mkfifo这意味着您将无法通过文件系统与涉及的进程进行交互 - 所有 IPC 必须通过父 shell 的描述符 8 和 9 进行协调。后台进程sedping进程的 PID 保存在 shell 变量$PNGPID和中$SEDPID。两者都可以通过 来解决kill

现在已经运行上面的脚本示例,然后发送电传打字机:开始命令结果是:

[mikeserv@localhost ~]$ printf \\nTTY:START\\n >&8

[mikeserv@localhost ~]$ 218 24.2 ms
219 21.2 ms
220 23.1 ms
221 21.3 ms
222 21.9 ms
^C

[mikeserv@localhost ~]$

...但是这两个进程仍在运行 -sed只是没有在任何地方写入输出。

ps -Fp "$SEDPID" "$PNGPID"
UID        PID  PPID  C    SZ   RSS PSR STIME TTY      STAT   TIME CMD
mikeserv 31601 28945  0  2143  1712   4 14:22 pts/0    SN     0:00 sed -n s/^64.*q=\(.*\) ttl.*=/\1\
mikeserv 31602 28945  0  2106   740   3 14:22 pts/0    SN     0:00 ping -On 8.8.8.8

答案3

Unix 在管道方面似乎确实很有限。看来我已经先将数据写入文件中,以便用它做任何有意义的事情。如果我重写整个过程,虚拟终端可能是解决方案。但我不会使用花哨的 GUI,而是简单地使用终端并使用 awk 渲染进度条。

ping -i 0.5  8.8.8.8 | gawk -F'[= ]' '/^64/{ for(i=0;i<($(NF-1)/3);i++) printf(" "); printf("|");  for(i=120;i>$(NF-1);i--) printf(" "); printf("\r");}'

将 120 替换为终端的宽度。

相关内容