Linux tail 命令源码解析

Linux tail 命令源码解析

基础用法

tail 命令是在 Linux 系统上经常使用的一个命令,其能快速查看文件尾部的内容。它的基本用法如下所示。

(1) -n 查看末尾指定行数的内容,n 代表的是 number 的意思:

$ tail -n 15 word-list.txt

(2) -c 查看末尾指定字节数的内容:

$ tail -c 93 list-2.txt

(3) -f 实时打印日志。某个文件的末尾有新的追加行的时候,立即在控制台上显示出来,经常用这个命令查看某个文件的实时输出的日志,方便跟踪问题:

tail -f nginx.log

tail 还有一些其他用法,可以通过执行 man tail 来查看。

读取倒数 n 行源码

tail 命令从文件尾部来显示文件内容,那么它是如何做到从尾部动态或者静态的去显示文件呢,它使用中有哪些需要注意的地方呢?接下来就带大家分析一下 tail 命令的源码。

tail 源码顶部,即定义了一个常量,表示如果没有指定查看的长度 (tail abc.txt),那么就默认显示这个文件后 10 行的内容:

/* Number of items to tail.  */
#define DEFAULT_N_LINES 10

首先是打开文件:

fd = open (f->name, O_RDONLY | O_BINARY);

然后调用 tail 函数,tail 函数内部会根据参数的不同,选择 tail_lines 还是 tail_bytes 来调用。再次我们选择 tail_lines 函数来解释。

static bool
tail (char const *filename, int fd, uintmax_t n_units,
      uintmax_t *read_pos)
{
  *read_pos = 0;
  if (count_lines)
    return tail_lines (filename, fd, n_units, read_pos);
  else
    return tail_bytes (filename, fd, n_units, read_pos);
}

tail_lines 内部,其检查这个文件是否是一个 regular 文件:

S_ISREG (stats.st_mode)

Linux 中一切皆文件。平日接触到的文件,例如可执行文件、文本文件、图片等这些都是 regular 文件,读或者写这些文件,其操作通常都与磁盘打交道。然后接下来使用 (start_pos = lseek (fd, 0, SEEK_CUR)) != -1 这句话用来检测文件是否可以 seek,如果可以,那么 lseek (fd, 0, SEEK_END) 将文件 seek 到尾部,并将文件偏移量赋值给 end_pos,并调用下一个函数 file_lines

if ( ! presume_input_pipe
    && S_ISREG (stats.st_mode)
    && (start_pos = lseek (fd, 0, SEEK_CUR)) != -1
    && start_pos < (end_pos = lseek (fd, 0, SEEK_END)))
{
    *read_pos = end_pos;
    if (end_pos != 0
        && ! file_lines (pretty_filename, fd, n_lines,
                        start_pos, end_pos, read_pos))
    return false;
}

file_linestail 函数的核心。其首先定义了每次读取的缓冲区间 char buffer[BUFSIZ],这个 BUFSIZ 大小在我自己电脑上的值是 8192。然后从尾部开始先读取 ((pos - start_pos) % BUFSIZ) 字节到 buffer 中,即把不是 BUFSIZ 整数倍的这部分字节先读取掉,然后再倒着每次读取 BUFSIZ 个,如下图所示从尾部倒着读取:

    __________
 ^ |   ...    | ...
 | |__________|
 | |          | BUFSIZ
 | |__________|
 | |          | BUFSIZ
 | |__________|
 | |__________| ((pos - start_pos) % BUFSIZ)

每次读取的位置都是通过 xlseek 函数来定位的,pos 值也相应的每次减去 BUFSIZ 个单位 pos -= BUFSIZ,读取的数据也是存放到了 buffer 中 :

pos -= BUFSIZ;
xlseek (fd, pos, SEEK_SET, pretty_filename);
bytes_read = safe_read (fd, buffer, BUFSIZ);

在每次读取数据之后,都会从这数据中去寻找有没有换行符,以便统计当前已经读取了多少行。memrchr 函数用于在前 n 个字节中,从后往前查找指定字符出现的位置,每找到一个换行符,n_lines 就递减一下:

size_t n = bytes_read;
while (n)
{
    char const *nl;
    nl = memrchr (buffer, line_end, n);
    if (nl == NULL)
        break;
    n = nl - buffer;
    if (n_lines-- == 0) 
    {
        // 打印数据
        // ...
        return true;
    }
}

当最后 n_lines 减少为 0 的时候,说明已经读取到了倒数第 n 行的位置,那么此时开始往屏幕上打印尾部 n 行的数据:

xwrite_stdout (nl + 1, bytes_read - (n + 1));
*read_pos += dump_remainder (false, pretty_filename, fd,
                            end_pos - (pos + bytes_read));

如下图所示,其首先打印从 nl + 1 字符开始的 bytes - (n + 1) 个字符:

buffer                   bytes_read
   [     \n         \n      ]
         nl---------------->

最后一次从当前 pos 位置读取数据到 BUFFER 的时候,文件的位置也移动到了 pos + bytes 的位置 (safe_read 本身会改变 fd 关联的位置),然后调用 dump_remainder 函数,再从 fd 的当前位置每隔 BUFSIZ 读取并打印一下:

while (true)
{
    char buffer[BUFSIZ];
    size_t n = MIN (n_remaining, BUFSIZ);
    size_t bytes_read = safe_read (fd, buffer, n);
    if (bytes_read == 0)
        break;

    xwrite_stdout (buffer, bytes_read);
}

无限读取文件

tail 函数增加 -f 选项的时候,那么在打印出倒数若干行之后,接下来只要这个文件有变动,便会立即打印出来尾部追加变动的数据到控制台上。

在默认配置下,tail 使用 xnanosleep 函数每隔 1 秒检测一次文件是否有变化。

// 每隔 1 秒检查一次
double sleep_interval = 1.0;
tail_forever (F, n_files, sleep_interval);

tail_forever 函数内部,如下所示,其根据文件大小以及文件的修改日期,来判断文件是否发生变化:

if (f[i].mode == stats.st_mode
    && (! S_ISREG (stats.st_mode) || f[i].size == stats.st_size)
    && timespec_cmp (f[i].mtime, get_stat_mtime (&stats)) == 0)
{
    // ...
    continue;
}

如果发生了变化,其调用 dump_remainder 函数将当前位置到末尾的数据打印到控制台上。

链接