最近有需要对目录进行遍历,并对目录中的文件进行处理,发现网上没有找到比较适合的shell并发遍历的脚本,看来就只能自己写了。。。
首先想到的代码就是
#! /bin/bash
# 这里可以是耗时比较久的任务
function do_something() {
echo "$(date "+%Y%m%d%H%M%S"): $1"
}
function traverse_dir(){
local vPath=$1
for vfile in `ls ${
vPath}` #注意此处这是两个反引号,表示运行系统命令
do
if [ -d "${vPath}/${vfile}" ];then #注意此处之间一定要加上空格,否则会报错
traverse_dir "${vPath}/${vfile}"
else
do_something "${vPath}/${vfile}" #在此处处理文件即可
fi
done
}
function main() {
local vPath=$1
local vStart=$(date +"%s")
echo "start time: ${vStart}"
local vEnd
traverse_dir "${vPath}"
vEnd=$(date +"%s")
echo "start time: ${vStart} end time: ${vEnd}"
echo "total time: $((vEnd-vStart)) seconds"
}
main "$@"
分析
这段代码虽然可以按照预期完成任务,但是如果do_something中的任务耗时比较久的话就会出现,整个遍历过程串行执行下来耗时非常的久。
这个时候有个2.0版本,就是将do_something "${vPath}/${vfile}"
替换为do_something "${vPath}/${vfile}" &
让耗时的任务在后台执行。
function traverse_dir(){
local vPath=$1
for vfile in `ls ${
vPath}`
do
if [ -d "${vPath}/${vfile}" ];then
traverse_dir "${vPath}/${vfile}"
else
do_something "${vPath}/${vfile}" &
fi
done
}
那么假设目录规模很大想进一步提升遍历的速度呢?3.0版本出来了,给traverse_dir "${vPath}/${vfile}"
也放到后台执行traverse_dir "${vPath}/${vfile}" &
function traverse_dir(){
local vPath=$1
for vfile in `ls ${
vPath}`
do
if [ -d "${vPath}/${vfile}" ];then
traverse_dir "${vPath}/${vfile}" &
else
do_something "${vPath}/${vfile}" &
fi
done
}
目录有多大,咱的进程就敢开多少,这个时候2.0和3.0的问题就暴露出来了——当目录非常多,以及目录下的非目录文件非常多的时候,脚本会由于开启的进程过多出现内存不足的报错,并且还会大量的占用主机的资源,导致整个主机都非常的卡,最终适得其反,处理速度反倒不如单进程。
此时是否存在一种方式能够控制并发度,使得遍历和处理速度不至于过慢,又不至于导致主机资源耗尽?
有没有觉得这个模型和生产者消费者很相似,哈哈是的没错,这就是一个典型的生产者消费者模型。
经过在其他博主那里取经——shell队列实现线程并发控制,最终咱们迎来了最终版本
最终版本
首先需要我们掌握几个知识。
有名管道
linux中的有名管道是一个类似队列的先进先出的结构,当从中拿取一份数据后,管道中的数据便会少一份,当从空的管道中拿数据时,相应的进程便会阻塞住。利用这个特性可以实现我们的并发控制
exec命令
有名管道可以使用mkfifo xxx的方式进行创建,为了使得我们的程序漂亮,并且各个shell进程都能独占自己的管道,我们需要了解exec命令。
通过exec fdNumber<>xxxxx 的方式我们可以将文件描述符和有名管道联系到一起,并且fdNumber这个文件描述符还具有有名管道没有的特性:无限存不阻塞,无限取不阻塞,而不用关心管道内是否为空,也不用关心是否有内容写入引用文件描述符
现在万事俱备只欠东风,看代码:
#!/bin/bash
readonly TMP_VISITOR="/tmp/visitor"
readonly TMP_DoSomething="/tmp/dosomething"
function create_fifo() {
local vCpuNums=$(cat /proc/cpuinfo| grep "processor"| wc -l)
local vVistorNums=$((2*vCpuNums)) # 二倍的CPU核心数进行遍历
local vDoSomethingNums=$((4*vCpuNums)) # 四倍的CPU核心数处理耗时较长的任务
# visitor
mkfifo ${TMP_VISITOR}
exec 3<>${TMP_VISITOR}
rm -f ${TMP_VISITOR}
for((i=0;i<${vVistorNums};++i)); do
echo >&3
done
# migrater
mkfifo ${TMP_DoSomething}
exec 4<>${TMP_DoSomething}
rm -f ${TMP_DoSomething}
for((i=0;i<${vDoSomethingNums};++i)); do
echo >&4
done
}
function close_fifo() {
exec 3<&-
exec 3>&-
exec 4<&-
exec 4>&-
}
function do_something() {
local vFileName=$1
read -u4
{
echo "$(date "+%Y%m%d%H%M%S"): ${vFileName}"
echo >&4
}&
}
function traverse_dir() {
local vPath=$1
local vFlag=$2
local vFile
local vRc
for vFile in `ls ${
vPath}`
do
if [ -d "${vPath}/${vfile}" ];then
# read超时意味着遍历队列已经满了,这个时候应当用自身进程去执行traverse_dir
# 由于使用的是自身进程,没有消耗队列中的数量,因此传"false",在traverse_dir
# 执行结束后不用执行echo >&3
read -t 1 -u 3
vRc=$?
if [[ ${vRc} -eq 0 ]]; then
traverse_dir "${vPath}/${vFile}" "true" &
else
traverse_dir "${vPath}/${vFile}" "false"
fi
else
do_something "${vPath}/${vFile}"
fi
done
if [[ ${vFlag} == "true" ]]; then
echo >&3
fi
wait
}
function main(){
create_fifo
local vPath="$1"
local vStart=$(date +"%s")
local vEnd
traverse_dir "${vPath}" "false"
vEnd=$(date +"%s")
echo "start time: ${vStart} end time: ${vEnd}"
echo "total time: $((vEnd-vStart)) seconds"
close_fifo
}
main "$@"
最终版本在单纯的遍历规模不大的目录,然后打印文件名称的时候速度可能比单进程的速度要慢,但是在目录规模较大,并且do_something的时间比较久的时候,处理速度是远远大于单进程处理的。