1.2 如何运行程序
运行Linux程序有3种方法。
(1)使文件具有可执行权限,直接运行文件。
(2)直接调用命令解释器执行程序。
(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程序:
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命令。