文章目录
  1. 1. 前言
  2. 2. 常用参数
    1. 2.1. 交互式login shell
    2. 2.2. bash和sh
  3. 3. 脚本调试
  4. 4. 环境变量
  5. 5. 命令历史
  6. 6. 资源限制
  7. 7. 最后

文章转自SHELL编程之执行环境

微博ID:orroz

微信公众号:Linux系统技术

前言

本文是shell编程系列的第三篇,主要介绍bash脚本的执行环境以及注意事项。通过本文,应该可以帮助您解决以下问题:

  • 执行bash和执行sh究竟有什么区别?
  • 如何调试bash脚本?
  • 如何记录用户在什么时候执行的某条命令?
  • 为什么有时ulimit命令的设置不生效或者报错?
  • 环境变量和一般变量有什么区别??

常用参数

交互式login shell

关于bash的编程环境,首先我们要先理解的就是bash的参数。不同方式启动的bash一般默认的参数是不一样的。一般在命令行中使用的bash,都是以login方式打开的,对应的参数是:-l或—login。还有-i参数,表示bash是以交互方式打开的,在默认情况下,不加任何参数的bash也是交互方式打开的。这两种方式都会在启动bash之前加载一些文件:

首先,bash去检查/etc/profile文件是否存在,如果存在就读取并执行这个文件中的命令。

之后,bash再按照以下列出的文件顺序依次查看是否存在这些文件,如果任何一个文件存在,就读取、执行文件中的命令:

  1. ~/.bash_profile
  2. ~/.bash_login
  3. ~/.profile

这里要注意的是,本步骤只会检查到一个文件并处理,即使同时存在2个或3个文件,本步骤也只会处理最优先的那个文件,而忽略其他文件。以上两个步骤的检查都可以用—noprofile参数进行关闭。

当bash是以login方式登录的时候,在bash退出时(exit),会额外读取并执行~/.bash_logout文件中的命令。

当bash是以交互方式登录时(-i参数),bash会读取并执行~/.bashrc中的命令。—norc参数可以关闭这个功能,另外还可以通过—rcfile参数指定一个文件替代默认的~/.bashrc文件。

以上就是bash以login方式和交互式方式登录的主要区别,根据这个过程,我们到RHEL7的环境上看看都需要加载哪些配置:

1.首先是加载/etc/profile。根据RHEL7上此文件内容,这个脚本还需要去检查/etc/profile.d/目录,将里面以.sh结尾的文件都加载一遍。具体细节可以自行查看本文件内容。
2.之后是检查~/.bash_profile。这个文件中会加载~/.bashrc文件。
3.之后是处理~/.bashrc文件。此文件主要功能是给bash环境添加一些alias,之后再加载/etc/bashrc文件。
4.最后处理/etc/bashrc文件。这个过程并不是bash自身带的过程,而是在RHEL7系统中通过脚本调用实现。

了解了这些之后,如果你的bash环境不是在RHEL7系统上,也应该可以确定在自己环境中启动的bash到底加载了哪些配置文件。

bash和sh

几乎所有人都知道bash有个别名叫sh,也就是说在一个脚本前面写!#/bin/bash和#!/bin/sh似乎没什么不同。但是下面我们要看看它们究竟有什么不同。

首先,第一个区别就是这两个文件并不是同样的类型。如果细心观察过这两个文件的话,大家会发现:

1
2
3
4
[zorro@zorrozou-pc0 bash]$ ls -l /usr/bin/sh
lrwxrwxrwx 1 root root 4 11月 24 04:20 /usr/bin/sh -> bash
[zorro@zorrozou-pc0 bash]$ ls -l /usr/bin/bash
-rwxr-xr-x 1 root root 791304 11月 24 04:20 /usr/bin/bash

sh是指向bash的一个符号链接。符号链接就像是快捷方式,那么执行sh就是在执行bash。这说明什么?说明这两个执行方式是等同的么?实际上并不是。我们都知道在程序中是可以获得自己执行命令的进程名称的,这个方法在bash编程中可以使用$0变量来实现,参见如下脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[zorro@zorrozou-pc0 bash]$ cat name.sh
#!/bin/bash
echo $0
case $0 in
*name.sh)
echo "My name is name!"
;;
*na.sh)
echo "My name is na"
;;
*)
echo "Name error!"
;;
esac

这个脚本直接执行的结果是:

1
2
3
[zorro@zorrozou-pc0 bash]$ ./name.sh
./name.sh
My name is name!

大家也能看到脚本中有个逻辑是,如果进程名字是以na.sh结尾,那么打印的内容不一样。我们如何能让同一个程序触发这段不同的逻辑呢?其实很简单,就是给这个脚本创建一个叫na.sh的符号链接:

1
2
3
4
[zorro@zorrozou-pc0 bash]$ ln -s name.sh na.sh
[zorro@zorrozou-pc0 bash]$ ./na.sh
./na.sh
My name is na

通过符号链接的方式改变进程名称是一种常见的编程技巧,我们可以利用这个办法让程序通过不同进程名触发不同处理逻辑。所以大家以后再遇到类似bash和sh这样的符号链接关系的进程时要格外注意它们的区别。在这里它们到底有什么区别呢?实际上bash的源代码中对以bash名称和sh名称执行的时候,会分别触发不同的逻辑,主要的逻辑区别是:以sh名称运行时,会相当于以—posix参数方式启动bash。这个方式跟一般方式的具体区别可以参见:http://tiswww.case.edu/php/chet/bash/POSIX。

我遇到过很多次因为不同文件名的处理逻辑不同而引发的问题。其中一次是因为posix模式和一般模式的ulimit -c设置不同导致的。ulimit -c参数可以设置进程出现coredump时产生的文件的大小限制。因为内存的页大多都是4k,所以一般core文件都是最小4k一个,当ulimit -c参数设置小于4k时,无法正常产生core文件。为了调试方便,我们的生产系统都开了ulimit -c限制单位为4。因为默认ulimit -c的限制单位是1k,ulimit -c 4就是4k,够用了。但是我们仍然发现部分服务器不能正常产生core文件,最后排查定位到,这些不能产生core文件的配置脚本只要将#!/bin/sh改为#!/bin/bash就可以正常产生core文件。于是郁闷之余,查阅了bash的处理代码,最终发现原来是这个坑导致的问题。原因是:在posix模式下,ulimit -c的参数单位不是1024,而是512。至于还有什么其他不同,在上述链接中都有说明。

脚本调试

程序员对程序的调试工作是必不可少的,bash本身对脚本程序提供的调试手段不多,printf大法是必要技能之一,当然在bash中就是echo大法。另外就是bash的-v参数、-x参数和-n参数。

-v参数就是可视模式,它会在执行bash程序的时候将要执行的内容也打印出来,除此之外,并不改变bash执行的过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
[zorro@zorrozou-pc0 bash]$ cat arg.sh
#!/bin/bash -v
echo $0
echo $1
echo $2
ls /123
echo $3
echo $4
echo $#
echo $*
echo $?

执行结果是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[zorro@zorrozou-pc0 bash]$ ./arg.sh 111 222 333 444 555
#!/bin/bash -v
echo $0
./arg.sh
echo $1
111
echo $2
222
ls /123
ls: cannot access '/123': No such file or directory
echo $3
333
echo $4
444
echo $#
5
echo $*
111 222 333 444 555
echo $?
0

-n参数用来检查bash的语法错误,并且不会真正执行bash脚本。这个就不举例子了。另外,三种方式除了可以直接在bash后面加参数以外,还可以在程序中随时使用内建命令set打开和关闭,方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[zorro@zorrozou-pc0 bash]$ cat arg.sh
#!/bin/bash
set -v
#set -o verbose
echo $0
set +v
echo $1
set -x
#set -o xtrace
echo $2
ls /123
echo $3
set +x
echo $4
echo $#
set -n
#set -o noexec
echo $*
echo $?
set +n

执行结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[zorro@zorrozou-pc0 bash]$ ./arg.sh
#set -o verbose
echo $0
./arg.sh
set +v
+ echo
+ ls /123
ls: cannot access '/123': No such file or directory
+ echo
+ set +x
0

以上例子中顺便演示了1、3、#、?的意义,大家可以自行对比它们的区别以理解参数的意义。另外再补充一个-e参数,这个参数可以让bash脚本命令执行错误的时候直接退出,而不是继续执行。这个功能在某些调试的场景下非常有用!

本节只列出了几个常用的参数的意义和使用注意事项,希望可以起到抛砖引玉的作用。大家如果想要学习更多的bash参数,可以自行查看bash的man手册,并详细学习set和shopt命令的使用方法。

环境变量

我们目前已经知道有个PATH变量,bash会在查找外部命令的时候到PATH所记录的目录中进行查找,从这个例子我们可以先理解一下环境变量的作用。环境变量就类似PATH这种变量,是bash预设好的一些可能会对其状态和行为产生影响的变量。bash中实现的环境变量个数大概几十个,所有的帮助说明都可以在man bash中找到。我们还是拿一些会在bash编程中经常用到的来讲解一下。

我们可以使用env命令来查看当前bash已经定义的环境变量。set命令不加任何参数可以查看当前bash环境中的所有变量,包括环境变量和私有的一般变量。一般变量的定义方法:

1
2
3
4
5
6
[zorro@zorrozou-pc0 ~]$ aaa=1000
[zorro@zorrozou-pc0 ~]$ echo $aaa
1000
[zorro@zorrozou-pc0 ~]$ env|grep aaa
[zorro@zorrozou-pc0 ~]$ set|grep aaa
aaa=1000

上面我们定义了一个变量名字叫做aaa,我们能看到在set命令中可以显示出这个变量,但是env不显示。export命令可以将一个一般变量编程环境变量。

1
2
3
4
5
[zorro@zorrozou-pc0 ~]$ export aaa
[zorro@zorrozou-pc0 ~]$ env|grep aaa
aaa=1000
[zorro@zorrozou-pc0 ~]$ set|grep aaa
aaa=1000

export之后,env和set都能看到这个变量了。一般变量和环境变量的区别是:一般变量不能被子进程继承,而环境变量会被子进程继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[zorro@zorrozou-pc0 ~]$ env|grep aaa
aaa=1000
[zorro@zorrozou-pc0 ~]$ bbb=2000
[zorro@zorrozou-pc0 ~]$ echo $bbb
2000
[zorro@zorrozou-pc0 ~]$ echo $aaa
1000
[zorro@zorrozou-pc0 ~]$ env|grep bbb
[zorro@zorrozou-pc0 ~]$ bash
[zorro@zorrozou-pc0 ~]$ echo $aaa
1000
[zorro@zorrozou-pc0 ~]$ echo $bbb
[zorro@zorrozou-pc0 ~]$

上面测试中,我们的bash环境里有一个环境变量aaa=1000,又定义了一个一般变量bbb=2000。此时我们在用bash打开一个子进程,在子进程中我们发现,aaa变量仍然能取到值,但是bbb不可以。证明aaa可以被子进程继承,bbb不可以。

搞清楚了环境变量的基础知识之后,再来看一下bash中常用的环境变量:

进程自身信息相关

BASH:当前bash进程的进程名。

BASHOPTS:记录了shopt命令已经设置为打开的选项。

BASH_VERSINFO:bash的版本号信息,是一个数组。可以使用命令:echo ${BASH_VERSINFO[*]}查看数组的信息。有关数组的操作我们会在其它文章里详细说明。

BASH_VERSION:bash的版本号信息。比上一个信息更少一点。

HOSTNAME:系统主机名信息。

HOSTTYPE:系统类型信息。

OLDPWD:上一个当前工作目录。

PWD:当前工作目录。

HOME:主目录。一般指进程uid对应用户的主目录。

SHELL:bash程序所在路径。

常用数字

RANDOM:每次取这个变量的值都能得到一个0-32767的随机数。

SECONDS:当前bash已经开启了多少秒。

BASHPID:当前bash进程的PID。

EUID:进程的有效用户id。

GROUPS:进程组身份。

PPID:父进程PID。

UID:用户uid。

提示符

PS1:用户bash的交互提示符,主提示符。

PS2:第二提示符,主要用在一些除了PS1之外常见的提示符场景,比如输入了’之后回车,就能看到这个提示符。

PS3:用于select语句的交互提示符。

PS4:用于跟踪执行过程时的提示符,一般显示为”+”。比如我们在bash中使用set -x之后的跟踪提示就是这个提示符显示的。

命令历史

交互bash中提供一种方便追溯曾经使用的命令的功能,叫做命令历史功能。就是将曾经用过的命令纪录下来,以备以后查询或者重复调用。这个功能在交互方式的bash中默认打开,在bash编程环境中默认是没有开启的。可以使用set +H来关闭这个功能,set -H打开这个功能。在开启了history功能的bash中我们可以使用history内建命令查询当前的命令历史列表:

1
2
3
4
5
[zorro@zorrozou-pc0 bash]$ history
1 sudo bash
2 ps ax
3 ls
4 ip ad sh

命令历史的相关配置都是通过bash的环境变量来完成的:

HISTFILE:记录命令历史的文件路径。

HISTFILESIZE:命令历史文件的行数限制

HISTCONTROL:这个变量可以用来控制命令历史的一些特性。比如一般的命令历史会完全按照我们执行命令的顺序来完整记录,如果我们连续执行相同的命令,也会重复记录,如:

1
2
3
4
5
6
7
8
9
10
11
12
[zorro@zorrozou-pc0 bash]$ pwd
/home/zorro/bash
[zorro@zorrozou-pc0 bash]$ pwd
/home/zorro/bash
[zorro@zorrozou-pc0 bash]$ pwd
/home/zorro/bash
[zorro@zorrozou-pc0 bash]$ history
......
1173 pwd
1174 pwd
1175 pwd
1176 history

我们可以利用这个变量的配置来消除命令历史中的重复记录:

1
2
3
4
5
6
7
8
9
10
11
12
[zorro@zorrozou-pc0 bash]$ export HISTCONTROL=ignoredups
[zorro@zorrozou-pc0 bash]$ pwd
/home/zorro/bash
[zorro@zorrozou-pc0 bash]$ pwd
/home/zorro/bash
[zorro@zorrozou-pc0 bash]$ pwd
/home/zorro/bash
[zorro@zorrozou-pc0 bash]$ history
1177 export HISTCONTROL=ignoredups
1178 history
1179 pwd
1180 history

这个变量还有其它配置,ignorespace可以用来让history忽略以空格开头的命令,ignoreboth可以同时起到ignoredups和ignorespace的作用,

HISTIGNORE:可以控制history机制忽略某些命令,配置方法:

1
export HISTIGNORE=”pwd:ls:cd:”。

HISTSIZE:命令历史纪录的命令个数。

HISTTIMEFORMAT:可以用来定义命令历史纪录的时间格式.在命令历史中记录命令执行时间有时候很有用,配置方法:

1
export HISTTIMEFORMAT='%F %T '

相关时间格式的帮助可以查看man 3 strftime。

HISTCMD:当前命令历史的行数。

在交互式操作bash的时候,可以通过一些特殊符号对命令历史进行快速调用,这些符号基本都是以!开头的,除非!后面跟的是空格、换行、等号=或者小括号():

!n:表示引用命令历史中的第n条命令,如:!123,执行第123条命令。

!-n:表示引用命令历史中的倒数第n条命令,如:!-123,执行倒数第123条命令。

!!:重复执行上一条命令。

!string:在命令历史中找到最近的一条以string字符串开头的命令并执行。

!?string[?]:在命令历史中找到最近的一条包括string字符的命令并执行。如果最有一个?省略的话,就是找到以string结尾的命令。

^string1^string2^:将上一个命令中的string1字符串替换成string2字符串并执行。可以简写为:^string1^string2

!#:重复当前输入的命令。

以下符号可以作为某个命令的单词代号,如:

^:!^表示上一条命令中的第一个参数,$123^表示第123条命令的第一个参数。

$:!$表示上一条命令中的最后一个参数。!123$表示第123条命令的最后一个参数。

n(数字):!!0表示上一条命令的命令名,!!3上一条命令的第三个参数。!123:3第123条命令的第三个参数。

:表示所有参数,如:!123:\或!123*

x-y:x和y都是数字,表示从第x到第y个参数,如:!123:1-6表示第123条命令的第1个到第6个参数。只写成-y,取前y个,如:!123:-7表示0-7。

x:表示取从第x个参数之后的所有参数,相当于x-$。如:!123:2\

x-:表示取从第x个参数之后的所有参数,不包括最后一个。如:!123:2-

选择出相关命令或者参数之后,我们还可以通过一些命令对其进行操作:

h 删除所有后面的路径,只留下前面的

1
2
3
4
5
[zorro@zorrozou-pc0 bash]$ ls /etc/passwd
/etc/passwd
[zorro@zorrozou-pc0 bash]$ !!:h
ls /etc
...

紧接着上面的命令执行,相当于运行passwd。

r 删除后缀.xxx, 留下文件名

1
2
3
4
[zorro@zorrozou-pc0 bash]$ ls 123.txt
ls: cannot access '123.txt': No such file or directory
[zorro@zorrozou-pc0 bash]$ !!:r
ls 123

e 删除文件名, 留下后缀

1
2
3
[zorro@zorrozou-pc0 bash]$ !-2:e
.txt
bash: .txt: command not found

p 只打印结果命令,但不执行

1
2
3
4
[zorro@zorrozou-pc0 bash]$ ls /etc/passwd
/etc/passwd
[zorro@zorrozou-pc0 bash]$ !!:p
ls /etc/passwd

q 防止代换参数被再次替换,相当于给选择的参数加上了’’,以防止其被转义。

1
2
3
4
5
[zorro@zorrozou-pc0 bash]$ ls `echo /etc/passwd`
/etc/passwd
[zorro@zorrozou-pc0 bash]$ !!:q
'ls `echo /etc/passwd`'
-bash: ls `echo /etc/passwd`: No such file or directory

x 作用同上,区别是每个参数都会分别给加上’’。如:

1
2
3
4
[zorro@zorrozou-pc0 bash]$ !-2:x
'ls' '`echo' '/etc/passwd`'
ls: cannot access '`echo': No such file or directory
ls: cannot access '/etc/passwd`': No such file or directory

s/old/new/ 字符串替换,跟上面的^^类似,但是可以指定任意历史命令。只替换找到的第一个old字符串。
& 重复上次替换
g 在执行s或者&命令作为前缀使用,表示全局替换。

资源限制

每一个进程环境中都有对于资源的限制,bash脚本也不例外。我们可以使用ulimit内建命令查看和设置bash环境中的资源限制。

[zorro@zorrozou-pc0 ~]$ ulimit -a
core file size (blocks, -c) unlimited
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 63877
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 63877
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited

在上文讲述bash和sh之间的区别时,我们已经接触过这个命令中的-c参数了,用来限制core文件的大小。我们再来看看其它参数的含义:

data seg size:程序的数据段限制。

scheduling priority:优先级限制。相关概念的理解可以参考这篇:http://wp.me/p79Cit-S

file size:文件大小限制。

pending signals:未决信号个数限制。

max locked memory:最大可以锁内存的空间限制。

max memory size:最大物理内存使用限制。

open files:文件打开个数限制。

pipe size:管道空间限制。

POSIX message queues:POSIX消息队列空间限制。

real-time priority:实时优先级限制。相关概念的理解可以参考这篇:http://wp.me/p79Cit-S

stack size:程序栈空间限制。

cpu time:占用CPU时间限制。

max user processes:可以打开的的进程个数限制。

virtual memory:虚拟内存空间限制。

file locks:锁文件个数限制。

以上参数涉及各方面的相关知识,我们在此就不详细描述这些相关内容了。在此我们主要关注open files和max user processes参数,这两个参数是我们在优化系统时最常用的两个参数。

这里需要注意的是,使用ulimit命令配置完这些参数之后的bash产生的子进程都会继承父进程的相关资源配置。ulimit的资源配置的继承关系类似环境变量,父进程的配置变化可以影响子进程。所以,如果我们只是在某个登录shell或者交互式shell中修改了ulimit配置,那么在这个bash环境中执行的命令和产生的子进程都会受到影响,但是对整个系统的其它进程没有影响。如果我们想要让所有用户一登录就有相关的配置,可以考虑把ulimit命令写在bash启动的相关脚本中,如/etc/profile。如果只想影响某一个用户,可以写在这个用户的主目录的bash启动脚本中,如~/.bash_profile。系统的pam模块也给我们提供了配置ulimit相关限制的配置方法,在centos7中大家可以在以下目录和文件中找到相关配置:

1
2
3
4
[zorro@zorrozou-pc0 bash]$ ls /etc/security/limits.d/
10-gcr.conf 99-audio.conf
[zorro@zorrozou-pc0 bash]$ ls /etc/security/limits.conf
/etc/security/limits.conf

即使是写在pam相关配置文件中的相关配置,也可能不是系统全局的。如果你想给某一个后台进程设置ulimit,最靠谱的办法还是在它的启动脚本中进行配置。无论如何,只要记得一点,如果相关进程的ulimit没生效,要想的是它的父进程是谁?它的父进程是不是生效了?

ulimit参数中绝大多数配置都是root才有权限改的更大,而非root身份只能在现有的配置基础上减小限制。如果你执行ulimit的时候报错了,请注意是不是这个原因。
```

最后

通过本文我们学习了bash编程的进程环境的相关内容,主要包括的知识点为:

  • bash的常用参数。
  • bash的环境变量。
  • 命令历史功能和相关变量配置。
  • bash脚本的资源限制ulimit的使用。
文章目录
  1. 1. 前言
  2. 2. 常用参数
    1. 2.1. 交互式login shell
    2. 2.2. bash和sh
  3. 3. 脚本调试
  4. 4. 环境变量
  5. 5. 命令历史
  6. 6. 资源限制
  7. 7. 最后