首页 > 解决方案 > 为什么当我使用 `>` 操作符对已重定向的文件进行 `tail -f` 时出现不一致的行为

问题描述

我正在编写一些脚本,并遇到了这种行为。这是一个简化的例子。

在一个 tty 上,

# touch file
# tail -f file 2> /dev/null

在另一个 tty 上,在同一目录中,运行以下脚本:

#!/bin/bash
for i in {1..15}; do
    echo $i > ./file
    sleep 2
done

为什么tail -f在我使用操作符时命令没有正确反映文件更改>?如果我使用>>附加运算符,它会按预期工作。

最终,已使用>操作员重定向的文件的尾部显示如下:

1
2
6
7
9

15

标签: bashio-redirectiontail

解决方案


tail -f仅可靠地检测附加的更改。它尝试检测文件被截断、缩短或以其他方式未在末尾修改的写入,但这种检测是参差不齐的。它可以检测到其中的一些,但会漏掉很多。

算法

  1. tail 使用inotify来观察变化。
  2. 当发生更改事件时,它会快速运行fstat()来检查文件的元数据,包括其大小。
  3. 如果大小较大,则假定已附加数据并读取添加的数据。
  4. 如果大小更小,它会打印“文件被截断”并从头开始。

结果

>将文件截断为 0,然后写入新内容。tail 是否检测到这些类型的写入是命中还是未命中。如果您写入更大或相等数量的字节,它通常会错过这些写入。

  • 第 4 步确保它能够可靠地检测文件长度是否缩短。如果您修改循环以在每次迭代中连续编写较短的字符串,则 tail 将检测每一个:

    for i in {1..5}; do
      for ((j=6-i; j>=0; --j)); do echo $i; done > file
      sleep 2
    done
    
    tail: file: file truncated
    1
    1
    1
    1
    1
    tail: file: file truncated
    2
    2
    2
    2
    tail: file: file truncated
    3
    3
    3
    tail: file: file truncated
    4
    4
    tail: file: file truncated
    5
    
  • 第 1 步和第 2 步之间存在竞争条件。当您运行时echo $i > file,有两个背靠背修改:文件被截断,然后$i被写入。如果 tail 能够fstat()在两个修改之间运行,那么它会检测到截断。但是,如果它太慢,它就会错过它。这就是通常发生的情况,这就是为什么它错过了大部分但不是全部的写入。

    这也解释了为什么睡觉没有帮助。要消除竞争条件,您需要在 truncating 和 writing 之间睡觉$i,而不是在之后。

    事实上,你可以这样做:

    for i in {1..15}; do (sleep 2; echo $i) > file; done
    

    > file立即运行并截断文件。然后脚本在写入前休眠两秒钟$i。两种修改之间有明显的差距。

    正如预期的那样,tail 现在检测到每一次写入:

    1
    tail: file: file truncated
    2
    tail: file: file truncated
    3
    tail: file: file truncated
    4
    tail: file: file truncated
    5
    tail: file: file truncated
    6
    tail: file: file truncated
    7
    tail: file: file truncated
    8
    tail: file: file truncated
    9
    tail: file: file truncated
    10
    tail: file: file truncated
    11
    tail: file: file truncated
    12
    tail: file: file truncated
    13
    tail: file: file truncated
    14
    tail: file: file truncated
    15
    

推荐阅读