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_lines
是 tail
函数的核心。其首先定义了每次读取的缓冲区间 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
函数将当前位置到末尾的数据打印到控制台上。