这篇文章包含开发shell脚本之前首先要了解的东西、刚接触shell开发容易遇到的常见错误等。其中有些总结是我师傅总结的,我在上增加了些自己的经验和教训,然后再把这些东西发出来给大家共享一下
1. 什么是环境变量
环境变量可以理解为进程内一种特殊的全局变量。在进程的内存中有一个全局变量,这是一个字符串数组,每个字符串里面的内容就是"key=value"形式的环境变量。当使用export增加或者修改一个环境变量时,实际就是修改的__environ数组。当我们输入一个命令时,比如ls,bash解释器就从这个数组中查找PATH环境变量的值,这个值是冒号分隔的目录列表,再按这个目录列表从前往后依次查找ls可执行程序。可以使用gdb调试进程,看到具体的__environ数组的值(详见附件)
2. 由iMAP执行 . svc_profile.sh看脚本执行方式
在进入iMAP的主目录后首先要执行. svc_profile.sh,而我们平时执行脚本的时候都是通过“./test.sh”或者“bash test.sh”执行脚本这种方式,那么,这两种方式有何不同呢:
1) 通过“点+空格+脚本”的执行脚本,是当前bash解释器进程读取并执行该脚本,所以svc_profile.sh里面设置的环境变量就会在当前bash进程中生效。在后台登陆sybase数据库时,就需要使用这种方式执行sybase的环境变量,否则会报一个类似于这样的错误:
dbuser@linux:/opt/sybase/ASE-15_0/install> /opt/sybase/ASE-15_0/bin/dataserver: error while loading shared libraries: libsbgse2.so: cannot open shared object file: No such file or directory
原因就是库的路径没有再环境变量里生效就没有找到这个库。
2) 通过“./test.sh”执行脚本过程中,当前bash进程会派生一个子bash进程去读取和执行脚本,脚本中设置的环境变量也就在该子进程中生效了,你的当前bash进程是没有办法办法使用这些环境变量的。
3) 通过“test.sh”执行脚本。由于“./”表示当前目录,所以加上“./”执行脚本表示在当前目录下找这个脚本执行;如果不加,shell解释器会从$PATH环境变量指定的路径里面查找,而通常$PATH里面是没有"."(表示当前路径)的,所以会报找不到shellscript.sh
4) 由于环境变量不是操作系统内核的东西,所以子进程使用的环境变量都是从父进程继承来的。当我们调用execve派生子进程时,第三个参数就是环境变量,操作系统内核会把这个数据传给新进程,最终execve执行完时,__environ数组的内容跟传入的内容是一致的。
int execve(const char *path, char *const argv[],char *const envp[]);
这样子进程就拥有了和父进程相同的环境变量了。另外main函数有一个隐含的参数就是这个环境变量数组的。
5) 经常被继承的环境变量有这些:
语言环境变量:LANG、LC_*、LC_ALL;
语言环境变量决定字符集的处理方式,设置不正确会导致:字符串转码失败、数据库操作失败、界面显示乱码、……等问题。
时区环境变量:TZ;
TZ环境变量决定着进程所用的时区规则,设置不正确会导致:获取本地时间错误、夏令时跳变错误、UTC/本地时间转换错误、……等问题。
可执行文件查找路径:PATH;
PATH环境变量决定了可执行文件的查找路径,设置不正确会导致进程调用外部可执行程序时,如果没有指定路径信息,可能会执行失败。
动态库查找路径:LD_LIBRARY_PATH;
Windows环境中,PATH环境变量即决定可执行文件的查找路径,也决定动态库的查找路径。但在Solaris/Linux环境中,动态库的查找路径由$LD_LIBRARY_PATH决定。如果设置错误,程序在加载或执行过程中会因为动态库找不到而异常退出(这种退出不会产生core文件)。
根据以上的解释,在执行脚本时就可以清楚地知道要采用哪种方式了。
3. 为什么shell脚本的开头会有#!/bin/bash这一行呢
1) "#!"就是指定了解释器执行,使用./test.sh的方式来执行脚本的时候就需要加上可执行权限,因为此时./test.sh相当于一个可执行程序。
2) "#!/bin/bash"是告诉shell解释器,用什么程序来执行这个文件。现在我们把./test.sh的脚本头改成"#!/bin/abc"再执行,我们可以看到报错如下:
linux-86:/opt/zsmTest # ./test.sh
bash: ./test.sh: /bin/ad: bad interpreter: No such file or directory
然后,再改成#!/bin/cat,执行这个脚本时,就会用cat这个命令执行这个脚本,即打印出脚本的内容,相当于执行cat ./test.sh:
linux-86:/opt/zsmTest # ./test.sh
#!/bin/cat
hadoop_pid=`ps -ef|grep hadoop|grep -v grep|awk '{print $2}'`
echo $ hadoop_pid
4. Shell脚本的调试
当Shell脚本没有执行出想要的结果或者打印出了一个错误的时候,可以调试一下,看看执行的详细过程,shell脚本不会像python那样可以单步跟踪,而是使用bash -x [需要执行的脚本],一次性把脚本的执行信息全部打出来,如下图:
linux-86:/opt/zsmTest # bash -x ./test.sh
++ ps -ef
++ grep mesos-master
++ grep -v grep
++ awk '{print $2}'
+ mesos_master_pid=16036
+ echo 16036
16036
linux-86:/opt/zsmTest #
每一行前面都会有“+”,“+”越多表示该句代码的嵌套层次越深。在脚本执行中,如果有语法错误或者脚本中写的路径找不到等问题,会直接有错误信息打屏的。
5. 为什么shell脚本在执行的时候必须是unix格式的
因为dos格式是以0x0D 0x0A两个字节做行分隔符的,而unix格式则只以0x0A做行分隔符,也就是0x0D会被解释器当成是这一行的内容,所以就会出现命令找不到等各种比较诡异的异常。当脚本已经是DOS格式时,可以先执行dos2unix [需要执行的脚本],然后再执行脚本即可。
6. Shell脚本中变量的作用域
1) Shell脚本中定义的变量都是global的,其作用域从被定义的地方开始,到shell结束或被显示删除的地方为止
2) Shell函数定义的变量默认是global的,其作用域从“函数被调用时执行变量定义的地方”开始,到shell结束或被显示删除处为止
3) 如果在函数中,使用local把变量显式的定义为局部变量,则只能在该函数中使用
4) 如果局部变量和全局变量同名,则在函数中,局部变量会覆盖全局变量
7. Shell脚本中函数的返回值
Shell脚本的函数只能返回整形数值,不能直接返回字符串或者数组等其他数据类型,但是可以通过其他方式拿到函数返回的字符串或者其他类型。
1) 使用return把函数返回只能返回整数值
function check_params()
{
# 提供帮助命令
if [ 2 -ne $#]
then
show_help
return 255
fi
if [ $1 != $start ] && [ $1 != $stop ]
then
show_help
return 255
fi
return 0
}
check_params $1 $2;
nRet=$? #此处nRet就是check_params中return的值。
2) 可以通过echo拿到函数返回的字符串等其他数据类型
function check_params()
{
# 提供帮助命令
if [ 2 -ne $#]
then
show_help
echo “error”
elif
then
echo “ok”
fi
}
nRet=`check_params $1 $2` #此处nRet就是check_params中echo的值。
注意:echo并不能像return那样终止函数的执行
8. 编写shell脚本常见的几种注意事项
1) 脚本的入口处需要进行参数个数的判断,参数不满足要求打印usage。
2) 要引用一个变量的话,需要使用$,单引号引起来的内容会屏蔽$,使得通过$引用的变量不能正确替换成正确结果。
ossuser@linux88:/opt/iEMP> alldir=`ls`
ossuser@linux88:/opt/iEMP> echo "$alldir"
bin
iEMP
mttools
uninstall
ossuser@linux88:/opt/iEMP> echo '$alldir'
$alldir
ossuser@linux88:/opt/iEMP>echo "alldir"
alldir
3) Shell脚本中a=1表示给a复制,a = 1表示a和1判等
4) 在脚本中通过export设置已有环境变量时,如PATH,切记:要把新的内容添加到原来内容上,而不是替换原有内容,否则会导致其他功能不可用。
5) 由于我司对安全问题看得比较重,所以脚本中尽量不要有切用户,避免出现使用公共用户操作了私有的东西。
6) 在编写脚本中,首先考虑该脚本或者某一部分命令是在那个目录下执行,用户是哪个,这样在执行脚本的时候就不会出现路径找不到、用户没有权限执行的问题。建议路径写成绝对路径,或者在脚本中使用currentDir=`pwd`取当前路径。
7) 对目录或者文件操作时,先进行准备操作,比如判断目录或者文件是否存在,需要执行的文件先添加执行权限等。
8) 当某个命令在执行过程中可能需要用户输入,导致程序挂住。比如:在进行远程操作的时候,如果机器之间的信任关系丢失了,就会提示用户输入对方主机的密码,就会挂住等待。另外一种常见的情况就是删除一个当前用户没有权限的文件:
ossuser@linux-86:/opt/zsmTest> ls -l
total 4
-rwxr-x--- 1 root root 110 Mar 12 16:12 test.sh
ossuser@linux-86:/opt/zsmTest> rm test.sh
rm: remove write-protected regular file `test.sh'?
对于这种情况,可以使用rm –f来删除(-f表示强制执行),虽然删除失败但是不会使脚本挂住。这就给我们一个警示,在使用一个命令的时候一定要真正了解他的用法和各种异常情况下的操作,而了解这些,最好的方法就是直接看man手册。
9) Shell脚本中也需要对某个命令或者某个函数的返回值进行判断,避免出现使用异常值造成的错误。
10) 在进行模式匹配或者搜索的时候,尽量使用精确的方式,避免错误的获取到错误信息或者误杀其他的进程
11) 脚本中使用的命令一定要准确。
假如想要列出某个目录下目录或者文件的大小
#!/bin/sh
alldir=`ls`
for dir in $alldir;
do
echo "du $dir"
done
打印出的内容出乎意料,是递归的把当前目录下的所有文件或者目录大小都打印出来了,而且文件大小的单位是Bytes,所以上面的脚本就可以改成;
#!/bin/sh
alldir=`ls`
for dir in $alldir;
do
echo "du -s –h $dir"
done
12) 切忌在shell脚本中垒命令,shell脚本虽然没有对象的概念,但是可以通过函数进行结构组织。
8. Shell脚本中常见的几种操作
1) 获取某个进程的信息,以获取进程号为例:
httpd _pid=`ps -ef|grep httpd|grep -v grep|awk '{print $2}'`
httpd _pid就是httpd进程的PID。如果httpd _pid是多个,可能需要for循环进行处理。比如,imap后台apache进程在启动时就会有多个:
root@Frame60 # ps -ef|grep http|grep -v grep
ossuser 14431 14203 0 14:55:01 ? 0:00 /opt/oss/server/3rdTools/apache/bin/httpd -DNFW=apache -k start -f /opt/oss/ser
ossuser 14203 1 0 14:54:13 ? 0:03 /opt/oss/server/3rdTools/apache/bin/httpd -DNFW=apache -k start -f /opt/oss/ser
ossuser 14213 14203 0 14:54:15 ? 0:00 /opt/oss/server/3rdTools/apache/bin/httpd -DNFW=apache -k start -f /opt/oss/ser
ossuser 14211 14203 0 14:54:15 ? 0:00 /opt/oss/server/3rdTools/apache/bin/httpd -DNFW=apache -k start -f /opt/oss/ser
ossuser 14212 14203 0 14:54:15 ? 0:00 /opt/oss/server/3rdTools/apache/bin/httpd -DNFW=apache -k start -f /opt/oss/ser
ossuser 14215 14203 0 14:54:15 ? 0:00 /opt/oss/server/3rdTools/apache/bin/httpd -DNFW=apache -k start -f /opt/oss/ser
ossuser 29353 14203 0 16:21:57 ? 0:00 /opt/oss/server/3rdTools/apache/bin/httpd -DNFW=apache -k start -f /opt/oss/ser
ossuser 14214 14203 0 14:54:15 ? 0:00 /opt/oss/server/3rdTools/apache/bin/httpd -DNFW=apache -k start -f /opt/oss/ser
2) 在一个命令的返回结果中截取某个字符串。比如id
linux-86:/opt/zsmTest # id
uid=0(root) gid=0(root) groups=0(root),105(sfcb)
linux-86:/opt/zsmTest #
当需要知道当前用户是哪个的时候,就需要把root截取出来,
currentUser=`id|cut -f2 -d"("|cut -f1 -d")"`
3) 重定向
当需要替换某个文件的内容或者在某个文件后面追加一些东西的时候,就需要使用重定向:
echo "it is a test" > test.txt
会把test.txt的内容替换为一个字符串"it is a test";
echo "it is a test" >> test.txt
会在test.txt尾部增加一行,内容是"it is a test"
也可以把一个文件的内容替换另一个文件或者追加到另一个文件的末尾:
cat source.txt >> dest.txt
cat source.txt > dest.txt
另外还有一种常见的用法,当你不想打印某个命令执行过程中的一些打屏信息时,可以把这些打屏信息重定向到空设备:
dst=$3
/usr/bin/ssh -q -o 'BatchMode yes' $1 "mkdir -p $dst" > /dev/null 2>&1
/dev/null表示空设备,把这些打屏信息重定向到空设备意思就是丢弃它,因为没有设备处理这个信息。2代表标准错误,1表示标准输出,也就是说错误信息还是会打屏的。
清空文件也可以用重定向的方法
> $DIST_DIR/hadoop/etc/hadoop/masters
或者
echo "" > $DIST_DIR/hadoop/etc/hadoop/masters
>之前什么也没有,就表示为空,重定向到后边那个文件后,就是把文件清空了
4) 定时操作
Shell脚本也可以通过linux提供的crontab来实现定时操作,比如可以定时把日志备份,定时去检测某个配置是否存在等。
9. 执行命令时,手动中断操作的两种方式:Ctrl+z和Ctrl+c
当使用Ctrl+z中断一个命令时,该进程并没有真正退出,而是转换为在后台挂起,比如下面的一个例子:
linux-93:/opt/zsmTest # sleep 1000
^Z
[1]+ Stopped sleep 1000
linux-93:/opt/zsmTest # ps
PID TTY TIME CMD
4809 pts/2 00:00:00 bash
5323 pts/2 00:00:00 bash
31049 pts/2 00:00:00 bash
31773 pts/2 00:00:00 sleep
31778 pts/2 00:00:00 ps
咱们可以看到sleep这个命令并没有真正退出。
linux-93:/opt/zsmTest # top -p 31773
top - 10:49:23 up 27 days, 18:38, 19 users, load average: 0.00, 0.06, 0.18
Tasks: 1 total, 0 running, 0 sleeping, 1 stopped, 0 zombie
Cpu(s): 0.4%us, 0.6%sy, 0.0%ni, 97.8%id, 1.2%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 48263M total, 38141M used, 10122M free, 5892M buffers
Swap: 32763M total, 95M used, 32668M free, 16580M cached
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
31773 root 20 0 11956 632 508 T 0 0.0 0:00.00 sleep
linux-93:/opt/zsmTest # jobs
[1]+ Stopped sleep 1000
linux-93:/opt/zsmTest # fg
sleep 1000
在用top查询进行的状态时,可以看到进程的状态是T,在linux中T表示进程被挂起了。可以通过jobs或者bg查看这些进程,也可以使用fg把这个进程放到前台来执行,让他正常结束。
假如咱们有个脚本是用来起apache进程的,当进程已经存在就不再进行启动。如果在执行这个脚本的过程中,按了Ctrl+z以为退出了,其实进程被挂住了,也不能正常工作。再用这个脚本启动的时候,无论如何也起不来。
下面来看下详细原因:
在一个shell窗口中执行sleep 1000
linux-16:/opt/zsmTest # sleep 1000
^Z
[1]+ Stopped sleep 1000
linux-16:/opt/zsmTest #
在另一个shell窗口中,使用strace跟踪系统调用和信号量
linux-16:~ # ps -ef|grep sleep
root 10714 9099 0 18:57 pts/6 00:00:00 sleep 1000
root 10728 23283 0 18:58 pts/8 00:00:00 grep sleep
linux-16:~ # strace -f -p 10714
Process 10714 attached - interrupt to quit
restart_syscall(<... resuming interrupted call ...>) = ? ERESTART_RESTARTBLOCK (To be restarted)
--- SIGTSTP (Stopped) @ 0 (0) ---
--- SIGTSTP (Stopped) @ 0 (0) ---
restart_syscall(<... resuming interrupted call ...>
可以看到,Ctrl+z其实是向10714这个进程发了一个停止的信号量:SIGTSTP。
如果我们再给10714这个进程发送一个继续执行的信号量,这个进程就可以继续执行了。在第一个shell窗口中输入kill -18 10714,就可以在第二个窗口中看到下面的内容
restart_syscall(<... resuming interrupted call ...>) = ? ERESTART_RESTARTBLOCK (To be restarted)
--- SIGCONT (Continued) @ 0 (0) ---
restart_syscall(<... resuming interrupted call ...>
linux-16:/opt/zsmTest # top -p 10714
top - 19:03:47 up 6 days, 9:10, 10 users, load average: 0.08, 0.05, 0.07
Tasks: 1 total, 0 running, 1 sleeping, 0 stopped, 0 zombie
Cpu(s): 0.9%us, 0.9%sy, 0.0%ni, 98.0%id, 0.1%wa, 0.0%hi, 0.1%si, 0.0%st
Mem: 11914M total, 8570M used, 3343M free, 242M buffers
Swap: 10235M total, 26M used, 10209M free, 6629M cached
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
10714 root 20 0 4272 528 440 S 0 0.0 0:00.00 sleep
当我们使用fg继续执行挂起的任务时,其实就是发了一个SIGCONT信号量
附录
1. 使用gdb查看进程的环境变量
linux-157:/opt/zsmTest # sleep 10000
^Z
[1]+ Stopped sleep 10000
linux-157:/opt/zsmTest # ps
PID TTY TIME CMD
2294 pts/1 00:00:00 sleep
2304 pts/1 00:00:00 ps
14451 pts/1 00:00:01 bash
linux-157:/opt/zsmTest # gdb -p 2294
(gdb) info var __environ
All variables matching regular expression "__environ":
Non-debugging symbols:
0x00007fb6c4e84f60 __environ
0x00007fb6c50a8130 __environ
(gdb) x/xg 0x00007fb6c4e84f60
0x7fb6c4e84f60 <environ>: 0x00007fffe1650ea0
(gdb) x/100xg 0x00007fffe1650ea0
0x7fffe1650ea0: 0x00007fffe16516aa 0x00007fffe16516c3
0x7fffe1650eb0: 0x00007fffe16516d3 0x00007fffe1651705
(gdb) x/s 0x00007fffe16516aa
0x7fffe16516aa: "LESSKEY=/etc/lesskey.bin"
(gdb) x/s 0x00007fffe1651705
0x7fffe1651705: "MANPATH=/usr/share/man:/usr/local/man:/usr/local/share/man"
(gdb)
2. 使用strace跟踪两种脚本执行方式,观察子进程的创建过程
linux-157:~ # strace -f -p 18586 2>&1|egrep "clone|execve" //这是跟踪命令
以下是跟踪 ./test_bash.sh执行时的系统调用,首先是clone生成一个子进程,再用这个子进程执行test_bash.sh这个脚本
clone(Process 14033 attached
[pid 14033] execve("./test_bash.sh", ["./test_bash.sh"], [/* 59 vars */]) = 0
[pid 14033] clone(Process 14034 attached
[pid 14034] clone(Process 14035 attached
[pid 14034] clone( <unfinished ...>
[pid 14034] <... clone resumed> child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f3a25da39d0) = 14036
[pid 14034] clone( <unfinished ...>
[pid 14034] <... clone resumed> child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f3a25da39d0) = 14037
[pid 14036] execve("/usr/bin/grep", ["grep", "hadoop"], [/* 58 vars */] <unfinished ...>
[pid 14036] <... execve resumed> ) = 0
[pid 14037] execve("/usr/bin/grep", ["grep", "-v", "grep"], [/* 58 vars */] <unfinished ...>
[pid 14034] clone( <unfinished ...>
[pid 14037] <... execve resumed> ) = 0
[pid 14034] <... clone resumed> child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f3a25da39d0) = 14038
[pid 14038] execve("/usr/bin/awk", ["awk", "{print $2}"], [/* 58 vars */] <unfinished ...>
[pid 14038] <... execve resumed> ) = 0
[pid 14035] execve("/bin/ps", ["ps", "-ef"], [/* 58 vars */] <unfinished ...>
[pid 14035] <... execve resumed> ) = 0
[pid 14035] read(6, "egrep\0clone|execve\0", 2047) = 19
以下是跟踪执行 . test_bash.sh时的系统调用,这时就没有生成子进程执行这个脚本,而是当前进程在执行,生成的子进程都是来执行脚本中的命令的
clone(Process 14105 attached
[pid 14105] clone(Process 14106 attached
[pid 14105] clone( <unfinished ...>
[pid 14105] <... clone resumed> child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fb427dfb9d0) = 14107
[pid 14105] clone( <unfinished ...>
[pid 14106] execve("/bin/ps", ["ps", "-ef"], [/* 59 vars */] <unfinished ...>
[pid 14105] <... clone resumed> child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fb427dfb9d0) = 14108
[pid 14106] <... execve resumed> ) = 0
[pid 14107] execve("/usr/bin/grep", ["grep", "hadoop"], [/* 59 vars */] <unfinished ...>
[pid 14107] <... execve resumed> ) = 0
[pid 14108] execve("/usr/bin/grep", ["grep", "-v", "grep"], [/* 59 vars */] <unfinished ...>
[pid 14108] <... execve resumed> ) = 0
[pid 14105] clone( <unfinished ...>
[pid 14105] <... clone resumed> child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fb427dfb9d0) = 14109
[pid 14109] execve("/usr/bin/awk", ["awk", "{print $2}"], [/* 59 vars */] <unfinished ...>
[pid 14109] <... execve resumed> ) = 0
[pid 14106] read(6, "egrep\0clone|execve\0", 2047) = 19
这就是昨天说的两种不同执行脚本方式差异的根本原因。
浙公网安备 33010602011771号