想请教一个关于 Bash 管道符和 tee 的问题 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Distributions
Ubuntu
Fedora
CentOS
中文资源站
网易开源镜像站
filtrate
V2EX    Linux

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

  •  
  •   filtrate 2024-05-11 15:58:39 +08:00 2993 次点击
    这是一个创建于 522 天前的主题,其中的信息可能已经有所发展或是发生改变。

    问题

    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  
       2024-05-11 16:07:14 +08:00   1
    tee 应该是没判断管道是否关闭。

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

    对于这种需求,你应该使用 sponge 命令,它会等读取完所有数据再一次写入:sort foo.txt | sponge foo.txt
    filtrate
        8
    filtrate  
    OP
       2024-05-11 17:11:47 +08:00
    原来是我对管道的理解有误,感谢楼上各位大佬答疑,也感谢推荐 sponge 的大佬。
    blessingsi
        9
    blessingsi  
       2024-05-11 17:25:36 +08:00   2
    sort 有个 -o 参数
    sort -o foo.txt foo.txt
    filtrate
        10
    filtrate  
    OP
       2024-05-11 17:30:14 +08:00
    @blessingsi 惭愧,居然一直不知道有这个参数...
    zhuisui
        11
    zhuisui  
       2024-05-11 17:36:41 +08:00   3
    pipeline 水管嘛,想想现实世界中的水管,谁会用水管储水,不都拿蓄水池嘛
    所以你想把上游的输出全部放到水管里以后再放到下游的水龙头,就知道这样做是不合适的了吧
    但是如果你真想干这种奇怪的事,那就是想办法造一个非常大非常粗的水管了
    filtrate
        12
    filtrate  
    OP
       2024-05-11 17:43:34 +08:00
    @zhuisui 大佬解释的非常形象
    mohumohu
        13
    mohumohu  
       2024-05-11 18:21:45 +08:00   1
    这是个 XY 问题,sort 本来就可以-o 回写。
    hellolinuxer
        14
    hellolinuxer  
       2024-05-11 18:39:15 +08:00   1
    sort foo.txt | cat | tee foo.txt 就 ok 了
    hellolinuxer
        15
    hellolinuxer  
       2024-05-11 18:45:03 +08:00   1
    @jinqzzz 不是你对管道理解有误,而是你对 tee 理解有误,tee 是三通,所有你的使用方法不对,虽然 sort foo.txt | cat | tee foo.txt 也能解决,但是很明显 sort -o foo.txt foo.txt 资源使用上是最优解,但不是最安全的
    vituralfuture
        16
    vituralfuture  
       2024-05-11 18:54:00 +08:00 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  
       2024-05-11 20:07:47 +08:00 via iPhone   1
    @hellolinuxer #14 这是错误的,中间的 cat 和没写的执行效果是完全一样的,纯粹是浪费资源。
    nuffin
        18
    nuffin  
       2024-05-11 20:28:18 +08:00   1
    最后的问题 3 ,系统就是你说的那样,先创建两个进程,把他们用管道连起来,然后在分别 exec 执行管道两边的命令。所以一行里写若干个管道的话,实际上管道里的多个进程都是同时在执行的。需要注意的就是,因为 fork 多个进程,再去 exec 不同命令( sort ,tee 这些)的调度依赖于系统的进程调度,所以谁先执行文件操作这点,并不一定。所以有时候小文件能执行成功,可能就是前面的已经把文件内容读到内存里了,那这时候 tee 情况文件已经不影响结果了。

    另外,这种问题其实可以写个 c 程序验证一下。其他语言在操作文件之前的准备工作可能久一些,会影响观察结果。
    GrayXu
        19
    GrayXu  
       2024-05-11 20:30:40 +08:00   1
    @vituralfuture #16 op 的操作是依赖的,如果还想要流式处理就不能用这样简单用 pipe 组合。sponge 就是拿来保证生产者可以一直往里塞
    nuffin
        20
    nuffin  
       2024-05-11 20:36:22 +08:00   1
    这种情况下,用多个文件是最合理的。尤其是文件比较大的时候。因为删掉一个文件是直接操作文件系统的分配表,不会真的去写个大文件,把新文件改名成原来的文件名也是一样的文件系统目录结构修改。另外,如果一个文件的处理过程比较长,那么在这时候系统重启或者断电的时候,都操作一个文件的方式就会导致文件的状态不可知,用临时文件的方式可以重复执行很多遍,都是同样的结果,即使中间有失败的情况也无所谓,因为在完整流程完成之前,新的文件没有“提交”。
    nuffin
        21
    nuffin  
       2024-05-11 20:43:14 +08:00   1
    我其实觉得 sponge 不够 “管道”,因为它断流了。
    VK2CnSG6oL4S9749
        22
    VK2CnSG6oL4S9749  
       2024-05-11 23:37:10 +08:00   1
    sort rpc.sh > >(tee rpc.sh)
    可以使用进程替换来实现呢
    VK2CnSG6oL4S9749
        23
    VK2CnSG6oL4S9749  
       2024-05-11 23:38:54 +08:00   1
    @sendi 也是类似于临时文件 但是 bash 在处理过程中有使用缓存或者临时存储
    VK2CnSG6oL4S9749
        24
    VK2CnSG6oL4S9749  
       span class="ago" title="2024-05-11 23:56:28 +08:00">2024-05-11 23:56:28 +08:00   1
    https://www.yuque.com/wangsendi/hmeaaw/yhti79b6guut4yt5
    可以参考 awk 的 结尾 1<>a 这样的模式 这样就不会截留了
    filtrate
        25
    filtrate  
    OP
       2024-05-12 07:59:29 +08:00
    @mohumohu 我的提问是不太准确,sort foo.txt | tee foo.txt 只是一种简化的场景,它代表了「如何在修改文件内容的同时,写入原文件」和「 | tee 的用法」 , 和 sort 没有太大关系。
    想了想,我为什么都没想过 sort 有 -o ,因为更常见的场景是 cat foo.txt | xxx | xxx | tee foo.txt ,显然没人会去奢望前边有一个 -o 可以解决所有的问题...
    filtrate
        26
    filtrate  
    OP
       2024-05-12 08:14:07 +08:00
    @sendi 进程替换这种用法还没见过,学习了,确实挺好用。
    但是我这里测试看有一个小问题,短时间内执行多次 sort foo.txt > >(tee foo.txt ) ,会有很低的概率把 foo 清空,如果用 for 批量执行,清空的概率就非常高了 for i in {01..20}; do echo $i; sort foo > >(tee foo ) ; done
    filtrate
        27
    filtrate  
    OP
       2024-05-12 08:21:27 +08:00
    @jinqzzz 还没写完就回车里,不过这种场景我现在还遇不到,就不太关心了。 1<> 是没有这个问题的,很好用
    filtrate
        28
    filtrate  
    OP
       2024-05-12 08:22:11 +08:00
    回车里 -> 回车了
    jinliming2
        29
    jinliming2  
       2024-05-12 09:00:44 +08:00 via iPhone   1
    @jinqzzz sort 有个比较特殊的点是,它必须一次性把所有内容都读入才能开始输出,因为有可能最后一行的内容被排序到最前面。在输出之前,内容都是要读到内存里的,处理大文件要足够的内存。
    所以可以用一些方法来延迟 tee 创建输出流的时间,确保 sort 已经读取所有内容。
    如果是 cat xxx | tee xxx 这样的,cat 是支持流式处理的,也就是读多少输出多少,读取的内容可能比内存都要大,这种情况 sort 命令都肯定要失败的。这种就不建议延迟 tee 了,还是换个文件名来写,确保读取写入全部完成之后再做文件替换是比较稳妥的。
    julyclyde
        30
    julyclyde  
       2024-05-13 11:54:30 +08:00
    @zhuisui 从技术上讲,用管道把俩进程连接起来,其打开顺序只能有一种吧
    zhuisui
        31
    zhuisui  
       2024-05-13 13:44:26 +08:00
    @julyclyde 理论上 bash 对管道没有这样的声明,实际上自然也不能做这样的假定。
    而且先开输入再开输出再连接它们在技术上是完全可行的。
    zhuisui
        32
    zhuisui  
       2024-05-13 13:55:33 +08:00
    @jinqzzz 因为 process substitution 也可能用 pipe 实现,道理一样
    https://www.gnu.org/software/bash/manual/html_node/Process-Substitution.html
    julyclyde
        33
    julyclyde  
       2024-05-13 17:04:57 +08:00
    @zhuisui 先开输入再开输入似乎不能保证完整性吧?
    zhuisui
        34
    zhuisui  
       2024-05-13 17:57:42 +08:00
    @julyclyde
    先开 a 输出再开 b 输入把 a 接到 b ,就需要 pipe 先 buffer 一下 a 输出;反过来直接对接管道就行了,甚至不需要 buffer 。
    你说呢
    onnethy
        35
    onnethy  
       2024-05-17 20:31:59 +08:00
    @aloxaf 牛逼
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2941 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 25ms UTC 12:51 PVG 20:51 LAX 05:51 JFK 08:51
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86