文章
服务器与存储开发
作者:Ken Gottry,最初于 2001 年 8 月发布
程序员开始使用 Oracle Solaris 时,他们希望立即开始编写脚本。刚开始他们并不关心效率和优美与否;他们关心有效性。本文介绍我经过验证的 shell 编程技巧,助您快速入门。等您有了经验,就可以发展自己的编程风格并提高脚本的效率和优美性。
|
命令 shell 是与用户交互并与操作系统通信的层。使用 MS-DOS 时,大多数人使用 command.com shell;不过,可以通过 COMSPEC 环境变量指定其他 shell。
类似地,每个 UNIX 用户必须选择一个命令 shell 用于和 UNIX 通信。建立 UNIX 帐户时,系统管理员选择用户的默认 shell。通常选项为 Bourne shell (/bin/sh)、C shell (/bin/csh)、Korn shell (/bin/ksh) 和 Bourne Again shell (/bin/bash)。尽管许多开发人员因为 C shell 类似于 C 的语法而使用它,但这是一个主观选择,本文专门使用 Korn shell。其语法在其他 shell 下不一定行得通。
从命令行执行 shell 脚本时,将使用默认 shell。如果默认 shell 是 Korn,本文中的脚本执行时不会出现语法错误。但如果您希望其他人来执行脚本怎么办?您不能依赖用户的默认 shell 来确保您的脚本始终使用 Korn shell 来运行。解决方案是使用一个 UNIX 特性,即通过 shell 脚本的第一行指示执行脚本的 shell。清单 1 中的示例语法强制脚本使用 Korn shell 运行,而不管当前用户正在执行的是何种 shell。
清单 1:强制脚本由 Korn shell 执行#!/bin/ksh # Your script goes here. All lines starting with # # are treated as comments.
有些文档使用不同的命令提示符来指示当前 shell,如表 1 所示。(由于我喜欢的 shell 是 Korn shell,因此本文所有示例均使用 $ 提示符。)由于不能确保脚本始终使用 Korn shell 执行,请将 #!/bin/ksh 置于每个脚本的第一行。(本文中的 $ 提示符只是指示在命令行输入命令。)
| 提示符 | Shell |
|---|---|
| $ | Bourne 或 Korn shell |
| % | C shell |
| # | Root 登录 |
UNIX 脚本文件与 DOS BAT 文件类似。DOS 世界的所有编程注意事项仍然适用于 UNIX。
编写任何脚本均涉及以下步骤:
假设您想编写一个脚本来捕获 vmstat 信息。您希望以 2 秒的时间间隔运行 vmstat 1 分钟。请使用上述 5 个步骤实现目标。
首先,使用 man vmstat 在文档中查找 vmstat。接下来,以交互方式运行命令,确保您了解语法和预期输出。清单 2 显示以 2 秒的时间间隔运行 vmstat 30 次的语法。
vmstat 命令 30 次 $ vmstat 2 30
接下来,创建包含此命令的脚本文件。您应建立描述脚本位置和脚本名称的标准。将特定类别(例如某公司)的所有内容存储在 /usr/local 下的一个子目录中。
在本示例中,假定公司是 Acme Products,于是目录为 /usr/local/acme。在此目录内,创建一个名为 scripts 的子目录以及另一个名为 logs 的子目录。其他子目录可能用于其他目的。
接着,使用文本编辑器(如 vi)创建一个名为 capture_vmstat.sh 的脚本文件。与 DOS(其中 EXE、COM 和 BAT 指示可执行文件)不同,在 UNIX 中文件扩展名没有意义。
您可以使用 .sh 作为扩展名来表示 shell 脚本文件,但这并不能使脚本可执行。该文件命名约定便于快速识别文件。如果文件名遵循某个标准,还可以使用 find 命令查找所有特定类型的文件。
capture_vmstat.sh 脚本文件包括两行,如清单 3 所示。第一行比较固定,它声明 Korn shell 应执行该脚本中的命令。第二行是 UNIX 命令本身。
capture_vmstat.sh 脚本按 2 秒的时间间隔运行 vmstat 30 次 #!/bin/ksh vmstat 2 30
与使用文件扩展名确定文件是否可执行的 DOS 不同,UNIX 依赖于文件权限。使用 chmod 命令将文件标记为可执行。
最简单的启用执行位的方式是使用 chmod +x capture_vmstat.sh。
在生产环境中,在公开的服务器上,还必须考虑所有者、组和任何人的权限以控制对脚本的完全访问。(有关文件权限的主题不在本文档的讨论范围内。)更多信息,请参见 man chmod。
现在可以测试脚本了。与 DOS 不同,UNIX 不会自动在当前目录中查找要执行的文件。UNIX 提供了 PATH 环境变量。UNIX 将只在 PATH 变量标识的目录中搜索可执行文件。由于大多数人未将当前目录包括在 PATH 中(由圆点指示当前目录),因此,只键入清单 4 中的代码将无法正常工作,因为 /usr/local/acme/scripts 不在 PATH 中。
PATH 中,否则该代码不会执行脚本 $ cd /usr/local/acme/scripts $ capture_vmstat.sh
必须显式指定脚本的完整文件名,包括路径。不要依赖 PATH 变量,因为它将来可能发生改变且以下两个地方之一可能出问题:
PATH 中意外删除,导致 UNIX 无法再找到脚本。PATH 中列出的不同目录中找到同名的脚本并执行。因此,为安全起见,应始终通过指定完整文件名来执行脚本,如清单 5 所示。
清单 5:指定完整文件名以确保 UNIX 能找到正确的脚本$ /usr/local/acme/scripts/capture_vmstat.sh
可能您不喜欢键盘输入,因此有了利用圆点 (.) 指代当前目录的快捷方式。首先,切换到脚本目录,然后通过在脚本名之前加点斜杠 (./) 来执行脚本,如清单 6 所示。如果您执行的只是一个脚本,这省不了什么键盘输入;但如果您要从脚本目录执行多个脚本,则只需输入一次目录名。
$ cd /usr/local/acme/scripts $ ./capture_vmstat.sh
无论如何调用 capture_vmstat.sh 脚本,输出均应与交互方式运行 vmstat 时所获得的输出相同。
现在,您已经有了脚本并知道它可以工作。有四种方式可以运行脚本:
1. 交互式。记录脚本并让其他人(也许是帮助台人员)运行脚本文件。运行脚本的人无需了解 UNIX 命令或语法,就像 DOS 用户无需了解 DOS 命令或语法就可以使用为其创建的 BAT 文件一样。
2. 使用 at 命令。在将来的某个时刻使用 at 命令执行一次脚本。有关详细信息,请查看 man at。当用户注销时,有些 UNIX 系统会取消正在运行的 at 作业。请仔细查看系统文档。
3. 使用 cron 实用程序。使用 crontab 文件按固定时间表重复执行脚本。有关详细信息,请查看 man crontab。清单 7 显示一个简单的 crontab 条目,该条目每周一、周三和周五从上午 8 点到下午 5 点在正点过 10 分钟时运行脚本,每小时运行一次:
capture_vmstat.sh 脚本的 crontab 条目 10 8-17 * * 1,3,5 /usr/local/acme/scripts/capture_vmstat.sh
在继续介绍第四种脚本启动方法之前,您需要了解与通过 crontab 运行脚本有关的两个问题:
#!/bin/ksh 作为脚本的第一行,如前所述。cron 启动脚本时,没有终端,因此 cron 必须将 stdout 重定向到某处。正常位置是其 crontab 启动了脚本的用户的电子邮件收件箱。尽管这可以接受,但当扩展基本脚本时,可以使用下面介绍的其他(更好的)解决方案。4. 使用 HTML 表单。使用 HTML 表单启动脚本并通过 CGI(通用网关接口)发送脚本。该命令的输出将发送回浏览器,因此应使用 <pre> 和 </pre> HTML 标记保留格式设置。
除了此处所述内容,此 HTML 表单方法还有一些事情,而且使用表单和 CGI 存在许多安全风险。但此方法被证明在用于内部帮助台人员或其他一线支持人员时非常成功。
前面的脚本是 hello, world 的 shell 脚本版本,hello, world 是学习一种新的编程语言时编写的第一个标准程序。现在可以向其再添加几个基本特性。
stdout首先,脚本将其输出发送到 stdout(通常是终端)。您可以对脚本进行扩展使其将输出重定向到日志文件,如清单 8 所示。
stdout 重定向到文件 #!/bin/ksh vmstat 2 30 > /usr/local/acme/logs/vmstat.log
但这将引入几个新问题。首先,每次运行脚本时,它都会覆盖日志文件的最新内容。为纠正此问题,请将新的输出添加到现有日志文件末尾。现在,您需要了解日志中每个输出的创建时间,因为文件上的日期时间戳只是指示最后一个输出的写入时间。
每次执行脚本之前,将当前日期和时间写入文件。使用 >> 将输出添加到文件末尾,而不是覆盖现有文件。
在清单 9 中,第一列放置了一个唯一标识符,以便使用 find 和 find next 扫描文件。
还可以将当前日期和时间写入日志文件。在清单 9 中,$(date) 指示 Korn shell 执行 date 命令并将输出放入 echo 命令行。无论您何时需要执行 UNIX 命令和使用输出,请键入 $ 并将命令括在圆括号内。
stdout 添加到日志文件 #!/bin/ksh echo "#--- $(date)" > /usr/local/acme/logs/vmstat.log vmstat 2 30 > /usr/local/acme/logs/vmstat.log
在清单 10 中,指示 Korn shell 运行 netstat 命令,使用 grep 搜索“ESTABLISH”,使用 wc 计算行数,这些命令括在 $() 中。再指示 Korn shell 将这些命令的输出存储在环境变量 CTR_ESTAB 中。然后在 echo 命令中,指示 Korn shell 使用该环境变量中存储的值。要使用环境变量中存储的值,请将 $ 置于变量名之前,例如:$CTR_ESTAB。为提高可读性和避免歧义,请使用将变量名括在花括号内的 Korn shell 选项,例如:${CTR_ESTAB}。
$(xxx) 在 Korn shell 脚本中执行命令
# store current date as YYYYMMDD in variable DATE for later
# use
export DATE=$(date +m%d)
# count number of established socket connections and write
# to log
export CTR_ESTAB=$(netstat -na -P tcp | grep ESTABLISH | wc
-l)
export CTR_CLOSE_WAIT=$(netstat -na -P tcp | grep CLOSE WAIT
| wc -l)
echo "${DATE} ${CTR_ESTAB} ${CTR_CLOSE_WAIT} >> ${LOG_FILE}
如果多个用户同时运行脚本,会出现什么情况?由于脚本的每个实例都写入同一个输出文件,因此每个脚本的输出将在输出文件中交错出现。可以通过在文件名中放入 PID 号(由 $$ 表示)来创建唯一的输出文件名,如清单 11 所示。
$$ 利用当前 PID 生成唯一的文件名 #!/bin/ksh echo "#--- $(date)" >> /usr/local/acme/logs/vmstat.$$.log vmstat 2 30 >> /usr/local/acme/logs/vmstat.$$.log
当下一个用户运行该脚本时,会为脚本的执行分配一个不同的 PID,因此结果是每次创建一个单独的日志文件,而不是将输出添加到现有日志文件。可能这样也不是什么坏事,但不是您希望实现的结果。
另一种可能是使用在脚本执行之前在脚本外设置一次的环境变量,而不是使用每次执行脚本时值都会变化的环境变量。只要用户登录,UNIX 就会自动设置 LOGNAME 环境变量。在清单 12 中,该值会嵌入日志文件名中,这样每个用户可以有一个日志文件:
#!/bin/ksh
echo "#--- $(date)"
>> /usr/local/acme/logs/vmstat.${LOGNAME}.log
vmstat 2 30
>> /usr/local/acme/logs/vmstat.${LOGNAME}.log
最后再润色两下就可以完成您的基本 Korn shell 脚本了。首先,如果您希望更改 vmstat 命令的频率或持续时间,该怎么办?可以使用命令行参数接受这些值,而不是在 vmstat 命令中硬编码时间间隔和持续时间。这些参数可以存储在 vmstat 命令可以访问的环境变量中。当然,脚本必须提供默认值,以防用户不使用命令行提供值。
其次,如果您对日志文件的命名约定改变了主意,该怎么办?这不是您希望用户每次必须使用命令行参数提供的东西。不过,如果您已经将日志文件名编码到脚本内的多个行中,当您决定使用不同的命名约定时,您将不得不搜索每一行脚本以查看指定名称的位置。
相反,可以将日志文件名存储在环境变量中,并修改每条命令以便将输出添加到变量中所包含的文件名。然后,当您更改日志文件命名约定时,您只需修改设置环境变量的那一行,如清单 13 所示。
清单 13:更健壮的capture_vmstat.sh 脚本版本
#!/bin/ksh
# ----------------------------------------------------
# capture_vmstat.sh <INTERVAL> <COUNT>
# <INTERVAL> vmstat interval
# <COUNT> vmstat count
# run vmstat and capture output to a log file
#-----------------------------------------------------
# indicate defaults for how often and for how long
# to run vmstat
export INTERVAL=2 # every 2 seconds
export COUNT=30 # do it 30 times
# obtain command line arguments, if present
if [ "${1}" != "" ]
then
INTERVAL=${1}
# if there is one command line argument,
# maybe there's two
if [ "${2}" != "" ]
then
COUNT=${2}
fi
fi
# directories where scripts and logs are stored
export PROGDIR=/usr/local/acme/scripts
export LOGDIR=/usr/local/acme/logs
# define logfile name and location
export LOG_FILE=${LOGDIR}/capture_vmstat.${LOGNAME}.log
# write current date/time to log file
echo "#--- $(date)" >> ${LOG_FILE}
vmstat ${INTERVAL} ${COUNT} >> ${LOG_FILE}
# say goodnight, Gracie
exit 0
for 循环脚本有时您希望针对一个对象列表执行一个命令。例如,您可能希望使用 rsh 命令对多台服务器远程执行同一命令(有关详细信息以及使用 r 命令的安全风险,请参见 man rsh)。
一个方法是将对象列表存储在一个环境变量(可能名为 LIST)中。然后可以使用 for 循环重复执行 rsh 命令,每次循环取 LIST 中的下一个值。清单 14 显示了 for 循环脚本的一个示例。
for 循环脚本
#!/bin/ksh
export LIST="bvapp1 bvapp2 bvapp3"
export LOG=/usr/local/acme/logs/throw_away.log
for SERVER in ${LIST}
do
# each loop has a different value for ${SERVER}
echo "#------- values from ${SERVER}" >> ${LOG}
rsh ${SERVER}
"ps -f -u bv -o pid,pmem,pcpu,rss,vsz" >> ${LOG}
done
# say goodnight, Gracie
exit 0
while 循环脚本有时您可能希望执行一个命令,等一会儿,然后再次执行该命令。有时您希望该循环无限继续,而有时则希望循环执行有限次数后终止。
比如说,您希望监视在用户 bv 下运行的进程。您希望每隔 10 秒监视 bv 一次,一共监视 2 小时。首先,使用清单 15 中的代码以交互方式测试命令(详细信息,请参见 man ps)。
-o 参数的交互式 ps 命令 ps -f -u bv -o pid,pcpu,pmem,rss,vsz,comm
现在需要编写一个脚本文件来循环执行此命令。该循环将在每次执行 ps 命令之间暂停 10 秒。循环应执行 720 次 [每隔 10 秒意味着每分钟 6 次或每小时 360 次 (60 分钟/小时 * 6/分钟),持续 2 小时]。清单 16 显示一个简单的 while 循环脚本。
while 循环脚本
#!/bin/ksh
export INTERVAL=10
export COUNT=720
export LOG=/usr/local/acme/logs/while_loop_test.log
export CTR=0
while [ true ]
do
if [ ${CTR} -ge ${COUNT} ]
then
exit
fi
echo "#------- $(date +m%d-03/24/03M%S)"
> ${LOG}
ps -f -u bv -o pid,pcpu,pmem,rss,vsz,comm
> ${LOG}
CTR=$(expr ${CTR} + 1)
sleep ${INTERVAL}
done
清单 17 显示输出日志文件的片段。
清单 17:while 循环脚本的输出 #------- 19991203-123237 PID %CPU %MEM RSS VSZ COMMAND 12007 0.2 0.8 13640 24280 cmsdb 11938 0.0 0.7 11536 20496 sched_poll_d <snip> #------- 19991203-123240 PID %CPU %MEM RSS VSZ COMMAND 12007 0.2 0.8 13640 24280 cmsdb 11938 0.0 0.7 11536 20496 sched_poll_d <snip> #------- 19991203-123243 PID %CPU %MEM RSS VSZ COMMAND 12007 0.3 0.8 13640 24280 cmsdb 11938 0.0 0.7 11536 20496 sched_poll_d <snip> #------- 19991203-123246 <and so on>
以下列表中的编程提示和技巧是本文所介绍的编程风格和方法的快速参考。在列表中,您将找到本文所述(更详细的)各项的快速参考版本。
#!/bin/ksh
BIN_DIR=/opt/bv1to1/bin
export SUPPORT_IDS="userA@domain.com,userB@domain.com
$,将命令括在圆括号中,并将输出存储在环境变量中:export CTR_ESTAB=$(netstat -na | grep ESTABLISH | wc -l)
$ 置于变量名之前。为提高可读性和避免歧义,请将变量名括在花括号内。
echo "The number of ESTABLISHED connections is ${CTR_ESTAB}"
$$ 将 PID 号包括在文件名中。将 PID 号插入到文件名中的文件扩展名前面。export LOG_FILE=/tmp/capture_vmstat.$$.log
chmod +x 文件名 使脚本文件变得可执行:chmod +x capture_vmstat.sh
./capture_vmstat.sh
stdout (>) 重定向到日志文件或将 stdout (>>) 添加到日志文件:
./capture_vmstat.sh >> ${LOG_FILE}
stderr 重定向到与 stdout 相同的目的地或一个唯一文件:
./capture_vmstat.sh >> ${LOG_FILE} 2>&1
- 或 -
./capture_vmstat.sh >> ${LOG_FILE} 2>>${ERR_LOG}
for 循环处理一系列事物:
export LIST=$(ls *sh)
for FILE in ${LIST}
do
echo "Processing ${FILE}"
cat ${FILE} | mailx -s "Here is ${FILE}"
userA@domain.com
done
while 循环重复处理同一命令:
export INTERVAL=20
export COUNT=180
export CTR=0
while [ true ]
do
if [ ${CTR} -ge ${COUNT} ]
then
exit
fi
# --- do some command here ---
sleep ${INTERVAL}
CTR=$(expr ${CTR} + 1)
done
《Unix Shell Programming》,第三版,作者:Stephen G. Kochan 和 Patrick H. Wood(2003 年,Sams Publishing)
Ken Gottry 是 NerveWire, Inc. 的资深基础架构架构师。他拥有 30 年的系统从业经验,从大型主机到台式机。过去 10 年,他专注于分布式、多层和基于 Web 的系统的设计、实现和调优。作为一个为众多 G2K 公司提供咨询的性能工程师,他对 Oracle Solaris 上运行的 Web 服务器、应用服务器和 Java 虚拟机进行了评估和调优。
| 修订版 1.0,2012 年 2 月 15 日 |