程序执行的一刹那
当我们在 Linux 下的命令行输入一个命令之后,这背后发生了什么?
用户使用计算机有两种常见的方式,一种是图形化的接口(GUI),另外一种则是命令行接口(CLI)。对于图形化的接口,用户点击某个图标就可启动后台的某个程序;对于命令行的接口,用户键入某个程序的名字就可启动某个程序。这两者的基本过程是类似的,都需要查找程序文件在磁盘上的位置,加载到内存并通过不同的解释器进行解析和运行。下面以命令行为例来介绍程序执行一刹那发生的一些事情。
首先来介绍什么是命令行?命令行就是
Command Line
,很直观的概念就是系统启动后的那个黑屏幕:有一个提示符,并有光标在闪烁的那样一个终端,一般情况下可以用 CTRL+ALT+F1-6
切换到不同的终端;在 GUI 界面下也会有一些伪终端,看上去和系统启动时的那个终端没有什么区别,也会有一个提示符,并有一个光标在闪烁。就提示符和响应用户的键盘输入而言,它们两者在功能上是一样的,实际上它们就是同一个东西,用下面的命令就可以把它们打印出来。$ echo $SHELL # 打印当前SHELL,当前运行的命令行接口程序
/bin/bash
$ echo $$ # 该程序对应进程ID,$$是个特殊的环境变量,它存放了当前进程ID
1481
$ ps -C bash # 通过PS命令查看
PID TTY TIME CMD
1481 pts/0 00:00:00 bash
从上面的操作结果可以看出,当前命令行接口实际上是一个程序,那就是
/bin/bash
,它是一个实实在在的程序,它打印提示符,接受用户输入的命令,分析命令序列并执行然后返回结果。不过 /bin/bash
仅仅是当前使用的命令行程序之一,还有很多具有类似功能的程序,比如 /bin/ash
, /bin/dash
等。不过这里主要来讨论 bash
,讨论它自己是怎么启动的,它怎么样处理用户输入的命令等后台细节?先通过
CTRL+ALT+F1
切换到一个普通终端下面,一般情况下看到的是 "XXX login: " 提示输入用户名,接着是提示输入密码,然后呢?就直接登录到了我们的命令行接口。实际上正是你输入正确的密码后,那个程序才把 /bin/bash
给启动了。那是什么东西提示 "XXX login:" 的呢?正是 /bin/login
程序,那 /bin/login
程序怎么知道要启动 /bin/bash
,而不是其他的 /bin/dash
呢?/bin/login
程序实际上会检查我们的 /etc/passwd
文件,在这个文件里头包含了用户名、密码和该用户的登录 Shell。密码和用户名匹配用户的登录,而登录 Shell 则作为用户登录后的命令行程序。看看 /etc/passwd
中典型的这么一行:$ cat /etc/passwd | grep falcon
falcon:x:1000:1000:falcon,,,:/home/falcon:/bin/bash
这个是我用的帐号的相关信息哦,看到最后一行没?
/bin/bash
,这正是我登录用的命令行解释程序。至于密码呢,看到那个 x
没?这个 x
说明我的密码被保存在另外一个文件里头 /etc/shadow
,而且密码是经过加密的。至于这两个文件的更多细节,看手册吧。我们怎么知道刚好是
/bin/login
打印了 "XXX login" 呢?现在回顾一下很早以前学习的那个 strace
命令。我们可以用 strace
命令来跟踪 /bin/login
程序的执行。跟上面一样,切换到一个普通终端,并切换到 Root 用户,用下面的命令:
$ strace -f -o strace.out /bin/login
退出以后就可以打开
strace.out
文件,看看到底执行了哪些文件,读取了哪些文件。从中可以看到正是 /bin/login
程序用 execve
调用了 /bin/bash
命令。通过后面的演示,可以发现 /bin/login
只是在子进程里头用 execve
调用了 /bin/bash
,因为在启动 /bin/bash
后,可以看到 /bin/login
并没有退出。那
/bin/login
又是怎么起来的呢?下面再来看一个演示。先在一个可以登陆的终端下执行下面的命令。
$ getty 38400 tty8 linux
getty
命令停留在那里,貌似等待用户的什么操作,现在切回到第 8 个终端,是不是看到有 "XXX login:" 的提示了。输入用户名并登录,之后退出,回到第一个终端,发现 getty
命令已经退出。类似地,也可以用
strace
命令来跟踪 getty
的执行过程。在第一个终端下切换到 Root 用户。执行如下命令:$ strace -f -o strace.out getty 38400 tty8 linux
同样在
strace.out
命令中可以找到该命令的相关启动细节。比如,可以看到正是 getty
程序用 execve
系统调用执行了 /bin/login
程序。这个地方,getty
是在自己的主进程里头直接执行了 /bin/login
,这样 /bin/login
将把 getty
的进程空间替换掉。这里涉及到一个非常重要的东西:
/sbin/init
,通过 man init
命令可以查看到该命令的作用,它可是“万物之王”(init is the parent of all processes on the system)哦。它是 Linux 系统默认启动的第一个程序,负责进行 Linux 系统的一些初始化工作,而这些初始化工作的配置则是通过 /etc/inittab
来做的。那么来看看 /etc/inittab
的一个简单的例子吧,可以通过 man inittab
查看相关帮助。需要注意的是,在较新版本的 Ubuntu 和 Fedora 等发行版中,一些新的
init
程序,比如 upstart
和 systemd
被开发出来用于取代 System V init
,它们可能放弃了对 /etc/inittab
的使用,例如 upstart
会读取 /etc/init/
下的配置,比如 /etc/init/tty1.conf
,但是,基本的配置思路还是类似 /etc/inittab
,对于 upstart
的 init
配置,这里不做介绍,请通过 man 5 init
查看帮助。配置文件
/etc/inittab
的语法非常简单,就是下面一行的重复,id:runlevels:action:process
id
就是一个唯一的编号,不用管它,一个名字而言,无关紧要。runlevels
是运行级别,这个还是比较重要的,理解运行级别的概念很有必要,它可以有如下的取值:0 is halt.1 is single-user.2-5 are multi-user.6 is reboot.不过,真正在配置文件里头用的是1-5
了,而0
和6
非常特别,除了用它作为init
命令的参数关机和重启外,似乎没有哪个“傻瓜”把它写在系统的配置文件里头,让系统启动以后就关机或者重启。1
代表单用户,而2-5
则代表多用户。对于2-5
可能有不同的解释,比如在 Slackware 12.0 上,2,3,5
被用来作为多用户模式,但是默认不启动 X windows (GUI接口),而4
则作为启动 X windows 的运行级别。action
是动 作,它也有很多选择,我们关心几个常用的initdefault
:用来指定系统启动后进入的运行级别,通常在/etc/inittab
的第一条配置,如:id:3:initdefault:这个说明默认运行级别是 3,即多用户模式,但是不启动 X window 的那种。sysinit
:指定那些在系统启动时将被执行的程序,例如:si:S:sysinit:/etc/rc.d/rc.S在man inittab
中提到,对于sysinit
,boot
等动作,runlevels
选项是不用管的,所以可以很容易解读这条配置:它的意思是系统启动时将默认执行/etc/rc.d/rc.S
文件,在这个文件里可直接或者间接地执行想让系统启动时执行的任何程序,完成系统的初始化。wait
:当进入某个特别的运行级别时,指定的程序将被执行一次,init
将等到它执行完成,例如:rc:2345:wait:/etc/rc.d/rc.M这个说明无论是进入运行级别 2,3,4,5 中哪一个,/etc/rc.d/rc.M
将被执行一次,并且有init
等待它执行完成。ctrlaltdel
,当init
程序接收到SIGINT
信号时,某个指定的程序将被执行,我们通常通过按下CTRL+ALT+DEL
,这个默认情况下将给init
发送一个SIGINT
信号。如果我们想在按下这几个键时,系统重启,那么可以在/etc/inittab
中写入:ca::ctrlaltdel:/sbin/shutdown -t5 -r nowrespawn
:这个指定的进程将被重启,任何时候当它退出时。这意味着没有办法结束它,除非init
自己结束了。例如:c1:1235:respawn:/sbin/agetty 38400 tty1 linux这一行的意思非常简单,就是系统运行在级别 1,2,3,5 时,将默认执行/sbin/agetty
程序(这个类似于上面提到的getty
程序),这个程序非常有意思,就是无论什么时候它退出,init
将再次启动它。这个有几个比较有意思的问题:- 在 Slackware 12.0 下,当默认运行级别为 4 时,只有第 6 个终端可以用。原因是什么呢?因为类似上面的配置,因为那里只有
1235
,而没有4
,这意味着当系统运行在第4
级别时,其他终端下的/sbin/agetty
没有启动。所以,如果想让其他终端都可以用,把1235
修改为12345
即可。 - 另外一个有趣的问题就是:正是
init
程序在读取这个配置行以后启动了/sbin/agetty
,这就是/sbin/agetty
的秘密。 - 还有一个问题:无论退出哪个终端,那个 "XXX login:" 总是会被打印,原因是
respawn
动作有趣的性质,因为它告诉init
,无论/sbin/agetty
什么时候退出,重新把它启动起来,那跟 "XXX login:" 有什么关系呢?从前面的内容,我们发现正是/sbin/getty
(同agetty
)启动了/bin/login
,而/bin/login
又启动了/bin/bash
,即我们的命令行程序。
而
init
程序作为“万物之王”,它是所有进程的“父”(也可能是祖父……)进程,那意味着其他进程最多只能是它的儿子进程。而这个子进程是怎么创建的,fork
调用,而不是之前提到的 execve
调用。前者创建一个子进程,后者则会覆盖当前进程。因为我们发现 /sbin/getty
运行时,init
并没有退出,因此可以判断是 fork
调用创建一个子进程后,才通过 execve
执行了 /sbin/getty
。因此,可以总结出这么一个调用过程:
fork execve execve fork execve
init --> init --> /sbin/getty --> /bin/login --> /bin/login --> /bin/bash
这里的
execve
调用以后,后者将直接替换前者,因此当键入 exit
退出 /bin/bash
以后,也就相当于 /sbin/getty
都已经结束了,因此最前面的 init
程序判断 /sbin/getty
退出了,又会创建一个子进程把 /sbin/getty
启动,进而又启动了 /bin/login
,又看到了那个 "XXX login:"。通过
ps
和 pstree
命令看看实际情况是不是这样,前者打印出进程的信息,后者则打印出调用关系。$ ps -ef | egrep "/sbin/init|/sbin/getty|bash|/bin/login"
root 1 0 0 21:43 ? 00:00:01 /sbin/init
root 3957 1 0 21:43 tty4 00:00:00 /sbin/getty 38400 tty4
root 3958 1 0 21:43 tty5 00:00:00 /sbin/getty 38400 tty5
root 3963 1 0 21:43 tty3 00:00:00 /sbin/getty 38400 tty3
root 3965 1 0 21:43 tty6 00:00:00 /sbin/getty 38400 tty6
root 7023 1 0 22:48 tty1 00:00:00 /sbin/getty 38400 tty1
root 7081 1 0 22:51 tty2 00:00:00 /bin/login --
falcon 7092 7081 0 22:52 tty2 00:00:00 -bash
上面的结果已经过滤了一些不相干的数据。从上面的结果可以看到,除了
tty2
被替换成 /bin/login
外,其他终端都运行着 /sbin/getty
,说明终端 2 上的进程是 /bin/login
,它已经把 /sbin/getty
替换掉,另外,我们看到 -bash
进程的父进程是 7081
刚好是 /bin/login
程序,这说明 /bin/login
启动了 -bash
,但是它并没有替换掉 /bin/login
,而是成为了 /bin/login
的子进程,这说明 /bin/login
通过 fork
创建了一个子进程并通过 execve
执行了 -bash
(后者通过 strace
跟踪到)。而 init
呢,其进程 ID 是 1,是 /sbin/getty
和 /bin/login
的父进程,说明 init
启动或者间接启动了它们。下面通过 pstree
来查看调用树,可以更清晰地看出上述关系。$ pstree | egrep "init|getty|\-bash|login"
init-+-5*[getty]
|-login---bash
|-xfce4-terminal-+-bash-+-grep
结果显示
init
是 5 个 getty
程序,login
程序和 xfce4-terminal
的父进程,而后两者则是 bash
的父进程,另外我们执行的 grep
命令则在 bash
上运行,是 bash
的子进程,这个将是我们后面关心的问题。从上面的结果发现,
init
作为所有进程的父进程,它的父进程 ID 饶有兴趣的是 0,它是怎么被启动的呢?谁才是真正的“造物主”?如果用过
Lilo
或者 Grub
这些操作系统引导程序,可能会用到 Linux 内核的一个启动参数 init
,当忘记密码时,可能会把这个参数设置成 /bin/bash
,让系统直接进入命令行,而无须输入帐号和密码,这样就可以方便地把登录密码修改掉。这个
init
参数是个什么东西呢?通过 man bootparam
会发现它的秘密,init
参数正好指定了内核启动后要启动的第一个程序,而如果没有指定该参数,内核将依次查找 /sbin/init
,/etc/init
,/bin/init
,/bin/sh