玩转 SHELL 脚本之:Shell 命令 Buffer 知多少?

时间:2022-04-28
本文章向大家介绍玩转 SHELL 脚本之:Shell 命令 Buffer 知多少?,主要内容包括1、问题:、2、解决方案:、3、推而广之、4、Refer:、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

1、问题:

下午有同学问了这么一个问题:

tail -n +$(tail -n1 /root/tmp/n) -F /root/tmp/ip.txt 2>&1| awk 'ARGIND==1{i=$0;next}{i++;if($0~/文件已截断/){i=0};print $1"---"i;print i >> "/root/tmp/n"}' /root/tmp/n -  

seq 10 > /root/tmp/ip.txt && tail -f /root/tmp/n

把这两条语句分别在同一台机器的两个终端上执行,你会发现第二条语句的 tail 跟踪不到结果,而第一条语句明明是有结果输出的。

在往下细说之前,咱们先简单介绍下第一个语句干嘛的:

这个语句是实时 tail 一份日志,并实现了两个小功能:

当文件被重写的时候将文件的行号置 0,并且当进程挂掉后,重启进程时,能从上次挂掉的地方开始 tail 起,类似“断点续传”。

不熟悉 awk 的同学看起来估计比较费劲,没关系,咱们简化下场景,写个简单的 test case 模拟上面的语句(1):

{ echo 21;sleep 10;echo 22; }|awk '{print >> "/root/tmp/n"}'

你会发现确实是当屏幕输出了 21 的时候, n 的值没有变化,但是当整个 echo 执行完成时,n 的值却一起变化了,输出了 21、22。

对此,你很容易写出另一个 test case:

while [[ $i -lt 10 ]]; do ((i++)); echo $i|awk '{print >> "/root/tmp/n"}'; sleep 2; done

那为什么这个 case 能实时看到 n 的值在变化呢?别急,读完本文,你自会找到答案。^ _ ^

其实语句(1)的问题在于 shell 下的一个概念引发的:buffer

写过程序的同学应该知道 磁盘与内存,内存与CPU 的 IO 交互速度都不在一个量级上,那么为了提高数据的存取效率,一般都会在软件程序、硬件设计中采用 buffer 的设计,当 buffer 满了才会请求一次 IO 操作,而不是一个字符或者一个字节的方式请求 IO 操作,具体说来一般是交互式的会无 buffer 或者小 buffer,非交互式的操作一般 buffer 都会比较大,因为对用户来说“实时性”要求不是那么高了嘛~

语句 command1 | command2 大体的流程图如下:

例如如下语句的流程图如下:

tail -f access.log | cut -d' ' -f1 | uniq

语句(1) 的重定向就是一个典型的非交互式操作,会由于 buffer 的原因,用户无法实时的看到日志中数据的变化。

2、解决方案:

知道原因了,咱们可以有如下几种方式,让 awk 中的重定向变得无 buffer 实时输出:

{ echo 21;sleep 10;echo 22; }|awk '{print >> "/root/tmp/n"; fflush("")}'
{ echo 21;sleep 10;echo 22; }|awk '{print >> "/root/tmp/n"; system("")}'
{ echo 21;sleep 10;echo 22; }|awk '{print >> "/root/tmp/n"; close("/root/tmp/n")}'
{ echo 21;sleep 10;echo 22; }|awk '{system("echo "$0" >> /root/tmp/n")}'
{ echo 21;sleep 10;echo 22; }|awk '{print |"cat - >> /root/tmp/n"}'

关于 fflush 的说明如下:

fflush([file]) Flush any buffers associated with the open output file or pipe file. If file is missing, then standard output is flushed. If file is the null string, then all open output files and pipes have their buffers flushed.

说道这儿,有同学或许会有疑问:还有什么办法去验证是 buffer 的原因呢?

其实你调大你的输出就行了:

{ seq 5000;sleep 10;seq 1000; }|awk '{print >> "/root/tmp/n"}'

3、推而广之

其实 linux shell 下的众多命令都采用了 buffer 的设计,例如 grep,比如就曾经有同学问过我:

tail -f logfile | grep 'ooxx' 为什么看不到结果呢?日志中明明就有的呀? 等等。。。

那本文在此稍稍总结下常用命令的 buffer 问题以及应对措施:

grep (e.g. GNU version 2.5.1)

--line-buffered

sed (e.g. GNU version 4.0.6)

-u,--unbuffered

awk (GNU awk)

use the fflush() function

awk (mawk)

-W interactive

tcpdump, tethereal

-l

例如上文提到的 grep buffer 问题:

tail -f /var/log/foo | grep --line-buffered

也有专门的命令或者工具包来解决这个问题,比如 unbuffer、stdbuf,或者直接调用 c 语言库禁用 buffer:

setvbuf(stdout, 0, _IONBF, 0);

4、Refer:

[1] 9.1.4 Input/Output Functions

https://www.gnu.org/software/gawk/manual/html_node/I_002fO-Functions.html

[2] What is buffering? Or, why does my command line produce no output: tail -f logfile | grep 'foo bar' | awk ...

http://mywiki.wooledge.org/BashFAQ/009

[3] How to fix stdio buffering

http://www.perkin.org.uk/posts/how-to-fix-stdio-buffering.html

[4] buffering in standard streams

http://www.pixelbeat.org/programming/stdio_buffering/

[5] 关于awk中通过管道执行shell后的管道关闭问题

http://hi.baidu.com/leejun_2005/item/26a5f8273e7e3555c28d5970

[6] Why does awk do full buffering when reading from a pipe

http://unix.stackexchange.com/questions/33650/why-does-awk-do-full-buffering-when-reading-from-a-pipe

[7] tailf and tail -f

http://blogread.cn/it/article/6892?f=wb