当前位置: 首页 > 科技观察

不好意思,一不小心就错过了整个linux流水线

时间:2023-03-12 10:30:36 科技观察

非常喜欢linux系统,尤其是linux的一些漂亮的设计。可以用现成的工具灵活解决,写shell脚本效率很高。在本文中,我将分享我在实践中使用重定向和管道符时遇到的一些陷阱,并了解一些底层原理,可以使编写脚本的效率提高很多。>和>>重定向字符的陷阱先说第一个问题,执行下面的命令会发生什么?$catfile.txt>file.txt读写同一个文件,感觉不会有事吧?其实在上面的命令上,运行上面命令的结果是清除file.txt文件中的内容。PS:有些Linux发行版可能会直接报错,可以执行catfile.txt来绕过这个检测。正如上面在Linux进程和文件描述符中提到的,程序本身不需要关心它的标准输入/输出指向哪里。是shell通过管道符号和重定向符号修改程序标准输入/输出的位置。因此,当执行命令catfile.txt>file.txt时,shell将首先打开file.txt。由于重定向符号是>,文件中的内容会被清空,然后shell会将cat命令的标准输出设置为file。txt,此时开始执行cat命令。即以下过程:1.shell打开file.txt并清除其内容。2.shell将cat命令的标准输出指向file.txt文件。3.shell执行cat命令,读取一个空文件。4.cat命令将空字符串写入标准输出(file.txt文件)。所以,最后的结果就是file.txt变成了一个空文件。我们知道>会清空目标文件,>>会在目标文件末尾追加内容,那么如果将重定向符>改成>>会怎样呢?$echohelloworld>file.txt#文件中只有一行内容$catfile.txt>>file.txt#这个命令会无限循环。首先在file.txt中写入一行内容。执行catfile.txt>>file.txt后,预期结果应该是两行内容。但遗憾的是,运行结果并不符合预期,而是会无限循环地不断向file.txt写入helloworld,文件很快就会变得非常大,所以只能用Control+C来停止命令。这就有意思了,为什么会出现死循环呢?其实稍加分析就能想到原因:首先需要回忆一下cat命令的行为。如果只执行cat命令,它会从命令行读取键盘输入。每按一次PressEnter,cat命令就会回显输入,也就是说cat命令是逐行读取数据,然后输出数据。那么,catfile.txt>>file.txt命令的执行过程如下:1.打开file.txt,准备在文件末尾追加内容。2.将cat命令的标准输出指向file.txt文件。3.cat命令读取file.txt中的一行并将其写入标准输出(附加到file.txt文件)。4、由于刚刚写入了一行数据,cat命令发现file.txt中还有可以读取的内容,会重复第3步。上面的过程就像是遍历列表,向其中添加元素一次列出一个。遍历永远不会结束,这导致我们的命令无限循环。>重定向字符和|管道字符配合我们经常会遇到这样的需求:截取文件的前XX行,其余删除。在Linux中,head命令可以完成截取文件前几行的功能:$catfile.txt#file.txt有五行内容12345$head-n2file.txt#head命令读取前两行12$catfile.txt|head-n2#head也可以读取标准输入12如果我们想保留文件的前两行,删除其他的,可以使用如下命令:$head-n2file.txt>file.txt但是这违反了前面的说法Error,最后file.txt会被清空,我们的需求无法实现。那么我们是不是可以通过写这样的命令来避免陷阱:$catfile.txt|head-n2>file.txt结论是否定的,文件内容还是会被清空。什么?管道是否泄漏,所有数据都丢失了?Linux进程和文件描述符中也提到了管道符的实现原理,本质上就是把两个命令的标准输入和标准输出连接起来,使得上一个命令的标准输出作为下一个命令的标准输入下一个命令。但是,如果你认为这样写命令就能得到预期的结果,那可能是因为你认为管道连接的命令是串行执行的。这是一个常见的错误。实际上,通过管道连接的多个命令是并行执行的。的。你可能会认为shell会先执行catfile.txt命令,正常读取file.txt中的所有内容,然后将这些内容通过管道传递给head-n2>file.txt命令。虽然此时file.txt中的内容会被清空,但是head并不是从文件中读取数据,而是从管道中读取数据,所以应该可以正确的向file.txt写入两行数据。但实际上,上述理解是错误的。shell将并行执行管道符号连接的命令。例如执行如下命令:$sleep5|sleep5shell会同时启动两个休眠进程,所以执行结果是休眠5秒而不是10秒。这有点违反直觉,比如这个常用命令:$catfilename|grep'pattern'直觉上好像是先执行cat命令一次读取文件名的所有内容,然后传给grep命令进行搜索。但实际上cat和grep命令是同时执行的。之所以能得到预期的结果,是因为grep'pattern'会阻塞等待标准输入,而cat通过Linux管道将数据写入grep的标准输入。执行如下命令可以直观的感受到cat和grep是同时执行的,grep是实时处理我们用键盘输入的数据:$cat|grep'pattern'说了这么多,我们来回顾一下问题所在开头:$catfile。txt|head-n2>file.txtcat命令和head会并行执行,执行结果不确定谁先到。如果head命令在cat之前执行,那么file.txt会先被清空,cat将无法读取任何内容;反之,如果cat先读取文件内容,则可以获得预期的结果。但是,通过我的实验(重复这种并发情况1w次),发现file.txt被清除的错误情况的概率远大于预期结果的概率。不清楚这是为什么,应该和linux内核一样,与实现进程和流水线的逻辑有关。解决方案中提到了这么多管道字符和重定向字符的特点,如何避免这个文件被清空的坑呢?最靠谱的办法不是同时读写同一个文件,而是通过一个临时文件atransfer来完成。比如只保留file.txt文件的前两行,可以这样写代码:#先将数据写入临时文件,然后覆盖原文件$catfile.txt|head-n2>temp.txt&&mvtemp.txtfile.txt这是最简单、最可靠、最万无一失的方法。如果觉得这条命令太长,也可以通过apt/brew/yum等包管理工具安装moreutils包,会多出一条sponge命令,可以这样使用:#Firstpassthedatatosponge,然后写入sponge原文件$catfile.txt|head-n2|spongefile.txtsponge这个词就是海绵的意思,很形象。它会先“吸收”输入的数据,最后写入file.txt。核心思想和我们使用的临时文件类似,这个“海绵”就像一个临时文件,可以避免同时打开同一个文件进行读写的问题。以上是重定向和管道字符的一些陷阱,希望对你有所帮助。