我们可以使用以下两个得到相同的结果bash
,
echo 'foo' | cat
和
cat <<< 'foo'
我的问题是,就使用的资源而言,这两者之间有什么区别,哪一个更好?
我的想法是,在使用管道时,我们使用额外的进程echo
和管道,而这里的字符串仅使用文件描述符cat
。
答案1
该管道是在内核文件系统中打开的文件,不能作为磁盘上的常规文件进行访问。它仅自动缓冲到一定大小,并最终在满时阻塞。与源自块设备的文件不同,管道的行为非常类似于字符设备,因此通常不支持lseek()
并且从它们读取的数据无法像读取常规文件那样再次读取。
这里的字符串是在已安装的文件系统中创建的常规文件。 shell 创建文件并保留其描述符,同时立即删除其唯一的文件系统链接(所以删除它)在向文件写入/读取字节之前。内核将维护该文件所需的空间,直到所有进程释放该文件的所有描述符。如果从这样的描述符中读取的子级有能力这样做,则可以使用以下命令将其倒回lseek()
并再次阅读。
在这两种情况下,标记<<<
和|
代表文件描述符,而不一定是文件本身。您可以通过执行以下操作来更好地了解正在发生的情况:
readlink /dev/fd/1 | cat
...或者...
ls -l <<<'' /dev/fd/*
这两个文件之间最显着的区别是,here-string/doc 几乎是一次性事件 - shell 在向子文件提供读取描述符之前将所有数据写入其中。另一方面,shell 在适当的描述符上打开管道,并分叉子进程来管理管道的描述符 - 因此它被写入/读取同时在两端。
然而,这些区别只是一般来说真的。据我所知(实际上并没有那么远)几乎每个处理此处文档重定向<<<
的字符串简写的shell 都是如此<<
,只有一个例外yash
。不过yash
,busybox
、dash
、 和其他ash
变体确实倾向于使用管道支持此处文档,因此在这些 shell 中,两者之间确实没有什么区别。
好的——有两个例外。现在我正在考虑它,ksh93
实际上根本没有为 做管道,而是使用套接字处理整个业务 - 尽管它确实像大多数其他人一样|
为 做删除的临时文件。<<<*
更重要的是,它只是将管道的各个部分放在一个子shell环境这是 POSIX 的一种委婉说法至少它的作用就像一个子shell,所以甚至不做叉子。
事实是@PSkocik 的基准(这非常有用)这里的结果可能会有很大差异许多原因,其中大部分与实现相关。对于此处文档设置,最大的因素是目标${TMPDIR}
文件系统类型和当前缓存配置/可用性,以及要写入的数据量。对于管道来说,它将是 shell 进程本身的大小,因为副本是为所需的 fork 制作的。这样bash
就是糟糕的在管道设置时(包括$(
命令)
替换)- 因为它很大而且非常慢,但用ksh93
它几乎没有任何区别。
这是另一个小 shell 片段,用于演示 shell 如何为管道拆分子 shell:
pipe_who(){ echo "$$"; sh -c 'echo "$PPID"'; }
pipe_who
pipe_who | { pipe_who | cat /dev/fd/3 -; } 3<&0
32059 #bash's pid
32059 #sh's ppid
32059 #1st subshell's $$
32111 #1st subshell sh's ppid
32059 #2cd subshell's $$
32114 #2cd subshell sh's ppid
pipe_who()
管道调用报告的内容与当前 shell 中运行的报告之间的差异是由于(
子 shell在扩展时)
声明父 shell 的 pid 的指定行为造成的。$$
尽管bash
子 shell 肯定是单独的进程,但$$
特殊的 shell 参数并不是此信息的可靠来源。尽管如此,子 shell 的子sh
shell 并没有拒绝准确报告其$PPID
.
答案2
基准测试无可替代:
pskocik@ProBook:~
$ time (for((i=0;i<1000;i++)); do cat<<< foo >/dev/null; done )
real 0m2.080s
user 0m0.738s
sys 0m1.439s
pskocik@ProBook:~
$ time (for((i=0;i<1000;i++)); do echo foo |cat >/dev/null; done )
real 0m4.432s
user 0m2.095s
sys 0m3.927s
$ time (for((i=0;i<1000;i++)); do cat <(echo foo) >/dev/null; done )
real 0m3.380s
user 0m1.121s
sys 0m3.423s
对于大量数据:
TENMEG=$(ruby -e 'puts "A"*(10*1024*1024)')
pskocik@ProBook:~
$ time (for((i=0;i<100;i++)); do echo "$TENMEG" |cat >/dev/null; done )
real 0m42.327s
user 0m38.591s
sys 0m4.226s
pskocik@ProBook:~
$ time (for((i=0;i<100;i++)); do cat<<< "$TENMEG" >/dev/null; done )
real 1m26.946s
user 1m23.116s
sys 0m3.681s
pskocik@ProBook:~
$ time (for((i=0;i<100;i++)); do cat <(echo "$TENMEG") >/dev/null; done )
real 0m43.910s
user 0m40.178s
sys 0m4.119s
管道版本的安装成本似乎更高,但最终效率更高。
答案3
正如您已经注意到的,管道创建一个子流程,同时这里的字符串才不是。这在某些情况下可能很重要。
比较这些:
尽管:
grep $USER /etc/passwd | IFS=: read user x1 uid gid x2 home shell
echo $user $uid $gid
不显示任何内容。
IFS=: read user x1 uid gid x2 home shell <<<$(grep $USER /etc/passwd)
echo $user $uid $gid
显示:
myuser 1000 1000