浅谈自底向上的Shell脚本编程及效率优化

小论文,大家多批评指导:)

======

  :   低效的shell脚本设计会进一步影响原本解释器模式下并不见长的程序运行效率,使其在面对大数据量文本分析时的资源与时间消耗变成难以接受的。本文分析了shell语言及其应用需求的特点,从分层设计的角度提出了“自底向上进行shell脚本编程”的理论,同时提出了从“外部系统环境”到“内部执行模式”全面的掌握工具软件的方法。实例充分证明,自底向上的shell脚本编程思想及方法能够有效提高脚本的执行效率。

关键词:  脚本; 正则;管道

引言

伴随着UNIX四十多年的发展,shell已经从最初的命令解释器发展成为现今强大的计算机语言, shell脚本编程以其简便快捷的特色广泛应用于日常系统管理操作上,熟悉并精通shell脚本编程已是系统管理员必备的技能。

1992IEEE通过了关于“shell and utilities”的IEEE Std 1003.2-1992标准,即POSIX.2标准,并于2001年合并至IEEE Std 1003.1-2001。该标准给出了关于操作系统支持的shell命令解释器及由其内置命令(built-in)、系统工具和其他应用程序等组成的工具软件(utilities)的标准接口。 

shell脚本开发便是这样一个整体的编程过程:基于shell结构化程序设计语言,并通过调用上述工具软件,形成类似命令批处理程序的shell脚本。shell与工具软件的紧密联系使得shell脚本开发具有鲜明的“自底向上”编程风格。掌握shell脚本编程既要熟悉shell程序设计语言,同时也要熟练掌握find/sed/awk/grep等各种工具软件。

1  shell脚本优化的意义

编写高效的shell脚本,这不仅是出于优秀程序员对精简高效程序的追求,更为了满足实际的应用需求。shell是一种解释型语言,尽管从执行方式方面考虑,解释型程序的效率上比不上C语言等编写的编译型程序,但脚本其所具有的简便快捷的特点使其能够有效实现对系统任务和重复性任务的自动化管理操作,普遍应用在日常的数据处理之中。

系统管理员在进行系统信息采集或网站日志处理中经常要面对从上百万或更大数据量的文件中提取、分析数据的需求,实际上优化shell脚本效率的效用直接体现在这种大数据量的文本处理上。低效的shell脚本设计会进一步影响原本解释型执行模式下并不见长的shell脚本运行效率,特别是在面对大数据量文本分析时的资源与时间消耗变成难以接受的。

此外,程序的优化需要有针对性,盲目的优化是费时费力的。“过早的优化,是万恶之源”。算法大师Donald E. Knuth曾分析到:程序员经常会由于预先的判断错误把97%的时间花费在优化非关键模块的效率之上,却只花了3%的时间在关键模块上,因此过早的优化是没必要的,只有确定了关键模块的真正所在,对其进行的优化才是有价值的。如何有针对性的去优化shell脚本,提高脚本执行效率?

2  shell脚本优化的方法

shell的编程模式来看,shell结构语言与各应用软件紧密结合的特色使shell编程具有鲜明的自底向上的编程风格。Shell和系统分别提供各种内置函数和工具软件,这些工具软件相当于程序设计中可重用的模块代码, 并且是采用C语言等语言开发的高效模块。这些工具软件的使用大大弥补了shell作为解释器在效率上的缺陷。同时尽管在应用需求上存在交叉,不同的工具具有不同的执行模式使其在解决同一个需求也存在效率上的差别,因此,采用何种工具直接影响着shell脚本的执行效率。

下边是一个工具软件应用效率的体现实例

需求:计算1100000累加结果

方法1:采用bash  shell的数值计算

# time for((i=0;i<=100000;i++)); do ((sum+=i)); done ; echo $sum

real    0m1.134s

user    0m1.080s

sys     0m0.048s

5000050000

方法2:采用awk的数值计算

# time awk 'BEGIN{while(i++<100000)sum+=i; printf "%d", sum;}' 

5000050000

real    0m0.029s

user    0m0.020s

sys     0m0.000s

实例结果分析:同等的累加运算,采用shell的数值计算耗时1.134秒,采用awk工具只需要0.029秒。实验结果表明,适当的采用工具软件,能够明显提高shell脚本的执行效率。       

 “一个程序只做一件事,并做好。程序要能协作。程序要能处理文本流,因为这是最通用的接口”,shell脚本开发便是这样一种“自底向上”的编程过程,这种过程既充分体现了UNIX的编程哲学,也体现了模块化结构式程序的模块化思想及其封装性与紧凑性。这种紧凑性表现在shell编程中必备的各种工具内部功能的精简,高效和无冗余。shell充分利用了这些工具,提高了脚本的适用范围。

基于shell脚本的“自底向上”的编程风格,可以从工具软件的挑选和应用方向考虑优化程序效率。而工具软件的挑选与应用,离不开以下二点:

a:系统原理知识的了解及应用

b:工具软件的执行模式的了解及应用

系统是工具软件的外部条件和应用环境,执行模式是软件的内部环境,只有由里到外对工具软件进行全面的了解,才能够结合实际需求和设计,提高脚本执行效率。

3  Shell脚本优化实践

3.1    实验环境

CHINAUNIX论坛的shell版非正式的统计在304人中有260的人“用BASH多一些”,占85%以上。BASH最新发布的稳定版本是4.1,但当前各LINUX发行版中大部分还是使用3.2版本。因此,本文实例测试环境采用VMware虚拟机安装的RH Linux系统、GNUBASHAWK/SED等主流自由软件。

高效shell脚本的基础是各种工具软件的应用,在工具的掌握过程中,有些知识点是通用的,比如操作系统原理和管道、正则等应用技巧;有些是工具自身的特色,比如执行模式,内部数据结构如哈希数组等。只有将这些知识点融会贯通,在面对各种需求时,才能从本质上明白工具间的执行效率差别的原因,并以此来确定采用何种适合的工具。

3.2    系统原理及应用

3.2.1           文件系统

了解操作系统相关原理能够帮助更好的理解、掌握并应用工具软件。首先以文件系统为例,只有了解LINXU的根文件系统,才能更好的理解find命令查找文件时的深度优先查找方式。再比如:通常LINUX文件系统包括存放于文件索引列表的文件相关信息和存放于磁盘数据块中的实际数据。有部分命令是采用索引列表里的文件信息,有些命令则需要去遍历数据。根据需求采用适当的工具才能提高脚本执行效率。

实例 :文件系统应用

需求:计算定长大数据量文件的总记录数

方法1:采用wc命令,在文件遍历过程中累计文件记录数。

方法2:采用ls命令,读取索引中的文件大小信息,除以单条记录长度即可获得总记录数。

实例结果分析:方法2的效率在于充分利用了文件系统和需求,减少了文件I/O和遍历计算。在大文件处处理中的效率提高程序尤为明显。

3.2.2           管道

管道是UNIX及其兼容系统中最早也最通用的进程间通信(IPC)手段,shell采用管道线简便实现了管道应用,管道已经普通采用于各种shell脚本程序中。

只有了解文件系统,多进程编程等相关知识方能更好的掌握管道的应用。管道实质上是内核基于在内存中构造的管道文件系统(PIPEFS)实现的。父子进程一端采用只读方式打开文件,另一端采用只写的方式。命名管道则是进一步利用了文件系统使应用管道的进程脱离了父子关系的限制。另外,BASH中的每一条的管道都是在其子进程环境中运行的,基于进程间环境的独立性,管道内的变量操作并不会对父进程同名变量产生影响。

通过管道线将多个存在数据流依赖的命令进行重定向和连接,既实现了进程间通信,又避免了数据流落地产生了IO消耗和临时文件处理工作。

3.2.3           正则

shell中的正则是形式语言学中正则语言理论的实际应用。形式语言探究的是语言的内容和规则的严格论证。构成语言的基本要素是字符集和在其之上的语法定义。shell中存在的诸多正则标准或称流派便是根源于各标准支持的不同的元字符定义和对基本规则的扩展;此外正则表达式的应用过程分为分析,编译,执行三个过程。通常将正则在这过程中所采用的不同的数据结构与匹配算法归类为不同的正则引擎,主要包括DFANFA二种方式。DFA正则引擎采用确定型有限自动机模型,对每个输入字符都有确定的输入状态,对于字符串只需要一遍扫描即可完成算法。所以DFA引擎效率较高,但支持的功能较少;后者采用非定型有限自动机模型,增加了回溯机制,使得能够支持环视,非贪婪等功能,但也因为回溯影响了效率。

正是基于不同的正则标准使得正则表达式总是要在复杂性,效率,与准确性三点进行平衡。一般来说,对于相同目的的正则表达式复杂度会随着准确性提高并且影响效率。不同的工具软件支持的正则标准也存在差别。如何优化正则是一个较大的话题,可以参考《精通正则表达式》一书。

正则是文本匹配的利器,被广泛应用于字符串匹配之中,而字符串匹配往往是进行文本增加、删除、修改、查询等操作的基础前提。从效率上考虑,尽管正则使用起来十分方便,但出于效率考虑,正则却未必是最好的选择。实际的shell脚本开发中需要了解各种工具所支持的正则标准和正则匹配脚本运行中占用的时间比,在开发过程中结合实际需求,考虑是否需要采用正则,然后再考虑采用哪种工具哪种正则标准。

实例:正则效率。

需求:某文本以空格和/进行分隔,获取第五列的内容.        

方法1:采用正则[ /]分别匹配二种分隔符,取得第五列

# time awk -F'[ /]' '{print $5}' a.txt  >/dev/null

real    0m17.717s

user    0m14.749s

sys     0m2.844s

方法2:实际数据分析中发现,可以采用单字符解析的方式,首先根据“空格”分隔符取得第4列,再利用“/”分隔符取得第二列。

# time awk '{print $4}' a.txt |awk -F/ '{print $2}' >/dev/null

real    0m0.565s

user    0m0.224s

sys     0m0.688s

实例结果分析:awk采用-F指定分隔符,在多分隔符情况下,会启用正则去解析记录,增加了函数调用,和字符串匹配的消耗,效率上理所当然的比不上默认的空格或单字符分隔符采用的简单的字符比较方式,尽管如此,方法2依赖于对需求的进一步分析,适用的需求范围没有使用正则那么简便灵活。

3.3    了解软件执行模式

执行模式涉及工具内部的程序流程和处理逻辑,不同的程序一般具有自身的执行模式,但同时也遵守着一些通用的标准。比如《GNU Coding Standards》(Richard Stallman1993)一文中关于GNU兼容接口提到一点:如果程序按行读取输入则应该每次只在内存中保留一行,以便于支持大文件操作。实际上,很多工具软件正是采用这种方式进行处理的。比如grepawksed等命令的一般执行模式便是每次读取一行输入,再执行指定的操作,保证了程序执行时的低内存消耗。

 shell编程的各种应用工具,用以文本处理的较为常用的命令以sedawk为典型代表,其执行模式也颇具特色。

3.3.1           sed执行模式及效率优化

sed是一个POSIX.2标准支持的流编辑程序。sed在执行过程中利用了二个可以操作的空间:模式空间和缓冲空间,和一个可控制的操作循环(cycle) sed的执行模式的便是基于这样一个循环操作:读入一行文本进入模式空间并执行操作,直到输入结束或主动退出。

sed的魅力便在于利用这二个空间和操作循环所进行的运算及由此产生的强大功能,这种强大充分体现在sed运算的图灵完备性:单个sed命令可以模拟所有图灵运算。简单的说,单个sed命令可以实现任何的运算任务。尽管如此,限于sed的执行模式和效率考虑,一般情况下却不将sed 用于文本操作外的计算比如数学运算等,sed通常应用于文本的查询,替换,过滤等操作。

了解了sed的执行模式,我们就可以通过控制sed的循环,采用合理的程序逻辑以提高命令执行效率。

实例:sed应用,读取指定行

方法1sed -n '45,50p' filename

方法2sed -n '51q;45,50p' filename

实例结果分析:方法2在方法1基础上增加了一个判断,当文件读取到第51行时即时退出。避免文件后续部分的遍历,在大数据量处理上能够很大的提高执行效率。

实例:sed应用,文本替换

方法1sed 's/foo/bar/g' filename

方法2sed '/foo/ s/foo/bar/g' filename

实例结果分析:sed支持采用正则进行匹配和替换,考虑字符串替换的需求中,不防加上地址以提高速度。实例中通过增加一个判断逻辑,采用“事先匹配”代替“直接替换”,由于sed会保留前一次的正则匹配环境,不会产生冗余的正则匹配,因此方法2具有比方法1 高的效率

3.3.2           awk执行模式及效率优化

awk不只是一个应用工具,还是一种语法和程序结构类似于C语言的高级编程语言,可以用来开发包括数学运算,字符串处理等运算功能完善的awk脚本程序。awk的执行模式也是基于这样一个循环:先从输入中获取一条记录,然后根据pattern真假决定是否执行actionpattern是对数值运算或文本匹配结果的逻辑判断,action即是由awk的编程语言定义支持的各种操作程序,awk支持用正则用以进行字符串匹配操作。

awk的效率体现在其类似于C语言的高效的主体程序结。awk采用了yaccyyparseawk脚本进行解析并形成node结点树,再由采用了c语言编写并且编译完成的awk主体程序树进行递归遍历。这种执行方式,使得awk既有解释程序的便捷,又有编译程序的效率。

前边的实例已经涉及采用awk进行高效的数值计算和文本分析。这里需要着重介绍的是awk的特色数据结构:哈希数组,也称关联数组。哈希数组中的每个单元包括二个元素:单元键(key)及单元值(values)。哈希结构的效益体现在以较小的内存空间实现大的数据空间上的数据存放,节省了数据占存空间;同时哈希数组中的数据访问是随机访问,不需要遍历而直接通过哈希函数直接访问数组单元取值,节省了数据查询时间。

实例 :哈希应用

需求:分析apace日志,获取访问量较大的前十个IP地址

方法: awk {ips[$1]++;}END{for(ip in ips)print ip,ips[ip]} |sort nk2|head -10

实例结果分析:该apace日志首列保存ip信息。能过awk对日志进行一次遍历,即实现了IP信息的获取,同时采用了哈希结构,对ip进行了累计。

4  总结

没有最好的语言,只有较合适的语言,本文在适当的需求下讨论了shell脚本的自底向上的编程风格及其效率优化的必要性, 介绍了shell脚本密切相关的系统知识及常用工具软件的执行模式,通过实例说明了了解这些并适当的挑选和应用工具软件可以有效的提高shell脚本的执行效率。需求分析与设计逻辑这二者在程序效率优化中也是至关重要的,限于篇幅没有具体展开。此外,shell脚本编程有其局限性与适用的需求范围,若在此范围之外,或对效率有进一步要求的情况下,还是需要考虑采用C,PERL等完善的程序设计语言。

阅读更多
个人分类: unix/linux 脚本编程
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭