我偶然发现一个命令有时有效,有时无效,即使在bash
shell 中快速连续执行多次(我还没有测试其他 shell 中的行为)。该问题已定位为读取管道末尾的语句BEGIN
块中的变量。awk
在某些执行期间,变量在BEGIN
块中被正确读取,而在其他执行期间,操作失败。假设这种异常行为可以被其他人重现(并且不是我的系统某些问题的结果),它的不一致可以解释吗?
将以下文件作为输入tmp
:
cat > tmp <<EOF
a a
b *
aa a
aaa a
aa a
a a
c *
aaa a
aaaa a
d *
aaa a
a a
aaaaa a
e *
aaaa a
aaa a
f *
aa a
a a
g *
EOF
在我的系统上,管道
awk '{if($2!~/\*/) print $1}' tmp | tee >(wc -l | awk '{print $1}' > n.txt) | sort | uniq -c | sort -k 1,1nr | awk 'BEGIN{getline n < "n.txt"}{print $1 "\t" $1/n*100 "\t" $2}'
将产生正确的输出:
4 28.5714 a
4 28.5714 aaa
3 21.4286 aa
2 14.2857 aaaa
1 7.14286 aaaaa
或错误消息:
awk: cmd. line:1: (FILENAME=- FNR=1) fatal: division by zero attempted
怎样才能一个命令可能当不涉及随机数生成并且中间没有对环境进行任何更改时,连续运行两次时会给出不同的输出吗?
为了证明这种行为是多么荒谬,请考虑在循环中连续执行上述管道十次所生成的输出:
for x in {1..10}; do echo "Iteration ${x}"; awk '{if($2!~/\*/) print $1}' tmp | tee >(wc -l | awk '{print $1}' > n.txt) | sort | uniq -c | sort -k 1,1nr | awk 'BEGIN{getline n < "n.txt"}{print $1 "\t" $1/n*100 "\t" $2}'; done
Iteration 1
awk: cmd. line:1: (FILENAME=- FNR=1) fatal: division by zero attempted
Iteration 2
4 28.5714 a
4 28.5714 aaa
3 21.4286 aa
2 14.2857 aaaa
1 7.14286 aaaaa
Iteration 3
4 28.5714 a
4 28.5714 aaa
3 21.4286 aa
2 14.2857 aaaa
1 7.14286 aaaaa
Iteration 4
awk: cmd. line:1: (FILENAME=- FNR=1) fatal: division by zero attempted
Iteration 5
awk: cmd. line:1: (FILENAME=- FNR=1) fatal: division by zero attempted
Iteration 6
awk: cmd. line:1: (FILENAME=- FNR=1) fatal: division by zero attempted
Iteration 7
4 28.5714 a
4 28.5714 aaa
3 21.4286 aa
2 14.2857 aaaa
1 7.14286 aaaaa
Iteration 8
awk: cmd. line:1: (FILENAME=- FNR=1) fatal: division by zero attempted
Iteration 9
4 28.5714 a
4 28.5714 aaa
3 21.4286 aa
2 14.2857 aaaa
1 7.14286 aaaaa
Iteration 10
awk: cmd. line:1: (FILENAME=- FNR=1) fatal: division by zero attempted
注意:我还尝试在读取变量后关闭文件( awk close
),以防问题与文件保持打开状态有关。然而,不一致的输出仍然存在。
答案1
您的重定向存在竞争条件。这:
>(wc -l | awk '{print $1}' > n.txt)
并行运行:
awk 'BEGIN{getline n < "n.txt"}...'
稍后在管道中。有时,当程序开始运行时,n.txt
仍然为空。awk
这(间接)记录在 Bash 参考手册中。在一个管道:
管道中每个命令的输出通过管道连接到下一个命令的输入。也就是说,每个命令都会读取前一个命令的输出。此连接在命令指定的任何重定向之前执行。
进而:
管道中的每个命令都在其自己的子 shell 中执行
(强调已添加)。全部管道中的进程启动,其输入和输出连接在一起,无需等待任何早期程序完成甚至开始执行任何操作。在那之前,流程替代与>(...)
是:
与参数和变量扩展、命令替换以及算术扩展同时执行。
这意味着运行该wc -l | awk ...
命令的子进程会提前启动,并且重定向会n.txt
在此之前清空,但awk
导致错误的进程会在不久之后启动。这两个命令都是并行执行的——这里将有多个进程同时运行。
在命令的输出写入之前awk
运行其BEGIN
块时会发生错误wc
n.txt
。在这种情况下,n
变量为空,因此用作数字时为零。如果BEGIN
文件填写后运行,则一切正常。
何时发生这种情况取决于操作系统调度程序,以及哪个进程首先获得一个槽,从用户的角度来看,这本质上是随机的。如果决赛awk
提前运行,或者wc
管道安排得稍晚一些,那么当awk
开始执行其工作时文件仍然是空的,整个事情将会中断。这些进程很可能实际上同时在不同的核心上运行,这取决于哪个核心首先进入争用点。您将得到的效果可能是该命令经常工作,但有时会因您发布的错误而失败。
一般来说,管道仅在它们只是管道时才是安全的 - 标准输出到标准输入很好,但因为进程是并行执行的依赖任何其他通信渠道的排序都是不可靠的,如文件,或任何一个进程的任何部分在另一个进程的任何部分之前或之后执行,除非它们通过读取标准输入锁定在一起。
这里的解决方法可能是在需要它们之前完成所有文件写入:在行末尾,保证整个管道及其所有重定向在下一个命令运行之前已完成。该命令永远不会可靠,但如果您确实需要它在这种结构中工作,您可以在运行最终命令之前插入延迟(sleep
)或循环直到n.txt
非空,awk
以增加事情按照您的方式工作的机会想。
答案2
pipe
表达式 inprocess substitution
会导致竞争条件,bash
而ksh
,zsh
则不会。
这里的主要问题是zsh
等待,bash
而不是等待。
您可以查看更多详细信息这里。
快速修复,添加sleep 1
您的awk
以使其n.txt
始终可用:
awk 'BEGIN{system("sleep 1");getline n < "n.txt"};{print $1 "\t" $1/n*100 "\t" $2}'
答案3
竞争条件已经确定。但如果你想要一个更简单的解决方案,你不需要单独wc
来计算记录,awk
可以这样做:
awk '{if($2!~/\*/){print $1;++n}END{print n >"n.txt"}' tmp | sort | uniq -c ...
除此之外,只要值适合内存,awk
就可以进行计数,并且还可以进行 x/n 计算,但可能以“随机”顺序输出;sort|uniq -c
使用 match/action 也更整洁:
awk '$2!~/\*/{++k[$1];++n} END{for(i in k){print k[i]"\t"k[i]/n*100"\t"i}}' tmp | sort -k1nr
或者最近GNU awk
您可以进行设置PROCINFO["sorted_in"]="@ind_num_desc"
,以便for
使用正确的顺序,并且不需要sort
.