管道与子shell
最近写一个shell脚本,遇到了一个奇怪的现象,经过一番查询发现之前没有注意过的陷阱:Shell(至少是Bash)会将管道运算符的前后命令放到子shell里执行。这将导致:看似在一个脚本的变量,出现改动后没效果的现象。下面是问题背景,关于此问题可以直接跳到“问题”一节查看。
背景
用nmon监控机器的CPU等数据,若要转换成可视化图表,可以使用IBM提供的nmon Excel宏来生成。但要借助Excel生成,不方便批量处理,太麻烦了。若是能用命令行生成网页,直接在服务器上看就好了。
正好nmon for linux本身就提供了生成网页版的工具nmonchart
,nmonchart
的原理是通过shell生成HTML,然后调用google的图表库渲染。
我将70台服务器上的nmon输出文件下载到某台机器上,然后准备用将其转换成HTML文件,并通过通过Python
的SimpleHTTPServer
模块启动一个简单的HTTP服务器,大家就可以方便的通过浏览器查看每台机器的nmon的可视化结果了。
任务本身很简单,只需循环将文件名传给nmonchart
即可,假如nmon
的输出文件都已下载到/home/maoshuai/nmon_files
下面,则做如下处理:
cd home/maoshuai/nmon_files
ls -1 *.nmon | while read fileName;do
nmonchart $fileName ${fileName}.html
然后发现速度太慢,每个nmon
文件要30多秒才能转换完毕,算下来要30多分钟才能处理完毕。top查看nmonchart调用的sed进程已经100%的CPU,但只能运行在一个CPU上,这是因为shell只能跑在一个CPU上。而当前机器有30个cpu都是空闲,显然是浪费。
接下来需要优化,即将任务分配到30个cpu上并行执行。思路是,先将nmon文件按照CPU的个数拆分为多个组,比如有60个nmon文件,则每个组分2个nmon文件。然后根据CPU的个数起多个后台任务。具体如下:
首先编写一个函数,接受以空格为分隔的文件名,用于处理多个nmon的转换
gen(){
cd home/maoshuai/nmon_files
for fileName in $1;do
nmonchart $fileName ${fileName}.html
}
然后将所有nmon文件,根据CPU个数拆分为多个组:
# get total cpu number
cpuNum=$(cat /proc/cpuinfo | grep "physical id" | wc -l)
cpuIndex=0
cd home/maoshuai/nmon_files
# divide files into serveral groups by cpuIndex
ls -1 *.nmon | while read fileName;do
# append new file by space
nmon_files_arr[$cupIndex]="${nmon_files_arr[$cpuIndex]} $fileName"
# update cpuIndex
let "cpuIndex=(cpuIndex+1)%$cpuNum"
最后,按组执行多个后台任务
# launch multi backgroud tasks
for i in $(seq 0 ${#nmon_files_arr[*]};do
groupFiles=${nmon_files_arr[i]}
# backgroud running
gen "$groupFiles" &
done
# wait all backgroup task
wait
echo "All done!"
一切看起来没问题,但第二部的拆分出了问题,我发现最后nmon_files_arr是空,但在循环内echo出来并不是空。
问题
将上面的问题做个简化。发现确实是这样,比如下面的代码,将/tmp下的文件编号输出,并最后输出总文件数:
i=0
ls -1 /tmp | while read fileName;do
let "i=i+1";
echo "$i $fileName";
done
echo "total file num: $i"
但得到的结果如下:
虽然while循环内变量i在递增,但while之外变量i还是保持为0,似乎while循环内的改变不起作用。
进一步简化:
name="shuai"
name="jack" | echo $name
echo $name
发现竟然两次输出的内容为还是shuai。这下看来蹊跷了,经过一番Google,确实有人也遇到过,根本原因是:管道运算符会将命令放到子shell中执行,子shell自然无法修改父shell的变量,而这一点在Bash的man文档里有一句话轻描淡写的一带而过:
Each command in a pipeline is executed as a separate process (i.e., in a subshell).
注意是each
这个关键字,意思是管道符左右都是在subshell里执行。这就是为什么最后一个例子两次都输出shuai的原因。
解决办法
针对上面的问题,可以通过消除管道的办法来解决,比如使用for代替:
i=0
for fileName in $(ls -1 /tmp);do
let "i=i+1";
echo "$i $fileName";
done
echo "total file num: $i"
或者干脆把后面的语句,整体通过大括号放到一个子shell里:
ls -1 /tmp |{
i=0
while read fileName;do
let "i=i+1";
echo "$i $fileName";
done
echo "total file num: $i"
}
或者使用Here string
i=0
while read fileName;do
let "i=i+1";
echo "$i $fileName";
done<<<$(ls -1 /tmp)
echo "total file num: $i"
总结
- Shell中(至少是bash),管道符左右的命令都是在子shell中执行的。
- 由于子shell无法修改父shell的变量,因此管道符两边的变量互相不影响,并且无法修改父shell的变量。
- 可以通过for或here string等方法规避这个问题,解决变量不通的问题。
-------------------------
本文采用 知识共享署名 4.0 国际许可协议(CC-BY 4.0)进行许可。转载请注明来源:https://imshuai.com/pipeline-subshell 欢迎指正或在下方评论。