用户空间初始化
linux内核本身只是任何嵌入式linux系统中的一个很小的组成部分,当内核完成其自身初始化后,它必须挂载一个根文件系统,并执行一组由开发人员定义的初始化历程。本章继续研究继内核初始化之后的系统初始化
根文件系统
Linux和很多其他高级操作系统一样,需要一个根文件系统以充分发挥它的优势。根文件系统是指挂载于文件系统层次结构根部的文件系统,简单的表示为/。即使是一个很小的嵌入式linux系统都会在文件系统层次结构的不同位置上挂载几个文件系统。这其中包括虚拟的和实际的文件系统,比如/proc和/sys。proc文件系统是一个虚拟文件系统它挂载于根文件系统的/proc位置下。根文件系统是linux内核挂载的第一个文件系统,挂载的位置是文件系统层次结构的顶端。linux要求根文件系统中包含应用程序和工具软件,通过他们来引导系统、初始化系统服务(比如网络和系统控制台)、加载设备驱动程序和挂载额外的文件系统。
FHS:文件系统层次结构标准
很多linux发行版中的目录布局和FHS标准中所描述的布局非常匹配。FHS标准允许你的应用软件预先知道某些系统成员(包括文件和目录)在文件系统中的位置。
文件系统布局:
考虑到存储空间有限,很多嵌入式系统的开发人员会在一个可引导设备(比如闪存)上创建一个很小的文件系统。之后再从另一个设备(可能是硬盘或NFS服务器)上挂载一个较大的文件系统。当后面研究初始RAM磁盘时,就会看到这样的例子。
bin:二进制可执行文件,系统的所有用户都可使用
dev:设备节点
etc:本地系统配置文件
home:用户帐号文件
lib:系统程序库,比如C程序库和很多其他程序库
sbin:二进制可执行程序,一般留给系统的超级用户使用
tmp:临时文件
usr:次级文件系统层次结构,用于存放应用程序,一般是只读的
var:包含一些易变的文件,比如系统日志和临时配置文件
还会包含像/proc和/mnt,/proc文件系统是一个特殊的文件系统,其中包含了系统信息。/mnt在文件系统层次结构中预留了一个位置用于挂载用户设备和文件系统。
最小的文件系统
--bin
--busybox
--sh->busybox
--dev
--console
--etc
--init.d
--rcs
--lib
--ld-2.3.3.so
--ld-linux.so.2->ld-2.3.2.so
--libc-2.3.2.so
--libc.so.6->libc-2.3.2.so
这个文件系统的配置使用了busybox,这是一个专门针对嵌入式系统的常用工具软件,busybox是一个独立的二进制可执行程序,但支持很多常用的linux命令行实用程序,busybox非常适合嵌入式系统。
这个小型根文件系统可以引导内核并为用户提供功能齐全的命令,用户可以在串行端口终端的命令提示符后输入任何busybox支持的命令。
/bin这个目录包含可执行程序busybox和一个名为sh的软连接,指向busybox。/dev中的文件是一个设备节点,我们需要用它来打开一个控制台设备,用于系统的输入和输出。/etc/init.d目录中的rcS文件是默认的初始化脚本,它会在系统启动时由busybox进行处理,当rcS文件不存在时,busybox会发出警告信息,包含rcS文件就不会有这些告警。/lib目录中包涵了一些必须的文件,他们时两个程序库:glibc和Linux动态加载器。glibc包含标准C程序库中的函数比如printf和其他大多数的应用程序都依赖的常用函数。linux动态加载其负责将二进制程序加载到内存中,并且如果应用程序引用了共享库中的函数,他还需要执行动态链接。目录中还有这两个文件的软连接。这些连接为程序库本身提供了版本保护和向后兼容,在所有的Linux系统中都可以找到。
这个简单的根文件系统构成了一个功能完备的系统。这个小型根文件系统在ARM开发板上大小是1.7M。其中80%左右的空间是C程序库占用的。如果精减嵌入式系统可以考虑使用library optimizer tool。
嵌入式根文件系统带来的挑战
删除一个根文件系统的内容,使它的大小能够适应给定的存储空间是一项艰巨的任务。很多软件包或子系统都包含数十个或甚至上百个文件。除了应用程序本身以外,很多软件包还包含配件、程序库、配置工具、图标、文档文件、国际化相关的语言文件、数据库文件等。流行的嵌入式Linux发行版中就有基本的Apache软件包,它包含了254个不同的文件,这些文件不仅仅是简单的复制到文件系统的同一目录中,他们会分散在文件系统中的几个不同位置,这样apache应用程序才能正常工作,而不需要对他做修改。
linux发行版厂商花费大量的工程资源,仅仅完成以下:将大量程序、程序库、工具、实用软件和应用程序集中打包在一起制作成了一个linux发行版。构建根文件系统也必然会设计若干发布板制作工作,只是规模小一些。
试错法
填充根文件系统的唯一方法就是试错法,在根文件系统中安装软件包可以使用像红帽软件包管理器(rpm)这样的工具,rpm能够合理解析软件包之间的依赖关系,但它很复杂,学起来也不容易。rpm并不便于创建小型文件系统。如果需要在某个软件包的安装文件中去除那些不必要的文件,比如文档和用不到的工具rpm无能为力了。
自动化文件系统构建工具
嵌入式Linux发行板的领先厂商推出了一些功能强大的工具,用于在闪存或其他设备上自动构建根文件系统。这些工具一般都有图形界面,开发人员可以按照应用程序或功能选择需要的文件。这些工具也能从软件包中去除那些不需要的文件,比如文档。有很多还需要你逐一选择需要的文件。这些工具可以生成各种格式的文件系统,以便于在后期所选设备上安装。开源的自动化软件系统构建工具比较有名的是bitbake和buildroot。
内核的最后一些引导步骤
.../init/main.c --这段代码来自函数init_post,而init_post函数由kernel_init()调用
...if(execute_command)
{
run_init_process(execute_command);
printk(KERN_WARNING "Failed to execute %s.Attempting"
"defaults...\n,execucute..,...,exuecucute)
}
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
}
在生成kernel_init()线程,并调用各个初始化函数之后,内核开始执行引导过程的最后一些步骤。这包括释放初始化函数和数据所占用的内存,打开系统控制台设备,并启用第一个用户空间进程。函数run_init_process()很简短,实质上调用了函数exev()--一个内核系统调有,其行为非常有趣。如果在执行过程中没有遇到错误条件,函数execv()永远都不会返回。调用线程在执行时所占用的内存空间会被覆盖,替换成被调有程序的内存镜像。最常见的系统配置一般会生成/sbin/init作为用户空间的初始化进程。实际上被调用的程序直接取代了调用线程,包括继承其进程ID。
嵌入式开发人员可以选择定制的用户空间初始化程序。这就是前面代码片段中的条件语句的意图。如果execute_command非空,它会指向一个运行在用户空间的字符串而这个字符串中包含了一个定制的、有用户提供的命令。开发人员在内核命令行中指定这个命令,并且它会由我们前面所研究的__setup宏进行设置。
initcall_debug init=/sbin/myinit console=ttyS1 ,115200, root=/dev/hda1
这个内核命令行指示内核所显示所有的初始化函数调用,配置初始化的控制台设备为/dev/ttyS1,其数据速率为115kbit/s,并执行一个定制的、名为myinit的用户空间初始化进程,这个程序位于根文件系统的/sbin目录中.他还知道内核从设备/dev/hda1挂载其根文件系统,这个设备是第一个IDE硬盘。
在linux内核的开发过程中这个初始化流程基本结构在很长时间都没有改变过。
这里的关键因素是:内核认为这些程序都位于一个根文件系统中,而且这个文件系统的结构和最小的根文件系统的结构的内容是相似的。因此,我们必须至少满足内核的需求,init进程才能正常执行。
上述代码中意味着至少有一个run_init_process()函数调用必须成功。我们会看到内核会按代码中的顺序依次执行四个程序,这四个程序如果都没有成功执行,引导中的内核会执行可怕的panic()函数,继而崩溃。文件.../init/main.c中的这个代码片段只会执行一次。如果没有成功的话内核所做的就只有抱怨而终止了,而这正是通过panic()来完成的。
第一个用户空间程序
在大多数linux系统中,/sbin/init这个程序是由内核在引导时执行的。在init_post中首先尝试执行它。实际上,它会称为第一个运行的用户空间程序。内核的执行次序如下:
1.挂载根文件系统
2.执行第一个用户空间程序,在这里,就是/sbin/init。
如果没有在run_init_process()相应的目录下存放init可执行文件。则执行一个用户空间进程的努力就会失败。在最小的文件系统中,bin目录下有一个名为sh的执行busybox的软连接。这个软连接的作用是:使busybox成为内核执行的第一个用户进程,同时也满足了用户空间对shell可执行程序的普遍需求。
解决依赖关系
将一个可执行程序放入文件系统中,必须同时满足它的依赖关系。比如,仅仅将文件系统中包含一个像init这样的可执行文件是不够的,不能指望就这样就可以完成系统的引导。大多数应用程序有两类依赖关系:1.动态连接的应用程序对程序库的依赖,这种应用程序中包含未解决的引用,这需要由程序库提供;2.应用程序可能需要外部配置文件或数据文件。我们可以使用工具来确定前一种依赖关系,但要想知道后一种依赖关系则至少需要对相关的应用程序有一个基本的理解。
举例子,init就是一个动态连接的可执行程序。为了运行init,我们必须满足它对程序库的依赖,有一专门为此开发的工具:ldd。为了找出某个应用所依赖的程序库,只需要对它运行一下交叉版本的ldd就可以了:
$ppc-4xx-ldd init
libc.so.6=>/opt/eldk/ppc_4xxFP/lib/libc.so.6
ld.so.1=>/opt/eldk/ppc_4xxFP/lib/ld.so.1
$
从这个ldd的输出中,我们可以看到power架构的init可执行程序依赖两个程序库-标准C库和linux动态加载器。
为了满足应用程序的第二种依赖关系,即它可能需要的配置文件和数据文件,唯一的办法是了解这个子系统是如何工作的。举例来说,init期望从/etc目录的inittab数据文件中读取其进行配置。除非你所使用的工具中内置了这种信息,否则你必须自己提供这些信息。
定制的初始进程
系统用户可以在启动时控制执行哪个初始进程。这是通过一个命令行参数实现的。
console=ttyS0,115200 ip=bootp root=/dev/nfs init=/sbin/myinit
内核命令行中以这种方式指定init=时,必须在根文件系统的/sbin目录中包含一个名为myinit的二进制可执行程序。在内核的引导过程完成后,myinit会成为第一个获得控制权的进程。
init进程
标准的init进程的功能非常强大,除非一些非常特别的事情,否则不需要提供用户定制的初始进程。init程序和一组启动脚本共同实现了通常所说的System V Init,该名字来源于最初使用的这种方案。
init是内核在完成引导以后创建的第一个用户空间进程。init是所有linux系统中所有用户空间进程的最终父进程。init提供了一组默认的环境参数,比如初始的系统路径PATH.而所有其他进程都会继承这组参数。
init的主要功能是根据一个特定的配置文件生成其他进程。这个配置文件通常是指/etc/inittab。init有运行级别的概念,可以将运行级别看作系统状态。每个运行级别是由进入这个级别时所运行的服务和生成的程序决定的。任何时刻,init只能处于一种运行级别之中。init的运行级别为1-6和一个被称为S的特殊运行级别。每个运行级别一般都有一组相关的启动和关闭脚本,他们定义了系统处于这个运行级别的动作和行为。配置文件/etc/inittab决定了系统处于某个运行级别时所执行的动作,我们稍后会讲述这个文件。
运行级别 作用
0 系统关机(终止)
1 单用户系统配置,用户维护
2 用户自定义
3 通用的多用户配置
4 用户自定义
5 多用户配置,启动后进入图形界面
6 系统重起
与运行级别相关的脚本文件一般位于目录/etc/rc.d/init.d中。在这个目录中可以找到大多数用于启动和停止相应服务的脚本。可以通过运行脚本手动配置服务,在运行脚本时需要将合适的参数(比如start、stop和restart)传递给脚本。
运行级别是由它所启动的服务定义的。大多数的Linux发行版都会在目录/etc中包含一个目录结构,这些目录中包含了符号连接,指向目录/etc/rc.d/init.d中的服务脚本。与运行级别相关的目录一般位于目录/etc/rc.d中。在这个目录中,存在一系列与运行级别相关的目录,一般一个运行级别对应一个目录,目录中包含每个运行级别的启动和关闭脚本。init只是在进入和退出一个运行级别时执行这些脚本。这些脚本定义了系统状态。而inittab则是告诉init某个运行级别是和哪个脚本相关联的。
运行级别目录结构
/etc/rc.d
drwxr-xr-x 2 root root 4096 otc 20 10:19 init.d
-rwx-xr-x 1 root root 2352 mar 16 2009 rc
drwx-xr-x 2 root root 4096 mar 22 2009 rc0.d
...
每个运行级别是由目录rcN.d中的脚本定义的。每个rcN.d目录都包含大量的符号连接,他们按照特定的顺序排列。这些符号连接的名字以K或S开头,以S开头的符号连接指向启动时所执行的(进入这个运行级别)执行的服务脚本。以K开头的符号连接指向关闭(退出这个运行级别)时执行的服务脚本。
lrwxrwxrwx 1 root root 17 Nov 25 2009 S10network -> .../init.d/network
lrwxrwxrwx 1 root root 16 Nov 25 2009 S12syslog -> .../init.d/syslog
lrwxrwxrwx 1 root root 16 Nov 25 2009 S56xinetd -> .../init.d/xinetd
lrwxrwxrwx 1 root root 16 Nov 25 2009 K50xinetd -> .../init.d/xinetd
lrwxrwxrwx 1 root root 16 Nov 25 2009 K88syslog -> .../init.d/syslog
lrwxrwxrwx 1 root root 16 Nov 25 2009 K90network -> .../init.d/network
根据这个目录中的内容,当进入这个假象的运行级别时,启动脚本会执行以下三个服务:network、syslog和xinetd。因为这三个以S开头的脚本是按照它们名称中的数字顺序排列的,他们会按这个顺序启动。类似的,当退出这个运行级别时,以下三个服务会停止:xinetd、syslog和network。同样,这三个以K开头的符号连接文件名中包含了一个两位数字,他们按照这个数字顺序终止服务。在一个实际系统中,运行级别目录中肯定会由更多的文件。也可以在这些目录中添加文件,以适合自己的定制应用。
init配置文件中定义了一个顶层脚本,它负责执行这些启动和关闭服务的脚本,我门来研究一下这个顶层脚本。
inittab
当init启动时,它会读取配置文件/etc/inittab。这个文件中包含了针对每个运行级别的指令,也包含了对所有运行级别都有效的指令。开发人员如何为嵌入式系统配置inittab。在终端输入man init和man inittab就可以了解inittab和init是如何协同工作的。
现在来看一个典型的inittab,它用于一个简单的嵌入式系统中。该系统只支持一个运行级别,以及关机和重起
简单的inittab
# /etc/inittab
#默认的运行级别(这个例子中为2)
id:2:initdefault:
#这是第一个运行的进程(实际上是一个脚本)
si::sysinit:/etc/rc.sysinit
#当进入运行级别0时,执行关机脚本
10:0:wait:/etc/init.d/sys.shutdown
#当进入运行级别2时,执行正常启动脚本
12:2:wait:/etc/init.d/runlv12.startup
#这一行执行一个重起脚本(运行级别6)
16:6:wait:/etc/init.d/sys.reboot
#这一行在控制台上生成一个登陆shell
#respawn意味着每次终止后它都会重新启动
con:2:respawn:/bin/sh
这个非常简单的inittab脚本描述了3个不同的运行级别。每个运行级别都和一个脚本相关联。脚本必须由开发人员根据运行级别的期望行为而创建。当init读取inittab文件时,执行的第一个脚本是/etc/rc.sysinit。由标签sysinit表示。然后,init进入级别2,并执行转为运行级别2定义的脚本。在这个例子中,这个脚本是指/etc/init.d/runlvl2.startup。从:wait:标签猜到了,init要等到这个脚本完成后才会继续。当运行级别2的脚本完成以后init会在控制台上生成一个登陆shell(通过/bin/sh符号连接)。上述代码的最后以行所示,关键字respawn指示init每次发现shell已经退出时重起它。下面的内容显示了系统引导时的输出消息。
...
VFS:Mounted root(nfs filesystem)
Freeing init memory:304K
INIT:version 2.78 booting
This is rc.sysinit
INIT:Entering runlevel:2
This is runlvl2.startup
#
该启动脚本很简单,仅仅是为了说明概念