V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Distributions
Ubuntu
Fedora
CentOS
中文资源站
网易开源镜像站
jinqzzz
V2EX  ›  Linux

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

  •  
  •   jinqzzz · 11 天前 · 1843 次点击

    问题

    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 管道的实现有关吗?如果还没有执行完管道符前的命令,就去执行管道符后的命令了,听起来不太合理。
    35 条回复    2024-05-17 20:31:59 +08:00
    tool2dx
        1
    tool2dx  
       11 天前   ❤️ 1
    tee 应该是没判断管道是否关闭。

    你可以用 ai 帮你写一个命令替代 tee ,确认输入管道完全关闭后,再写入文件。
    lieh222
        2
    lieh222  
       11 天前 via Android   ❤️ 1
    tee 跟排序进程是同时启动的吧,tee 不加-a 打开文件的时候就清空了,但是 sort 读文件失败?
    lieh222
        3
    lieh222  
       11 天前 via Android   ❤️ 1
    在 tee 前面加 strace 你就可以看到,tee 进程和 sort 并行启动,tee 启动就会用 w 模式打开文件,这一步已经清空文件了,sort 再读就会为空进程退出
    hxy100
        4
    hxy100  
       11 天前   ❤️ 1
    曾经我也有相同的疑问,tee 行为相当之迷惑。期待大佬的权威解答
    zhuisui
        5
    zhuisui  
       11 天前   ❤️ 2
    bash 的 pipeline 只声明了会将命令程序的输出和输入连接起来,可没声称这些命令的执行开始和结束顺序。
    sandylaw
        6
    sandylaw  
       11 天前   ❤️ 2
    为什么会有不确定的行为:
    当你使用 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
        7
    aloxaf  
       11 天前   ❤️ 3
    管道是流式的,如果你写「 sort foo.txt | tee foo.txt 」,「 sort foo.txt 」和「 tee foo.txt 」会一起启动,而后者启动时会清空 foo.txt ,导致前者读不到东西。

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

    另外,这种问题其实可以写个 c 程序验证一下。其他语言在操作文件之前的准备工作可能久一些,会影响观察结果。
    GrayXu
        19
    GrayXu  
       11 天前   ❤️ 1
    @vituralfuture #16 op 的操作是依赖的,如果还想要流式处理就不能用这样简单用 pipe 组合。sponge 就是拿来保证生产者可以一直往里塞
    nuffin
        20
    nuffin  
       11 天前   ❤️ 1
    这种情况下,用多个文件是最合理的。尤其是文件比较大的时候。因为删掉一个文件是直接操作文件系统的分配表,不会真的去写个大文件,把新文件改名成原来的文件名也是一样的文件系统目录结构修改。另外,如果一个文件的处理过程比较长,那么在这时候系统重启或者断电的时候,都操作一个文件的方式就会导致文件的状态不可知,用临时文件的方式可以重复执行很多遍,都是同样的结果,即使中间有失败的情况也无所谓,因为在完整流程完成之前,新的文件没有“提交”。
    nuffin
        21
    nuffin  
       11 天前   ❤️ 1
    我其实觉得 sponge 不够 “管道”,因为它断流了。
    sendi
        22
    sendi  
       11 天前   ❤️ 1
    sort rpc.sh > >(tee rpc.sh)
    可以使用进程替换来实现呢
    sendi
        23
    sendi  
       11 天前   ❤️ 1
    @sendi 也是类似于临时文件 但是 bash 在处理过程中有使用缓存或者临时存储
    sendi
        24
    sendi  
       11 天前   ❤️ 1
    https://www.yuque.com/wangsendi/hmeaaw/yhti79b6guut4yt5
    可以参考 awk 的 结尾 1<>a 这样的模式 这样就不会截留了
    jinqzzz
        25
    jinqzzz  
    OP
       11 天前
    @mohumohu 我的提问是不太准确,sort foo.txt | tee foo.txt 只是一种简化的场景,它代表了「如何在修改文件内容的同时,写入原文件」和「 | tee 的用法」 , 和 sort 没有太大关系。
    想了想,我为什么都没想过 sort 有 -o ,因为更常见的场景是 cat foo.txt | xxx | xxx | tee foo.txt ,显然没人会去奢望前边有一个 -o 可以解决所有的问题...
    jinqzzz
        26
    jinqzzz  
    OP
       11 天前
    @sendi 进程替换这种用法还没见过,学习了,确实挺好用。
    但是我这里测试看有一个小问题,短时间内执行多次 sort foo.txt > >(tee foo.txt ) ,会有很低的概率把 foo 清空,如果用 for 批量执行,清空的概率就非常高了 for i in {01..20}; do echo $i; sort foo > >(tee foo ) ; done
    jinqzzz
        27
    jinqzzz  
    OP
       11 天前
    @jinqzzz 还没写完就回车里,不过这种场景我现在还遇不到,就不太关心了。 1<> 是没有这个问题的,很好用
    jinqzzz
        28
    jinqzzz  
    OP
       11 天前
    回车里 -> 回车了
    jinliming2
        29
    jinliming2  
       11 天前 via iPhone   ❤️ 1
    @jinqzzz sort 有个比较特殊的点是,它必须一次性把所有内容都读入才能开始输出,因为有可能最后一行的内容被排序到最前面。在输出之前,内容都是要读到内存里的,处理大文件要足够的内存。
    所以可以用一些方法来延迟 tee 创建输出流的时间,确保 sort 已经读取所有内容。
    如果是 cat xxx | tee xxx 这样的,cat 是支持流式处理的,也就是读多少输出多少,读取的内容可能比内存都要大,这种情况 sort 命令都肯定要失败的。这种就不建议延迟 tee 了,还是换个文件名来写,确保读取写入全部完成之后再做文件替换是比较稳妥的。
    julyclyde
        30
    julyclyde  
       9 天前
    @zhuisui 从技术上讲,用管道把俩进程连接起来,其打开顺序只能有一种吧
    zhuisui
        31
    zhuisui  
       9 天前
    @julyclyde 理论上 bash 对管道没有这样的声明,实际上自然也不能做这样的假定。
    而且先开输入再开输出再连接它们在技术上是完全可行的。
    zhuisui
        32
    zhuisui  
       9 天前
    @jinqzzz 因为 process substitution 也可能用 pipe 实现,道理一样
    https://www.gnu.org/software/bash/manual/html_node/Process-Substitution.html
    julyclyde
        33
    julyclyde  
       9 天前
    @zhuisui 先开输入再开输入似乎不能保证完整性吧?
    zhuisui
        34
    zhuisui  
       9 天前
    @julyclyde
    先开 a 输出再开 b 输入把 a 接到 b ,就需要 pipe 先 buffer 一下 a 输出;反过来直接对接管道就行了,甚至不需要 buffer 。
    你说呢
    onnethy
        35
    onnethy  
       5 天前
    @aloxaf 牛逼
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   4679 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 03:46 · PVG 11:46 · LAX 20:46 · JFK 23:46
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.