想请教一个关于 Bash 管道符和 tee 的问题

38 天前
 jinqzzz

问题

v2 大佬比较多,想在这里请教各位大佬一个困惑了我多年的问题:如何在一行命令里,排序文件内容并用 tee 写到原来的文件中?

使用临时文件

想要将 foo.txt 文件中的文本排序后依然保存到 foo.txt 文件中,需要先写到一个临时文件,然后将临时文件重命名为 foo.txt 。这也是一个比较常见的方案。

sort foo.txt | tee tmp-foo.txt
mv tmp-foo.txt foo.txt

我一直以来都以为 tee 无法直接回写(不知道这个词用的对不对)到文件,如果直接 sort foo.txt | tee foo.txt,那么 foo.txt 的内容会是空的。

tee 有时可以直接回写文件

但是最近我发现并不是这样,有时是可以回写成功的。文件足够小时有很大概率可以直接回写,比如下图可以看到回写成功了两次。而稍微大点的文件就比较难

和朋友们的讨论

在 v2 上发帖提问之前,我和同事、朋友们讨论过这个问题,我们有了一点点进展。

我们认为,没有回写成功,可能是因为文件还没读完就去写入。因此可以让写入晚一点,比如加一个 sleep ,这样确实可以解决,也是目前为止唯一的解决方案。

sort foo.txt | { sleep 1; tee foo.txt; }

这样听起来很合理,但是我们还是不理解为什么有时没有读完

  1. 我用 strace 分析过回写成功和失败的日志,没有发现任何区别。
  2. sort 命令是在内存中排序,读取速度和硬件性能有关,但是内存频率高、性能好、读的快,就可以成功写入,这也太不稳定了。
  3. 这可能和 Bash 管道的实现有关吗?如果还没有执行完管道符前的命令,就去执行管道符后的命令了,听起来不太合理。
2074 次点击
所在节点    Linux
35 条回复
tool2dx
38 天前
tee 应该是没判断管道是否关闭。

你可以用 ai 帮你写一个命令替代 tee ,确认输入管道完全关闭后,再写入文件。
lieh222
38 天前
tee 跟排序进程是同时启动的吧,tee 不加-a 打开文件的时候就清空了,但是 sort 读文件失败?
lieh222
38 天前
在 tee 前面加 strace 你就可以看到,tee 进程和 sort 并行启动,tee 启动就会用 w 模式打开文件,这一步已经清空文件了,sort 再读就会为空进程退出
hxy100
38 天前
曾经我也有相同的疑问,tee 行为相当之迷惑。期待大佬的权威解答
zhuisui
38 天前
bash 的 pipeline 只声明了会将命令程序的输出和输入连接起来,可没声称这些命令的执行开始和结束顺序。
sandylaw
38 天前
为什么会有不确定的行为:
当你使用 tee 写回到相同的文件时,tee 和 sort 的处理对文件的打开、读取、写入的时序会影响最终结果。这个命令有一个竞态条件的问题:

文件读写的时间差:sort 命令开始读取文件 foo 的内容,并进行排序。如果在 sort 读取完成之前 tee 就开始写入数据到 foo ,tee 的写入操作可能会覆盖 sort 还未读取的数据,导致数据丢失。

缓存和写入的延迟:UNIX 系统通常会使用缓存来优化读写操作。sort 可能还在处理数据,而 tee 可能已经开始写入,这种不同的处理速度可能导致 foo 文件的内容在未完全排序前就被覆盖。

**延迟写入**
如果你希望避免使用临时文件但仍需要确保数据的完整性,你可以考虑使用命令缓冲的方法,例如使用 Bash 的进程替换功能。这种方法可以让你在不创建物理临时文件的情况下处理数据。

下面是一个使用 Bash 进程替换来安全更新文件内容的例子:

```bash
sort -u foo | sponge foo
```
这里使用了 sponge 命令,它属于 moreutils 包的一部分。sponge 会读取所有的标准输入直到 EOF ,然后将数据写入到文件。这样可以避免在读取数据时同时写入同一个文件所引起的问题。

如果你的系统上还没有 sponge ,你可以通过包管理器安装 moreutils:
```bash
sudo apt-get install moreutils
```
延迟写入:由于 sponge 延迟写入,它避免了 tee 可能遇到的读写冲突问题,但代价是必须有足够的内存来存储所有输入,直到处理完成。
aloxaf
38 天前
管道是流式的,如果你写「 sort foo.txt | tee foo.txt 」,「 sort foo.txt 」和「 tee foo.txt 」会一起启动,而后者启动时会清空 foo.txt ,导致前者读不到东西。

对于这种需求,你应该使用 sponge 命令,它会等读取完所有数据再一次写入:sort foo.txt | sponge foo.txt
jinqzzz
38 天前
原来是我对管道的理解有误,感谢楼上各位大佬答疑,也感谢推荐 sponge 的大佬。
blessingsi
38 天前
sort 有个 -o 参数
sort -o foo.txt foo.txt
jinqzzz
38 天前
@blessingsi 惭愧,居然一直不知道有这个参数...
zhuisui
38 天前
pipeline 水管嘛,想想现实世界中的水管,谁会用水管储水,不都拿蓄水池嘛
所以你想把上游的输出全部放到水管里以后再放到下游的水龙头,就知道这样做是不合适的了吧
但是如果你真想干这种奇怪的事,那就是想办法造一个非常大非常粗的水管了
jinqzzz
38 天前
@zhuisui 大佬解释的非常形象
mohumohu
38 天前
这是个 XY 问题,sort 本来就可以-o 回写。
hellolinuxer
38 天前
sort foo.txt | cat | tee foo.txt 就 ok 了
hellolinuxer
38 天前
@jinqzzz 不是你对管道理解有误,而是你对 tee 理解有误,tee 是三通,所有你的使用方法不对,虽然 sort foo.txt | cat | tee foo.txt 也能解决,但是很明显 sort -o foo.txt foo.txt 资源使用上是最优解,但不是最安全的
vituralfuture
38 天前
bash 的管道,就是先创建一个 pipe ,然后 fork ,再分别设置输入输出,然后 exec ,并不是前一个命令执行完毕,后一个命令拿到它的输出,开始执行。应该理解为,read write 系统调用会在管道没有数据的时候阻塞,如果后一个命令需要读输入,而管道没有数据,就会阻塞等待前一个命令输出。而 read write 系统调用时,进程进入阻塞状态,而进程转为就绪状态时,何时执行又依赖于调度器,所以 bash 管道连接的两个命令,执行时序不容易预测
举一个例子,有个需求是给一个目录 xxx 加上 x 权限,然后 cd 进去,我有个朋友在初学 shell 时使用的命令是 chmod +x xxx | cd xxx
这个命令,有时能行,有时又 permission denied ,本质就是进程执行时序的问题。如果需要保证时序,可以用分号分成两个命令,也可以使用&&
geelaw
38 天前
@hellolinuxer #14 这是错误的,中间的 cat 和没写的执行效果是完全一样的,纯粹是浪费资源。
nuffin
38 天前
最后的问题 3 ,系统就是你说的那样,先创建两个进程,把他们用管道连起来,然后在分别 exec 执行管道两边的命令。所以一行里写若干个管道的话,实际上管道里的多个进程都是同时在执行的。需要注意的就是,因为 fork 多个进程,再去 exec 不同命令( sort ,tee 这些)的调度依赖于系统的进程调度,所以谁先执行文件操作这点,并不一定。所以有时候小文件能执行成功,可能就是前面的已经把文件内容读到内存里了,那这时候 tee 情况文件已经不影响结果了。

另外,这种问题其实可以写个 c 程序验证一下。其他语言在操作文件之前的准备工作可能久一些,会影响观察结果。
GrayXu
38 天前
@vituralfuture #16 op 的操作是依赖的,如果还想要流式处理就不能用这样简单用 pipe 组合。sponge 就是拿来保证生产者可以一直往里塞
nuffin
38 天前
这种情况下,用多个文件是最合理的。尤其是文件比较大的时候。因为删掉一个文件是直接操作文件系统的分配表,不会真的去写个大文件,把新文件改名成原来的文件名也是一样的文件系统目录结构修改。另外,如果一个文件的处理过程比较长,那么在这时候系统重启或者断电的时候,都操作一个文件的方式就会导致文件的状态不可知,用临时文件的方式可以重复执行很多遍,都是同样的结果,即使中间有失败的情况也无所谓,因为在完整流程完成之前,新的文件没有“提交”。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1039828

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX