程序是如何被执行的?


  当操作系统启动之后,就会等待用户操作。那么用户是如何与操作系统沟通呢?如何触发程序执行呢? 通常,与操作系统交互,有两种实现方式:CLI(command line interface); CUI(graphic user interface)

  比如,当我们启动 windows系统,我们想看电影,听音乐,那么我们必须去触发程序的快捷方式,以启动程序。

  同理,在Linux中用户要与kernel交互,必须通过一个shell的程序。Linux命令的执行必须依赖于Shell命令解释器。Shell实际上是在Linux系统中运行的一种特殊程序,它位于操作系统内核与用户之间,负责接受用户输入的命令并进行解释,将需要执行的操作传递给系统内核执行,Shell在用户和内核之间充当了一个“翻译官”的角色。当用户登陆到Linux系统时,会自动加载一个Shell程序,以便给用户提供可以输入命令的操作环境。


Linux内置命令和外部命令


  Bash是很多Linux系统中默认使用的Shell程序,文件位于/bin/bash。对于Shell来说,可以识别三种基本命令:内建命令、Shell函数以及外部命令。

  • 内部命令:指的是集成于Shell解释器程序(如Bash)内部的一些特殊指令,也成为内建(Built-IN)指令。内部命令属于Shell的一部分,所以并没有单独对应的系统文件,只要Shell解释器被运行,内部指令也就自动载入内存并驻留在系统内存中,用户可以直接使用。内部命令无需从硬盘中重新读取文件,而且解析内部命令shell不需要创建子进程,因此执行效率更高。常见的builtin命令有: cd, echo, history 等。

   有些命令是由于其必要性才内建的,例如cd用来改变目录,read会将来自用户(和文件)的输入数据传给Shell外壳。

   另一种内建命令的存在则是为了效率,其中最典型的就是test命令,编写脚本时经常会用到它。

   另外还有I/O命令,例如echo于printf.

  • shell函数:Shell函数是功能健全的一系列程序代码,以Shell语言写成,它们可以像命令那样引用。

  • 外部命令:指的是Linux系统中能够完成特定功能的脚本文件或二进制程序,每个外部命令对应了系统中的一个文件,是属于Shell解释器程序之外的命令,所以称为外部命令。Linux系统必须知道外部命令对应的文件位置,才能够由Shell加载并执行。

    二进制可执行文件,需要由磁盘装入内存执行。会派生新的进程,shell解释程序会调用fork自身的一个拷贝,然后用exec系列函数来执行外部命令,然后外部命令就取代了先前fork的子shell。 


外部命令就是由Shell副本(新的进程)所执行的命令,基本的过程如下:

  1. shell通过fork()建立一个新的进程。此进程即为当前Shell的一个副本。

  2. 在新的进程里,在PATH变量内所列出的目录中,寻找特定的命令。当命令名称包含有斜杠(/)符号时,将略过路径查找步骤。

  3. 在新的进程里,通过exec 系列函数,以所找到的新程序取代执行中的Shell进程并执行。

  4. 子进程退出后,最初的Shell会接着从终端读取下一条命令,和执行脚本里的下一条命令。

  wKiom1UOwsPA05P4AABTBFhZVbQ105.jpg

  Linux系统默认会将存放外部命令、程序的目录(如/bin、/usr/bin、/usr/local/bin等)添加到用户的“搜索路径”中,当使用位于这些目录中的外部命令时,用户不需要指定具体的位置。因此在大多数情况下,不用刻意去分辨内部、外部命令,其使用方法是基本类似的。


Why cd is internal command?

    To keep things simple, I won't go into much details, but it requires a little understanding of Unix processes to understand the answer to this question.
    Whenever a processes is created by BASH, it is executed in a subshell of BASH (child process of current BASH process). The new processes makes changes and outputs (if reqquired) and no property of this subshell is returned to the parent when the process dies. Note that cd command changed the PWD of the shell. If cd were an external command, it would have changed the PWD of the subshell, returning nothing to the parent shell. And hence, the PWD of the shell will never change. All the commands that make changes to the environment of the shell, must be implemented as internal command. we could never achieve what we require by making cd an external command.



使用type可以查看是否是内建命令:
type (不带参数)会显示命令是内建命令还是外部命令
-t :file 外部命令;alias 命令别名;builtin 内置命令
-a :会将命令PATH路径显示出来

[root@skype ~]# type cat
cat is /bin/cat
[root@skype ~]# type cd
cd is a shell builtin
[root@skype ~]# type ls
ls is aliased to `ls --color=auto'

路径与命令查找顺序(通过 type -a 查看)

  1. 以相对/绝对路径执行的命令。

  2. 由alias找到该命令来执行。

  3. 由bash内置的(builtin)命令来执行

  4. 通过 PATH变量制定的顺序找到的第一个命令来执行。


如何执行交互式命令:
  用户在命令行输入命令后,一般情况下Shell会fork并exec该命令,但是Shell的内建命令例外,执行内建命令相当于调用Shell进程中的一个函数,并不创建新的进程.
  比如:cd、alias、umask、exit等命令即是内建命令,凡是用which命令查不到程序文件所在位置的命令都是内建命令,内建命令没有单独的man手册,要在man手册中查看内建命令,应该man bash-builtins,内建命令虽然不创建新的进程,但也会有Exit Status,通常也用0表示成功非零表示失败,虽然内建命令不创建新的进程,但执行结束后也会有一个状态码,也可以用特殊变量$?读出。


获取系统帮助

对于builtin命令:  help command    # 如: help cd

对于外部命令:    command --help    # 如: ls --help

通用方式:     man command


绝对路径与相对路径


  Linux的文件系统采用倒置数结构组织,树状结构的话就会有两种路径表示方式:绝对路径与相对路径。

  • 绝对路径:从 / 目录开始。路径从根目录 / 开始。绝对路径的正确度要比较好,如果写程序(shell scripts)来管理系统的条件下,务必使用绝对路径的写法。

  • 相对路径:不是从 / 开始。相对于当前工作目录的路径。

因此绝对路径和相对路径也非常好区分,如果路径的第一个字符为/,则这是一个绝对路径,如果不是/,则为相对路径

一般来说,绝对路径的正确性更好。强烈建议:在编写 shell 脚本的情况下,务必使用绝对路径的写法。


特殊的目录

.   代表当前目录

..  代表上一层目录

~   代表用户家目录(cd  等价于  cd ~ )

~USERNAME   进入指定用户的家目录

-   代表前一个工作目录

在所有目录下面都会存在 . 和 .. 两个目录,分别表示此层与上层目录。但是根目录下,代表上一层(..)与根目录自己(.)是同一个目录。


Linux知识:为什么要用字符~来表示home目录

Unix风格的操作系统里(包括BSD,GNU/Linux Mac OS X),通常用波浪号“~”来表示当前用户的主目录(home目录):例如,如果当前用户的home目录是/home/bloggsj,那么,输入cd cd ~ cd /home/bloggsj cd $HOME 都是等效的。这种习惯源自于1970年代流行的Lear-SieglerADM-3A终端机,这种机器上波浪号和“home”键(用于把光标移动到最左端)正好在同一个键上。

timg?p_w_picpath&quality=80&size=b9999_10000&s



Shell 命令搜索机制


  想过一个问题没有,为什么我们可以在任何目录执行像 ls, cat 这些命令,而没有指定其绝对或者相对路径呢?却不会提示:command not found 呢?

  这全是PATH的功劳了。PATH is an environmental variable in Linux and other Unix-likeoperating systems that tells the shell which directories to search for executable files (i.e., ready-to-run programs) in response to commands issued by a user. It increases both the convenience and the safety of such operating systems and is widely considered to be the single most important environmental variable.


  注意: 另外,我们讨论的前提是当你键入一个命令时并没有指定该命令的路径, 举例来说就是我们键入的命令是以commandname的形式而不是/path/commandname或./path/commandname的形式来 运行的. 一旦我们指定了命令(或脚本或二进制文件)相对或绝对路径时就谈不上搜索机制了.

  通常,我们在Linux系统终端提示符下键入如ls等命令时,shell是如何找到这个命令的呢? shell下都有哪几类命令呢? 这些命令是如何被加载的呢?
  

一、 Linux命令搜索顺序:

  当我们键入某个命令时, 那么shell会按照alias->keyword->function,->built-in->$PATH的顺序进行搜索, 本着”搜索顺序是从左至右的,先到先得”的原则, 就是说如果有如名为mycmd的命令同时存在于alias和function中的话, 那么肯定会使用alias的mycmd命令(当然, 这不是绝对的, 下面会说到特例).


1) hash命令:

  首先, 我们来看hash这命令(和我上面说的”不是绝对的”有关系了!), hash命令用于记录在当前shell环境下用户曾经键入的命令路径记录缓存表, 主要是为了加快命令搜寻速度. 下面看个例子:

  例:我在shell下键入 ls, find, pwd, ls, echo “Hello, world”, mail及if共7个命令(注意, ls执行2次), 下面是history的结果:

  1 ls
  2 find
  3 pwd
  4 ls
  5 echo “Hello, world”
  6 mail
  7 if

  那么, 现在我执行hash命令, 其显示结果为:  

[ancharn@fc8 var]$ hash
  hits command
  1    /bin/mail
  2    /bin/ls
  1    /usr/bin/find

  不知大家发现了什么没有? 这个hash表左边一列表示该命令在当前shell环境下共被使用了几次, 右边一列表示命令路径. 但是我们发现这个hash缓存中缺少了if,pwd和echo这3个命令, 为什么呢? 我们在这儿要得出一个重要的结论就是: hash不会记录function, built-in命令(其实还包括alias)

  为什么呢? 答案是因为他们没有路径, 即不会存在于某个目录之下, 它们是随shell而加载进而存在于内存中, 所以这样的命令还有必要进行缓存以提高搜索效率吗?!

  但是有人会说, ls不是被hash记录下来了吗? 没错, 你的观察很细致, 通常ls在bash中是一个alias, 那么, 在这儿我们先下一个结论: (2) alias中若定义的是包含了路径的别名命令则不会被记录到hash中, 只有没有指定路径的alias才会被记录到hash中. 情况例子:

  这是我当前shell(bash)环境下的ls别名的定义

[ancharn@fc8 var]$ alias ls
alias ls=’ls –color=auto’

  (注意:后面的”ls –color=auto”没有指定如/bin/ls这样的路径)

  所以, 正如你看到的, 上面我键入了2次ls命令(是ls –color=auto的别名), 那么在hash中能够看到被记录; 下面看个write命令的例子:

  [ancharn@fc8 //]$ alias write
  -bash: alias: write: not found
  
  [ancharn@fc8 //]$ write
  usage: write user [tty]
  [ancharn@fc8 //]$ hash  
  hits command
  1 /usr/bin/write
  1 /bin/mail
  2 /bin/ls
  1 /usr/bin/find

  write 这个命令没有alias, 也就是说当执行write命令时其实找到的是PATH变量中的/usr/bin/write这个二进制文件来执行的, 这时hash记录了write的路径并被引用了1次, 然后我定义write别名就是write本身, 但是指定具体路径是/usr/bin/write:

  [ancharn@fc8 //]$ alias write=’/usr/bin/write’
  [ancharn@fc8 //]$ alias write
  alias write=’/usr/bin/write’
  [ancharn@fc8 //]$ write
  usage: write user [tty]
  [ancharn@fc8 //]$ hash
  hits command
  1 /usr/bin/write
  1 /bin/mail
  2 /bin/ls
  1 /usr/bin/find

  请看, hash表中的write的hits数还是1次; 这里要注意的是当我们定义了write的alias后(指定路径), PATH就不会被搜到了, 为什么呢? 很简单, 因为write的alias中已经指明了它的具体路径了!

  接着unalias掉write重新定义write别名:

  [ancharn@fc8 //]$ unalias write
  [ancharn@fc8 //]$ alias write
  -bash: alias: write: not found
  [ancharn@fc8 //]$ alias write=’write’
  [ancharn@fc8 //]$ alias write
  alias write=’write’
  [ancharn@fc8 //]$ write
  usage: write user [tty]
  [ancharn@fc8 //]$ hash
  hits command
  2 /usr/bin/write
  1 /bin/mail
  2 /bin/ls
  1 /usr/bin/find

  这次, 我们没有指定write别名中的路径, 当我们定义好write的别名后去执行write时, hash表中就会增加一次hits.这里要注意的是当我们定义了write的alias后(不指定路径, 请和上面的例子比较下), PATH就会被搜到了, 所以hash的hits增加了. 请大家切记alias中若定义的是包含了路径的别名命令则不会被记录到hash中, 只有没有指定路径的alias才会被记录到hash中这条结论.

  另外, hash因为是built-in命令, 所以用help hash来查看帮助. 常用的有hash -r用于清空hash表, hash -d name用于delete某个command. 如:

    [ancharn@fc8 //]$ hash
  hits command
  3 /usr/bin/write
  1 /bin/mail
  2 /bin/ls
  1 /usr/bin/find
  删除具体的:
  [ancharn@fc8 //]$ hash -d ls
  [ancharn@fc8 //]$ hash
  hits command
  3 /usr/bin/write
  1 /bin/mail
  1 /usr/bin/find
  
  清空hash:
  [ancharn@fc8 //]$ hash -r
  [ancharn@fc8 //]$ hash
  hash: hash table empty

  2) set +-h:
  
  set 命令大家应该很熟悉, 我们在这里主要说的是set +-h的作用: help set可以看到”-h Remember the location of commands as they are looked up.” 中文意思就是记忆命令的路径以便于查询. 当我们键入set +h后再运行hash:
  
  [ancharn@fc8 //]$ set +h
  [ancharn@fc8 //]$ hash
  -bash: hash: hashing disabled
 
  也就是说”set +h”用于禁用hash而”set -h”用于启用hash.

 

  小结: 我们都知道了shell在搜索命令时的顺序是alias->keyword->function,->built-in->$PATH, 那么其中还有2点需要注意的就是 (1) hash不会记录function, built-in命令(其实还包括alias), (2) alias中若定义的是包含了路径的别名命令则不会被记录到hash中, 只有没有指定路径的alias才会被记录到hash中. 另外, (3) 不要忘记, 我们讨论的前提是a) 受限于具体的shell种类b)且只在当前shell环境有效.切记!!!

  到这里, 请大家来思考一个问题:

  请看下面的执行情况:

  [ancharn@fc8 var]$ function gcc { echo “just a test for gcc”; }
  [ancharn@fc8 var]$ alias gcc=’gcc’

  [ancharn@fc8 var]$ gcc
  just a test for gcc

  [ancharn@fc8 var]$ /usr/bin/gcc
  gcc: no input files

  [ancharn@fc8 var]$ alias gcc=’/usr/bin/gcc’
  [ancharn@fc8 var]$ gcc
  gcc: no input files

  [ancharn@fc8 var]$

  为什么定义了gcc这个funtion后, 两次定义gcc的alias时指定不指定具体的/usr/bin/gcc路径时, 执行gcc这个命令的反应不同呢? 按照alias->keyword->function,->built-in->$PATH 这个顺序来看, 应该执行alias的gcc啊?! 请思考!
当然, 别着急, 后面我会给出答案. 但是, 请您思考下!

  四, 命令举例:

  * alias(别名):
  alias 命令通常被设定在文件~/.bashrc和/etc/bashrc中,~/.bashrc通常用于用户自己的环境,而/etc/bashrc用于全局定义 (即对所有用户生效,当然,只对用户shell是bash生效). 具体的这两个文件的关系及如何加载在后面有介绍.

  * Shell keyword(shell关键字):
  诸如if,while,until,case,for这些命令.

  * Function(函数):
  
  举例:
  
  定义个名为pwd的函数, 其功能是简单地显示”my function pwd”这句话
  function pwd { echo “my function pwd”; }
  定义好了之后可以用set或type -a pwd来查看,取消则用unset pwd即可。

  * Shell built-in command(shell内置命令):
  命令enable可以查看所有当前shell环境下的内置命令; 或者用man cd(任何一个内置命令均可)查看到的manpage的上部列出了全部的内置命令. 

  * PATH variable
  该变量定义在文件/etc/profile, /etc/profile.d/*.sh(POSIX), ~/.bash_profile(Bash)中.

  其加载顺序是: 先/etc/profile (invoke /etc/profile.d/*.sh), 然后是~/.bash_profile, 再由~/.bash_profile调用执行 ~/.bashrc, 然后由~/.bashrc去调用执行 ~/.bashrc, ~/.bashrc再调用执行文件/etc/bashrc.
  
  1) 为了查看具体的加载顺序, 你可以在四个文件中的头部和尾部分别添加两句话, 例如:

  [ancharn@fc8 ~]$ cat ~/.bashrc
  echo “start of ~/.bashrc”
  if [ -f /etc/bashrc ] ; then
  . /etc/bashrc
  fi
  alias ll=’ls -l’
  alias cp=’cp -i’
  alias mv=’mv -i’
  alias rm=’rm -i’
  ……

  echo “end of ~/.bashrc”
  其它的文件一样添加, 这样当你用某个用户登录系统时就会看到如下的显示, 诸如:
  start of /etc/profile
  end of /etc/profile
  start of ~/.bash_profile
  start of ~/.bashrc
  start of /etc/bashrc
  end of /etc/bashrc
  end of ~/.bashrc
  end of ~/.bash_profile

  从上面的显示你能够清晰的看到每个文件的加载顺序及相互调用执行关系(注意查看start和end).

  2) PATH变量和hash的关系
  这里, 我们来看一个例子:

  [ancharn@fc8 ~]$ echo $PATH
  /usr/kerberos/bin:/usr/local/bin:/bin:/usr/bin:/home/ancharn/bin
 
  我首先在/home/ancharn/bin目录下写一个名为test.sh的脚本,内容如下:
  [ancharn@fc8 bin]$ cat /home/ancharn/bin/test.sh
  
  #!/bin/sh
  # just test for PATH and hash
  echo “This is my 1st shell script in /home/ancharn/bin directory.”

  # end
  
  [ancharn@fc8 bin]$
  那么, 执行test.sh这个脚本如下:
  [ancharn@fc8 /]$ echo $PATH
  /usr/kerberos/bin:/usr/local/bin:/bin:/usr/bin:/home/ancharn/bin
  [ancharn@fc8 /]$ test.sh
  This is my 1st shell script in /home/ancharn/bin directory.
  [ancharn@fc8 /]$ hash
  hits command
  1 /home/ancharn/bin/test.sh
  接着,在/usr/bin目录下建立一个同test.sh名的文件, 内容如下:

  [ancharn@fc8 /]$ cat /usr/bin/test.sh
  #!/bin/sh
  # just test for PATH and hash
  echo “This is my 2nd shell script in /usr/bin directory.”

  # end
  继续执行test.sh脚本:

  [ancharn@fc8 /]$ test.sh
  This is my 1st shell script in /home/ancharn/bin directory.
  [ancharn@fc8 /]$ hash
  hits command
  2 /home/ancharn/bin/test.sh
 
  说明什么呢?

  如果按照PATH的顺序即/usr/kerberos/bin:/usr/local/bin:/bin:/usr/bin:/home/ancharn /bin, 会先找/usr/bin然后再找/home/ancharn/bin, 注意, 这个前提是hash表中没有该命令的记录, 因此我们看到/usr/bin/test.sh脚本并没有被执行, 因为在执行test.sh前, shell去hash表中查看了缓存, 进而继续执行了/home/ancharn/bin/test.sh脚本, 所以我们看到hits数增加了一次, 而/usr/bin/test.sh不会被执行.

  现在, 我们清空hash, 重新执行test.sh脚本:
  
  [ancharn@fc8 /]$ hash -r
  [ancharn@fc8 /]$ hash
  hash: hash table empty
  [ancharn@fc8 /]$ test.sh
  This is my 2nd shell script in /usr/bin directory.
  [ancharn@fc8 /]$ hash
  hits command
  1 /usr/bin/test.sh

  现在正常了. 所以一定要注意PATH和hash的这层关系.

  注意: su, su-, bash –login, bash –norc这些命令的不同就在于是否执行了login-shell, 大家可以su和su -后, 再去运行echo $PATH看看有何不同.

  好了, 回答上面的思考题, 其核心在于alias如果定义的如alias gcc=’gcc’时, 其实alias->keyword->function,->built-in->$PATH 这个顺序并没有变, 但是要知道alias gcc=’gcc’这种没有指定路径的alias会在找到gcc这个alias后, 再去找到后面指定的’gcc’, 怎么找? 当然到下一个了, 就是keyword->function….这个顺序了. 而如果是alias gcc=’/usr/bin/gcc’这样的指定具体路径的定义alias的话, 那么alias执行后就直接找到了那个具体文件而跳过了后面的所有搜索(即keyword->function,->built-in->$PATH). 请大家留意.

  最后, 大家在做实验验证的时候可以分成2类验证, 因为一个命令不可能既属于keyword又属于built-in, 所以你可以:
  
  1) 选择一个keyword如while, 定义一个while的alias,function,然后编写一个shell脚本名为while存放于PATH变量的某个路径下;

  2) 选择一个built-in命令如pwd来验证.