1.2 如何运行程序

运行Linux程序有3种方法。

(1)使文件具有可执行权限,直接运行文件。

(2)直接调用命令解释器参见1.4节“Linux Shell是解释型语言”。执行程序。

(3)使用source执行文件。

第三种方法运行结果和前两种是不同的。例1.1中我们运行程序时,采用的是第一种方法。

1.2.1 选婿:位于第一行的#!

当命令行Shell执行程序时,首先判断是否程序有执行权限。如果没有足够的权限,则系统会提示用户:“权限不够”。从安全角度考虑,任何程序要在机器上执行时,必须判断执行这个程序的用户是否具有相应权限。在第一种方法中,我们直接执行文件,则需要文件具有可执行权限。

chmod命令可以修改文件的权限。+x参数使程序文件具有可执行权限。

命令行Shell接收到我们的执行命令,并且判定我们有执行权限后,则调用Linux内核命令新建(fork)一个进程,在新建的进程中调用我们指定的命令。如果这个命令文件是编译型的(二进制文件),则Linux内核知道如何执行文件。不幸的是,我们的echo.sh程序文件并不是编译型的文件,而是文本文件,内核并不知道如何执行,于是,内核返回“not executable format file”(不是可执行的文件类型)出错信息。Shell收到这个信息时说:“内核不知道怎么运行,我知道,这一定是个脚本!”

Shell知道这是个脚本后,启动了一个新的Shell进程来执行这个程序。但是现在的Linux系统往往拥有好几个Shell,到底挑选哪个夫婿呢?这就要看脚本中意哪个了。在第一行中,脚本通过“#! /bin/sh”告诉命令行:“我只和他好,让他来执行吧!”

这种选婿方法有助于执行方式的通用化。用户在编写脚本时,在程序的第一行通过#!来设置运行Shell创建一个什么样的进程来执行此脚本。在我们的echo.sh中,Shell创建了一个/bin/sh(标准Shell)进程来执行脚本。

命令行在扫过第一行,发现#!时,开始试图读取#!之后的字符,搜寻解释器的完整路径。如果在第一行中的解释器也有参数,则一并读取。例如,我们可以这样来引用我们的解释器:

#! /bin/bash-l

这样,命令行Shell会启用一个新的bash进程来执行程序的每一行。并且,-l参数使得这个bash进程的反应与登录Shell相似。

这种选婿方法,使得我们可以调用任何的解释器,并不局限于Linux Shell。例如,我们可以创建这样一个python参见http://www.python.org。程序:

1 #! /usr/bin/python

2 print“hello world!”

当这个文件被赋予可执行权限,并且用第一种方式运行时,就像调用了python解释器来执行一样。

NOTE:

填写完整的解释器路径。如果不知道某解释器的完整路径,可使用whereis命令查询。

alloy@ubuntu:~/Linux Shell/ch1$ whereis bash

bash: /bin/bash /etc/bash.bashrc /usr/share/man/man1/bash.1.gz

每个脚本的头都指定了一个不同的命令解释器,为了帮助你打破#!的神秘性,我们可以这样来写一个脚本,如例1.2所示。

例1.2 自删除脚本

1 #!/bin/rm

2 # 自删除脚本

3 # 当你运行这个脚本时, 基本上什么都不会发生……当然这个文件消失不见了

4 WHATEVER=65

5 echo "This line will never print!"

6 exit $WHATEVER # 不要紧, 脚本是不会在这退出的

当然,你还可以试试在一个README文件的开头加上一个#!/bin/more,并让它具有执行权限。结果将是文档自动列出自己的内容。

1.2.2 找碴:程序执行的差异

3种程序运行方法中,如果#!中指定的Shell解释器和第二种指定的Shell解释器相同的话,这两种的执行结果是相同的。我们来看看第三种方法的执行过程。

例1.3

alloy@ubuntu:~/LinuxShell/ch1$ pwd         #查看当前工作目录

/home/alloy/LinuxShell/ch1              #当前工作目录

alloy@ubuntu:~/LinuxShell/ch1$ source echo.sh  #执行echo.sh文件

“hello world!”                     #输出运行结果

alloy@ubuntu:/tmp$ pwd

/tmp                           #工作目录改变

细心的你,一定发现了不同!是的,当前目录发生了改变!

我们再来看例1.4。

例1.4

alloy@ubuntu:~/LinuxShell/ch1$ pwd           #查看当前工作目录

/home/alloy/LinuxShell/ch1

alloy@ubuntu:~/LinuxShell/ch1$cd /tmp         #改变当前工作目录

alloy@ubuntu:/tmp$ pwd

/tmp                              #工作目录改变

为什么例1.3和例1.4的cd命令可以改变工作目录,而例1.1中的工作目录并没有改变呢?

这个问题的答案,我们将在1.2.3小节揭晓。

1.2.3 Shell的命令种类

Linux Shell可执行的命令有3种:内建命令、Shell函数和外部命令。

(1)内建命令就是Shell程序本身包含的命令。这些命令集成在Shell解释器中,例如,几乎所有的Shell解释器中都包含cd内建命令来改变工作目录。部分内建命令的存在是为了改变Shell本身的属性设置,在执行内建命令时,没有进程的创建和消亡;另一部分内建命令则是I/O命令,例如echo命令。

(2)Shell函数是一系列程序代码,以Shell语言写成,它可以像其他命令一样被引用。我们在后面将详细介绍Shell函数。

(3)外部命令是独立于Shell的可执行程序。例如find、grep、echo.sh。命令行Shell在执行外部命令时,会创建一个当前Shell的复制进程来执行。在执行过程中,存在进程的创建和消亡。外部命令的执行过程如下:

①调用POSIX系统fork函数接口,创建一个命令行Shell进程的复制(子进程);

②在子进程的运行环境中,查找外部命令在Linux文件系统中的位置。如果外部命令给出了完全路径,则跳过查找这一步;

③在子进程里,以新程序取代Shell复制并执行(exec),此时父进程进入休眠,等待子进程执行完毕;

④子进程执行完毕后,父进程接着从终端读取下一条命令。过程如图1-1所示。

NOTE:

(1)子进程在创建初期和父进程一模一样,但是子进程不能改变父进程的参数变量。

(2)只有内建命令才能改变命令行Shell的属性设置(环境变量)。

我们回到例1.1。在这个例子中,我们使用cd(内建命令)试图改变工作目录。但是未获成功。为了理解失败的原因,图1-2说明了执行的过程。

图1-1 创建进程

图1-2 echo.sh的执行过程

在我们运行Shell程序的3种方法中,前两种方法的执行过程都可以用图1-2解释。

(1)父进程接收到命令“./echo.sh”或“/bin/sh echo.sh”,发现不是内建命令,于是创建了一个和自己一模一样的Shell进程来执行这个外部命令。

(2)这个Shell子进程用/bin/sh取代自己,sh进程设置自己运行环境变量,其中包括$PWD变量(标识当前工作目录)。

(3)sh进程依次执行内建命令cd和echo,在此过程中,sh进程(子进程)的环境变量$PWD被cd命令改变,注意:父进程的环境变量并没有改变。

(4)sh子进程执行完毕,消亡。一直在等待的父进程醒来继续接收命令。

这样,例1.1中cd命令失效的原因就可以理解了!聪明的你,一定猜到了例1.3中使用source命令为什么可以改变命令行Shell的环境变量了吧!

这也是在例1.1中目录没有改变的原因:父进程的当前目录(环境变量)无法被子进程改变!

NOTE:

使用source执行Shell脚本时,不会创建子进程,而是在父进程中直接执行!

source

语法:

source file

. file

描述:

使用Shell进程本身执行脚本文件。souce命令也被称为“点命令”,通常用于重新执行刚修改的初始化文件。使之立即生效。

行为模式:

和其他运行脚本不同的是,source命令影响Shell进程本身。在脚本执行过程中,并没有进程创建和消亡。

警告:

当需要在程序中修改当前Shell本身环境变量时,使用source命令。