构建Linux根文件系统

嵌入式Linux系统移植之构建Linux根文件系统

《嵌入式Linux应用完全开发手册》第3篇第17章总结归纳

本章目标

  1. 了解Linux的文件系统层次标准(FHS)
  2. 了解根文件系统下各目录的作用
  3. 掌握构建根文件系统的的方法:移植Busybox、构造各个目录、文件等
  4. 掌握制作yaffs、jffs2文件系统映像的方法

Linux文件系统概述

Linux文件系统的特点

类似于Windows下的C、D、E等各个盘,Linux系统也可以将磁盘、Flash等存储设备划分为若干个分区,在不同分区存放不同类别的文件。与Windows的C盘类似,Linux一样要在一个分区上系统启动必须的文件,比如内核映像文件(在嵌入式系统中,内核一般单独存放在一个分区中)、内核启动后运行的第一个程序(init)、给用户提供操作界面的shell程序、应用程序所依赖的库等。这些必需、基本的文件合称为根文件系统。它们存放在一个分区中。Linux系统启动后首先挂载这个分区,称为挂载根文件系统。其他分区上所有目录、文件的集合,也称为文件系统。
Linux中没有C、D、E盘符的概念,它以树状结构管理所有目录,文件。其他分区挂载在某个目录上,这个命令称为挂载点,然后就可以通过访问这个目录来访问这个分区上的文件了。比如根文件系统挂载在“/”上之后,根目录下就有根文件系统的各个目录、文件:/bin、/sbin、/mnt等;再将其他分区挂接到/mnt目录上,/mnt下目录下就有这个分区的目录、文件了。
在一个分区下存储文件时,需要遵循一定的格式,这种格式称为文件系统类型,比如fat16fat32ntfsext2ext3jffs2yaffs等。除这些拥有实实在在的存储分区的文件系统类型外,Linux还有几种虚拟的文件系统类型,比如procsysfs等,它们的文件并不存储在实际的设备上,而是在访问它们时由内核临时生成。比如proc文件系统下的uptime文件,读取它时可以得到两个时间值(用来表示系统启动后运行的秒数、空闲的秒数),每次读取时都由内核即刻生成,每次读取结果都不一样。

Linux根文件系统目录结构

img not found

/bin 目录

该目录下存放所有用户都可以使用的、基本的命令,这些命令在挂接其他文件系统之前就可以使用,所以/bin目录必须和根文件系统在同一个分区中。
/bin目录下常用的命令有:cat、chgrp、chmod、cp、ls、sh、kill、mount、umount、mkdir、mknod、[、test等。“[”命令就是test命令,在脚本文件中“[ expr ]”等价于“test expr”。

/sbin目录

该目录下存放系统目录,即只有管理员能够使用的命令,系统命令话可以存放在/usr/sbin、/usr/local/sbin目录下。/sbin目录中存放的是基本的系统命令,它们用于启动系统、修复系统等。与/bin目录相似,在挂载其他文件系统之前就可以使用/sbin,所以/sbin目录必须和根文件系统在同一个分区中。
/sbin下常用的命令有:shutdown、reboot、fdisk、fsck等。
不是急迫需要使用的系统命令存放在/usr/sbin目录下。本地安装的系统命令存放在/usr/local/sbin目录下。

/dev目录

该目录下存放的是设备文件。设备文件是Linux中特有的文件类型,在Linux系统下,以文件的方式访问各种外设,即通过读写某个设备文件操作某个具体硬件。比如通过“/dev/ttySAC0”文件可以操作串口0,通过“/dev/mtdblcok1”可以访问MTD设备(NAND Flash、NOR Flash等)的第2个分区。
设备文件有两种:字符设备和块设备。在PC上执行命令“ls /dev/ttySAC0 /dev/hda1 -l”可以看到如下结果:

1
2
brwxrwxr-x  1   root    49      3,  1   Oct 9   2005 /dev/hda1
crwxrwxr-x 1 root root 4, 64 Sep 24 2007 /dev/ttySAC0

其中字母“b”、“c”表示这是一个块设备文件或字符设备文件;“3,1”、“4,64”表示设备文件的主、次设备号;主设备号用来表示这是哪类设备,次设备号用来表示这是这类设备中的哪个。
设备文件可以使用mknod命令创建

1
2
mknod /dev/ttySAC0 c 4 64
mknod /dev/hda1 b 3 1

/dev 的创建有3种方法。

  1. 手动创建
    在制作根文件系统的时候,就在/dev目录下创建好要使用的设备文件,比如ttySAC0等。系统挂接根文件系统后就可以使用/dev下的设备文件了。
  2. 使用devfs文件系统(这种方法已经过时)
    以前的内核中有一个配置选项CONFIG_DEVFS_FS,它用来将虚拟文件系统devfs挂接在/dev目录上,各个驱动程序注册时会在/dev下目录下自动生成各种设备文件。就免去了手动创建设备文件的麻烦,在制作根文件系统时,/dev目录可以为空。
    使用devfs比手动创建设备节点更便利,但是它仍有一些无法克服的缺点
    ①不确定的设备映射
    比如USB接口连接两台打印机A、B。都开机的情况下以/dev/usb/lp0访问A,以/dev/usb/lp1访问B。但是假如A没有上电,则系统启动时会根据扫描到的设备的顺序,以/dev/usb/lp0访问B。
    ②没有足够的主/次设备号
    主次设备号是两个8位的数字,它们不足以与日益增加的外设一一对应。
    ③命名不够灵活
    由于devfs由内核创建设备节点,当想重新修改某个设备的名字时需要修改编译内核。
    ④devfs消耗大量内存
    由于上述缺点,Linux在2.3.46引入devfs之后,又在Linux2.6.13后面版本移除了devfs,而使用udev机制代替。
  3. udev
    udev是个用户程序(userspace device),它能够根据系统中硬件设备的状态动态的更新设备文件,包括设备文件的创建和删除等。
    使用udev机制也不需要在/dev目录下创建设备节点,他需要一些用户程序的支持,并且内核要支持sysfs文件系统。它的操作相对复杂,但是灵活性很高。

/etc 目录

如下表所示,该目录下存放各种配置文件。对于PC上的Linux系统,/etc目录下目录、文件非常多。这些目录、文件都是可选的,它们依赖于系统中所拥有的应用程序,依赖于这些程序是否需要配置文件。在嵌入式系统中,这些内容可以大为精减。

目录 描述
opt 用来配置/opt下的程序 (可选)
X11 用来配置X Window (可选)
sgml 用来配置SGML (可选)
xml 用来配置XML (可选)
文件 描述
export 用来配置NFS文件系统 (可选)
fstab 用来指明当执行“mount -a”时,需要挂接的文件系统 (可选)
mtab 用来显示已经加载的文件系统,通常是/proc/mounts的链接文件 (可选)
ftpusers 启动FTP服务时,用来配置用户的访问权限 (可选)
group 用户的组文件 (可选)
inittab init进程的配置文件 (可选)
ld.so.conf 其他共享库的路径 (可选)
passwd 密码文件 (可选)

/lib 目录

该目录下存放共享库和可加载模块(驱动程序),共享库用于启动系统、运行根文件系统中的可执行程序,比如/bin、/sbin目录下的程序。其他不是根文件系统所必须的库文件可以放在其他目录,比如/usr/bin、/usr/X11R6/lib、、var/lib等。

目录/文件 描述
libc.so.* 动态连接C库(可选)
ld* 连接器、加载器(可选)
modules 内核可加载模块存放的目录(可选)

/home 目录

用户目录,它是可选的。对于每个普通用户,在/home目录下都有一个以用户命名的子目录,里面存放用户相关的配置文件。

/root 目录

根用户(用户名为root)的目录,与此对应,普通用户的目录是/home下的某个子目录。

/usr 目录

/usr 目录的内容可以存放在另一个分区中,在系统启动后再挂接到根文件系统的/usr目录下。里面存放的是共享、只读的程序和数据,这表明/usr目录下的内容可以在多个主机间共享,这些主机也符合FHS标准,/usr中的文件应该也是只读的,其他相关、可变的文件也应该保存在其他目录下,比如/var。

目录 描述
bin 很多用户命令存放在这个目录下
include C程序的头文件,这在PC下进行开发时才用到,在嵌入式系统中不需要
lib 库文件
local 本地目录
sbin 非必需的系统命令(必需的系统命令放在/sbin目录下)
share 架构无关的数据
X11R6 XWindows系统
games 游戏
src 源代码

/var 目录

与/usr目录相反,/var目录下存放可变的数据,比如/spool目录,log文件、临时文件。

/proc

这是一个空目录,常作为proc文件系统的挂接点。proc文件系统是个虚拟的文件系统,它没有实际的存储设备,里面的目录、文件都是由内核临时生成的,用来表示系统的运行状态,也可以操作里面的文件控制系统。
系统启动后,使用以下命令挂接proc文件系统(常在/etc/fstab进行设置以自动挂接)

1
# mount -t proc none /proc

/mnt 目录

用于临时挂接某个文件系统的挂接点,通常是空目录;也可以在里面创建一些空的子目录,比如/mnt/cdram、/mnt/hda1等,用来临时挂接光盘、硬盘。

/tmp 目录

用于存放临时文件,通常是空目录。一些需要临时生成文件的程序要用到/tmp目录,所以/tmp目录必须存在并可以访问。
为减少对Flash的操作,当在/tmp目录上挂接内存文件系统时,如下所示:

1
# mount -t tmpfs none /tmp

Linux 文件属性介绍

文件类型 描述
普通文件 最常见的文件类型
目录文件 目录也是文件
字符设备文件 用来访问字符设备
块设备文件 用来访问块设备
FIFO 用于进程间通信,也称为命名管道
套接字 用于进程间的网络通信
连接文件 指向另一个文件,有软连接、硬连接

使用“ls -lih”命令可以看到各个文件的具体信息,下面选取这几种文件,列出它们的信息。
img not found
除设备文件ttySAC0、mtdblock0外,这些信息都分为8个字段,比如:

1
2
228883  -rw-r--r--  2   root    root    6   Sep 27  22:10   readme.txt
字段1 2 3 4 5 6 7 8
  1. 字段1:文件的索引节点inode
    索引节点存放一个文件的上述信息,比如文件大小、属主、归属的用户组、读写权限等,并指明文件的实际数据存放的位置。
  2. 字段2:文件种类和权限
    这字段共分10位,格式如下:
    文件类型有7种,“-”表示普通文件,“d”表示目录,“c”表示字符设备,“b”表示块设备,“p”表示FIFO,“l”表示软连接,“s”表示套接字。
    没有专门的符号来表示“硬连接”类型,硬连接也是普通文件,只不过文件的实际内容只有一个副本,连接文件、被连接文件都指向它。比如上面的ln_hard文件时是使用命令“ln readme.txt ln_hard”创建出来的到readme.txt文件的硬连接,readme.txt和ln_hard的地位完全一致,它们都指向文件系统中的同一位置,它们的“硬连接个数”都是2,表示这个文件的实际内容被引用两次,可以看到这两个文件的inode都是228883。
    硬连接文件的引入的另一个作用是使得可以用别名来引用一个文件,避免文件被误删—只有当硬链接个数为1时,对一个文件执行删除操作时才会真正删除文件的副本。它的缺点是不能创建到目录的连接,被连接的文件和连接文件都必须在同一个文件系统中。对此,引入软连接,也称符号连接,软连接只是简单的指向一个文件(可以是目录),并不增加它的硬连接个数。比如上面的ln_soft文件就是使用命令“ln -s readme.txt ln_hard”创建出来的到readme.txt文件的软连接,它使用另一个inode。
    剩下的9位分为3组,分别表示文件拥有者、同一个群组的用户、其他用户对这个文件的访问权限。每组权限由rwx三位组成,表示可读、可写、可执行。如果某一位被设为“-”,则表示没有相应的权限,比如“rw-”表示只有读写权限,没有可执行权限。
  3. 字段3:硬链接个数
  4. 字段4:文件拥有者
  5. 字段5:所属群组
  6. 字段6:文件或目录大小
  7. 字段7:最后访问或修改时间
  8. 字段8:文件名

对于设备文件,字段6表示主设备号,字段7表示次设备号。

移植 Busybox

所谓制作根文件系统,就是创建各种目录,并且在里面创建各种文件,比如在/bin、/sbin目录下存放各种可执行程序,在/etc目录下存放配置文件,在/lib目录下存放库文件。这节讲述如何使用Busybox来创建/bin、/sbin等目录下的可执行文件。

Busybox 概述

Busybox是一个遵循GPL v2协议的开源项目。Busybox将众多的UNIX命令集合进一个很小的可执行程序中,可以用来替换GNU fileutils、shellutils等工具集。Busybox中各种命令与相应的GNU工具相比,所能提供的选项较少,但是能够满足一般应用。Busybox为各种小型的或者嵌入式系统提供了一个比较完全的工具集。
Busybox在编写过程中对文件大小进行优化,并考虑了系统有限的资源的情况。与一般的GNU工具集动辄几MB的体积相比,动态连接的Busybox只有几百KB,即使静态链接也只有几MB左右。Busybox按模块进行设计,可以很容易的加入、去除某些命令,或增减命令的某些选项。
在创建一个最小的根文件系统时,使用Busybox的话,只需要在/dev目录下创建必要的设备节点、在/etc目录下创建一些配置文件就可以了,如果Busybox使用动态连接,还要在/lib目录下包含库文件。
Busybox支持uClibc库和glibc库,对Linux 2.2.x之后的内核支持良好。
Busybox的官方网站时 http://www.busybox.net/,源码可以从http://www.busybox.net/downloads下载。

init进程介绍及用户程序启动过程

init进程是由内核启动的第一个用户进程,进程ID为1,它根据配置文件决定启动哪些程序,比如执行脚本、启动shell、运行用户指定的程序等。init进程是后续所有进程的发起者,比如init进程启动/bin/sh之后,才能够在控制台输入各种命令。
init进程的执行程序通常是/sbin/init,上面讲述的init进程的作用只不过是/sbin/init这个程序的功能。我们完全可以编写自己的/sbin/init程序,或者传入命令行参数“init=xxxxx”指定某个程序作为init进程运行。
一般而言,在Linux系统中有两种init程序:BSD init和System V init。BSD和System V 是两种版本的UNIX系统。这两种init程序各有优缺点,现在大多数Linux的发行版本使用System V init。但是在嵌入式领域,通常使用Busybox集成init程序。

内核如何启动init进程

内核启动的最后一步就是启动init进程,代码在init/main.c文件中,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static int noinline init_post(void)
{
...
if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
printk(KERN_WARNING "Warning: unable to open an initial console.\n");

(void) sys_dup(0);
(void) sys_dup(0);

if (ramdisk_execute_command) {
run_init_process(ramdisk_execute_command);
printk(KERN_WARNING "Failed to execute %s\n",
ramdisk_execute_command);
}
...
if (execute_command) {
run_init_process(execute_command);
printk(KERN_WARNING "Failed to execute %s. Attempting "
"defaults...\n", execute_command);
}
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");

panic("No init found. Try passing init= option to kernel.");
}

代码不复杂,其中的run_init_process函数使用它的参数所指定的程序来创建一个用户进程。需要注意,一旦run_init_process函数创建进程成功,它将不会返回。
内核启动init进程的过程如下:

  1. 打开标准输入、标准输出、标准错误设备。
    Linux最先打开的3个文件分别称为标准输入(stdin)、标准输出(stdout)、标准错误(stderr),它们对应的文件描述符分别为0、1、2。所谓标准输入就是在使用scanf()、fscan()获取数据时,从哪个文件(设备)读取数据;标准输出、标准错误都是输出设备,前者对应printf()、fprintf(stdout),后者对应发fprintf(stderr)。
    第[4]行尝试打开/dev/console设备文件,如果成功,它就是init进程标准输入设备。
    第[7]、[8]行将文件描述符0复制给文件描述符1、2,所以标准输入、标准输出、标准错误都对应同一个文件(设备)。
    在移植Linux内核时,如果发现打印出“Warning:unable to open an initial console.”,其原因大多是:根文件系统虽然被正确挂接了,但是里面的内容不正确,要么就是没有/dev/console这个文件,要么它没有对应的设备。
  2. 如果ramdisk_execute_command变量指定了要运行的程序,启动它。
    ramdisk_execute_command的取值(代码也在init/main.c中)分3种情况。
    ①如果命令行参数指定了“rdinit=…”,则ramdisk_execute_command等于这个参数指定的程序。
    ②否则,如果/init程序存在,ramdisk_execute_command就等于“/init”。
    ③否则,ramdisk_execute_command为空。
    本书所用的命令行没有设定“rdinit=…”,根文件系统中也没有/init程序,所以ramdisk_execute_command为空,所以第[11]-[13]行代码不执行。
  3. 如果execute_command变量指定了要运行的程序,启动它。
    如果命令行参数中指定了“init=…”,则execute_command等于这个参数指定的程序,否则为空。
    本书所用的命令行没有设定“init=…”,所以第[17]-[19]行代码不执行。
  4. 依次尝试执行/sbin/init、/etc/init、/bin/init、/bin/sh。
    第[21]行执行/sbin/init程序,这个程序在我们的根文件系统中是存在的,所以init进程所用的程序就是/sbin/init。从此系统的控制器交给/sbin/init,不再返回init_post函数中。
    run_init_process函数也在init/main.c中,代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    static char * argv_init[MAX_INIT_ARGS+2] = { "init", NULL, };
    char * envp_init[MAX_INIT_ENVS+2] = { "HOME=/", "TERM=linux", NULL, };
    ...
    static void run_init_process(char *init_filename)
    {
    argv_init[0] = init_filename;
    kernel_execve(init_filename, argv_init, envp_init);
    }
    所以执行/sbin/init程序时,它的环境参数为“HOME=/”,“TERM=linux”。

Busybox init 进程的启动过程

Busybox init 程序对应的代码在init/init.c文件中,下面以busybox-1.7.0为例进行讲解。
先概述其流程,再结合一个/etc/inittab文件讲述init进程的启动过程。

  1. Busybox init程序流程
    流程图如下所示,其中与构建根文件系统关系密切的是控制台的初始化、对inittab文件的解释和执行。
    img not found
    内核启动init进程时已经打开“dev/console”设备作为控制台,一般情况下Busybox init 程序就使用/dev/console。但是如果内核启动init进程的同时设置了环境变量CONSOLE或console,则使用环境变量所指的的设备。在Busybox init 程序中,还会检查这个设备是否可以打开,如果不能打开则使用“/dev/null”。
    Busybox init 只是作为其他进程的发起者和控制者,并不需要控制台与用户交互,所以init进程会把它关掉,系统启动后运行命令“ls /proc/1/fd”可以看到该目录为空。init进程创建其他子进程时,如果没有在/etc/inittab中指明它的控制台,则使用前面确定的控制台。
    /etc/inittab文件的相关文档和示例代码都在Busybox的example/inittab文件中。
    如果存在/etc/inittab文件,Busybox init程序解析它,然后按照它的指示创建各种子进程;否则使用默认的配置创建子进程。
    /etc/inittab文件中每个条目用来定义一个子进程,并确定它的启动方法,格式如下:
    1
    <id>:<runlevels>:<action>:<process>
    例如:
    1
    ttySAC0::askfirst:-/bin/sh
    对于Busybox init 程序,上述字段作用如下。
    :表示这个子进程要使用的控制台(即标准输入、标准输出、标准错误设备)。如果省略,则使用与init进程一样的控制台。
    :对于Busybox init 程序,这个字段没有意义。
    :表示init进程如何控制这个子进程,有如下8种取值。
    :要执行的程序,它可以是可执行程序,也可以是脚本。
    如果字段前面有“-”字符,这个程序被称为“交互的”。
    action名称 执行条件 说明
    sysinit 系统启动后最先执行 只执行一次,init进程等待它结束才继续执行其他动作
    wait 系统执行完sysinit进程后 只执行一次,init进程等待它结束才继续执行其他动作
    once 系统执行完wait进程后 只执行一次,init进程不等待它结束
    respawn 启动完once进程后 init进程检测发现子进程退出时,重新启动它
    askfirst 启动完respawn进程后 与respawn类似,不过init进程先输出“Please press Enter to activate this console.”,等用户输入回车键之后才启动子进程
    shutdown 当系统关机时 即重启、关闭系统命令时
    restart Busybox中配置了CONFIG_FEATURE_USE_INITTAB,并且init进程接收到SIGHUP信号时 先重新读取、解析/etc/inittab文件,再执行restart程序
    ctrlaltdel 按下Ctrl + Alt + Del组合键时

在/etc/inittab文件的控制下,init进程的行为总结如下。
①在系统启动前期,init进程首先启动为sysinit、wait、once的3类子进程。
②在系统正常运行期间,init进程首先启动为respawn、askfirst的两类子进程,并监视他们,发现某个子进程退出时重新启动它们。
③在系统退出时,执行为shutdown、restart、ctrlaltdel的3类子进程
如果根文件系统中没有/etc/inittab文件,Busybox init程序将使用如下默认的inittab条目。

1
2
3
4
5
6
7
8
::sysinit:/etc/init.d/rcS
::askfirst:/bin/sh
ttys2::askfirst:/bin/sh
ttys3::askfirst:/bin/sh
ttys4::askfirst:/bin/sh
::ctrlaltdel:/sbin/swapoff -a
::shutdown:/bin/umount -a -r
::restart:/sbin/init
  1. /etc/inittab 实例
    仿照Busybox的example的examples/inittab文件,创建一个inittab文件,内容如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # /etc/inittab
    # 这是init进程启动的第一个子进程,它是一个脚本,可以在里面指定用户想执行的操作
    # 比如挂接其他文件系统、配置网络等
    ::sysinit:/etc/init.d/rcS

    # 启动shell,以/dev/ttySAC0作为控制台
    ttySAC0:askfirst:-/bin/sh

    # 按下Ctrl + Alt + Del之后执行的程序
    ::ctrlaltdel:/sbin/reboot

    # 重启、关机前执行的程序
    ::shutdown:/bin/umount -a -r

编译/安装 Busybox

http://www.busybox.net/downloads下载busybox-1.7.0.tar.bz2。
使用如下命令解压得到busubux-1.7.0目录。

1
tar xjf busybox-1.7.0.tar.bz2

Busybox 集合了几百个命令,在一般系统中并不需要全部使用。可以通过配置Busybox来选择这些命令、定制某些命令的功能(选项)、指定Busybox的连接方法、指定Busybox的安装路径。

配置 Busybox

在 busybox-1.7.0 目录下执行“make menuconfig”命令即可进入配置界面。Busybox将所有配置项分类存放,如下表所示:

配置项类型 说明
Busybox Settings —>
General Configuration
一些通用的设置,一般不需要理会
Busybox Settings —>
Build Options
连接方式、编译选项等
Busybox Settings —>
Debugging Options
调试选项,使用Busybox时将打印一些调试信息,一般不选
Busybox Settings —>
Installation Options
Busybox的安装路径,不需要设置,可以在命令行中指定
Busybox Settings —>
Busybox Library Tuning
Busybox的性能微调,比如设置在控制台上可以输入的最大字符个数,一般使用默认值
Archival Utilities 各种压缩、解压缩工具,根据需要选择相关命令
Coreutils 核心的命令,比如ls、cp等
Console Utilities 控制台相关的命令,比如清屏命令clear等。
Debian Utilities Debian(Linux的一种发行版本)命令
Editors 编辑命令,一般都选中Vi
Finding Utilities 查找命令
Init Utilities init程序的配置选项,比如是否读取inittab文件
Login/Password Management Utilities 登录、用户账号/密码等方面的命令
Linux Ext2 FS Progs Ext2文件系统的一些工具
Linux Module Utilities 加载/卸载模块的命令
Linux System Utilities 一些系统命令,比如打印内核信息的dmesg、分区命令fdisk
Miscellaneous Utilities 一些不好分类的命令
Networking Utilities 网络方面的命令,可以选择一些方便调试的命令,比如telnetd、ping、tftp等
Process Utilities 进程相关的命令,如ps、free、kill、top等
Shells 有多种shell,比如msh、ash等,一般选择ash
System Logging Utilities 系统记录方面的命令
ipsvd Utilities 监听TCP、DPB端口
  1. Busybox的性能微调
    设置“TAB”键补全,如下配置:
    1
    2
    3
    Busybox Settings --->
    Busybox Library Tuning --->
    [*] Tab completion
  2. 连接/编译选项
    以下选项指定是否使用静态连接:
    1
    2
    Build Options --->
    [] Build Busybox as a static binary (no shared libs)
    使用glibc时,如果静态编译Busybox 会提示以下警告信息,表示会出现一些奇怪的问题。
    1
    # warning Static linking against glibc produces buggy executable
    所以,本书使用动态连接的Busybox,在构造根文件系统时需要在/lib目录下放置glibc库文件。
  3. Archival Utilities 选项
    选择tar命令:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Archival Utilities --->
    [*] tar
    [*] Enable archive creation
    [*] Enable -j option to handle .tar.bz2 files
    [*] Enable -X and -T options
    [*] Enable -z option
    [*] Enable -Z option
    [*] Enable support for old tar header format
    [*] Enable support for some GNU tar extensions
    [*] Enable long options
  4. Linux Module Utilities 选项
    要使用可加载模块,下面的配置选上。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Linux Module Utilities --->
    [*] insmode
    [*] Module version checking
    [*] Add module symbols to kernel symbol table
    [*] In kernel memory optimization (uClinux only)
    [*] Enable load map (-m) option
    [*] Symbols in load map
    [*] rmmod
    [*] lsmod
    [*] Support version 2.6.x Linux kernels
  5. Linux System Utilities 选项
    支持mdev,这可以很方便的构造/dev目录,并且可以支持热插拔设备。另外,为方便调试,选中mount、unmount 命令,并让mount命令支持NFS。
    1
    2
    3
    4
    5
    6
    7
    8
    Linux System Utilities --->
    [*] mdev
    [*] Support /etc/mdev.conf
    [*] Support command execution at device addtion/removal
    [*] mount
    [*] Suuport mounting NFS file system
    [*] unmount
    [*] unmount -a option
  6. Networking Utilities 选项
    1
    2
    3
    4
    5
    6
    7
    Linux System Utilities --->
    [*] ifconfig
    [*] Enable status reporting output (+7k)
    [] Enable slip-specific options "keepalive" and "outfill"
    [] Enable options "mem_start","io_addr" and "irq"
    [*] Enable option "hw" (ether only)
    [*] Enable set the broadcast automatically

编译和安装 Busybox

编译之前,先修改Busybox根目录的Makefile,使用交叉编译器。

1
2
3
4
5
6
修改前:
ARCH ?= $(SUBARCH)
CROSS_COMPILE ?=
修改后:
ARCH ?= arm
CROSS_COMPILE ?= arm-linux-

然后执行“make”命令编译Busybox。
最后是安装,执行“make CONFIG_PREFIX=dir_path install”就可以将Busybox安装在dir_name指定的目录下。执行以下命令在/work/nfs_root/fs_mini目录下安装Busybox。

1
$ make CONFIG_PREFIX=/work/nfs_root/fs_mini install

一切完成后,将在/work/nfs_root/fs_mini目录下生成如下文件、目录。

1
2
3
4
drwxr-xr-x  2   book    book    4096    2008-01-22  06:56   bin 
lrwxrwxrwx 1 book book 11 2008-01-22 06:56 linuxrc -> bin/busybox
drwxr-xr-x 2 book book 4096 2008-01-22 06:56 sbin
drwxr-xr-x 4 book book 4096 2008-01-22 06:56 usr

其中linuxrc和上面分析的/sbin/init程序功能完全一样;其他目录下是各种命令,不过它们都是到/bin/busybox的符号链接,比如/work/nfs_root/fs_mini/sbin目录下:

1
2
3
4
5
lrwxrwxrwx  1   book    book    14        2008-01-22  06:56   halt ->  ../bin/busybox
lrwxrwxrwx 1 book book 14 2008-01-22 06:56 ifconfig -> ../bin/busybox
lrwxrwxrwx 1 book book 14 2008-01-22 06:56 init -> ../bin/busybox
lrwxrwxrwx 1 book book 14 2008-01-22 06:56 insmod -> ../bin/busybox
lrwxrwxrwx 1 book book 14 2008-01-22 06:56 klogd -> ../bin/busybox

除bin/busybox外,其他文件都是到bin/busybox的符号链接。busybox是所有命令的集合体,这些符号链接文件可以直接运行。在开发板上,运行“ls”和“busybox ls”是一样的。

使用 glibc 库

在制作交叉编译工具链时,已经生成了glibc库,可以直接使用它来构建根文件系统。

glibc 库的组成

glibc库的位置是/work/tools/gcc-3.4.5-glibc-2.3.6/arm-linux/lib。
这个目录下的文件并非属于glibc库,比如crtl.o、libstdc++.a等文件是GCC工具本身生成的。本书不区分它们的来源,统一处理。
里面的目录、文件可以分为8类。
①加载器ld-2.3.6.so、ld-linux.so.2。
动态程序启动前,它们被用来加载动态库。
②目标文件(.o)
比如crtl.o、crti.o、crtn.o、gcrtl.o、Mcrtl.o、Scrtl.o等。在生成应用程序时,这些文件像一般的目标一样被连接。
③静态库文件(.a)
比如静态数学库libm.a、静态c++库libstdc++.a等,编译静态程序时会连接它们。
④动态库文件(.so,.so.[0-9]*)
比如动态数学库libm.so、动态c++库libstdc++.so等,它们可能是一个链接文件。编译动态库时会用到这些文件,但是不会连接它们,运行时才连接。
⑤libtool库文件(.a)
在连接库文件时,这些文件会被用到,比如它们列出了当前库文件所依赖的其他库文件。程序运行时无需这些文件。
⑥gconv目录
里面是有头字符集的动态库,比如ISO8859-l.o、GB18030.so等。
⑦ldscripts目录
里面是各种连接脚本,在编译应用程序时,它们被用于指定程序的运行地址、各段的位置等。
⑧其他目录及文件

安装glibc 库

在开发板上只需要加载器和动态库,假设要构建的根文件系统目录为/work/nfs_root/fs_mini,操作如下:

1
2
3
mkdir -p /work/nfs_root/fs_mini/lib
cd /work/tools/gcc-3.4.5-glibc-2.3.6/arm-linux/lib
cp *.so* /work/nfs_root/fs_mini/lib -d

上面复制的库文件不是每个都会用到,可以根据应用程序对库的依赖关系保留需要用到的。通过ldd命令查看一个程序会用到哪些库,主机自带的ldd命令不能查看交叉编译出来的文件。有以下两种替代方法。
①如果有uClibc-0.9.28的代码,可以进入utils子目录生成ldd.host工具。

1
2
cd uClibc-0.9.28/utils
make ldd.host

然后将生成的ldd.host放到主机/usr/local/bin目录下即可使用。
比如对于动态连接的Busybox,它的库的依赖关系如下:

1
2
3
4
5
ldd.host busybox
libcrypto.so.1 => /lib/libcrypto.so.1 (0x00000000)
libm.so.6 => /lib/libm.so.6 (0x00000000)
libc.so.6 => /lib/libc.so.6 (0x00000000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x00000000)

这表示Busybox要使用的要使用的库文件有libcrypto.so.1、libm.so.6、libc.so.6,加载器为/lib/ld-linux.so.2。上面的“not found”表示主机上没有这个条件,这么关系,开发板的根文件系统上有就行。
②可以使用以下命令:

1
arm-linux-readelf -a "your binary" | grep "Shared"

比如对于动态连接的Busybox,它的库依赖如下:

1
2
3
4
arm-linux-readelf -a ./busybox | grep "Shared"
0x00000001 (NEEDED) Shared library:[libcrypto.so.1]
0x00000001 (NEEDED) Shared library:[libm.so.6]
0x00000001 (NEEDED) Shared library:[libc.so.6]

构建根文件系统

前面在介绍了如何安装Busybox、C库,建立了bin/、sbin/、usr/bin、usr/sbin、lib/等目录,最小根文件系统的大部分目录、文件已经建立好。

构建etc目录

init进程根据/etc/inittab来创建其他子进程,比如调用脚本配置文件配置IP地址、挂接其他文件分系统,最后启动shell等。
etc目录下的内容取决于要运行的程序,本节只需要3个文件:etc/inittab、etc/init.d/rcS、etc/fstab。

创建etc/inittab文件

仿照Busybox的examples/inittab文件,在/work/nfs_root/fs_mini/etc目录下创建一个inittab文件,内容如下。

1
2
3
4
5
# /etc/inittab
::sysinit:/etc/init.d/rcS
ttySAC0::askfirst:-/bin/sh
::ctrlaltdel:/sbin/reboot
::shutdown:/bin/unmount -a -r

创建etc/init.d/rcS文件

这是一个脚本文件,可以在里面添加想自动执行的命令。以下命令配置IP地址、挂接/etc/fstab指定的文件系统。

1
2
3
#!/bin/sh
ifconfig eth0 192.168.1.17
mount -a

第一行表示这是一个脚本文件,运行时使用/bin/sh解析。
第一行用来配置IP地址。
第三行挂接/etc/fstab文件指定的所有文件系统。
最后还要改变它的属性,使它能够执行。

1
chmod +x etc/init.d/rcS

创建etc/fstab文件

内容如下,表示执行“mount -a”命令后将挂接proc、tmpfs文件系统。

1
2
3
# device        mount-point        type        options      dump        fsck        order
proc /proc proc defaults 0 0
tmpfs /tmp tmpfs defaults 0 0

/etc/fstab文件被用来定义文件系统的“静态信息”,这些信息被用来控制mount命令的行为。文件中各字段的意义如下。
①device:要挂接的设备
比如/dev/hda2、/dev/mtdblock1等设备文件;也可以是其他格式,比如对于proc文件系统这个字段没有意义,可以是任意值;对于NFS文件系统,这个字段为<host>:<dir>
②mount-point:挂接点
③type:文件系统类型
比如proc、jffs2、yaffs、ext2、nfs等;也可以是auto,表示自动检测文件系统类型。
④options:挂接参数,以逗号隔开。
/etc/fstab的作用不仅仅是用来控制“mount -a”的行为,即使是一般的mount命令也受它控制,这可以从下表的参数看出。除与文件系统类型相关的参数外,常用的有以下几种取值。

参数名 说明 默认值
auto
noauto
决定执行“mount -a”时是否自动挂接
auto:挂接 noauto:不挂接
auto
user
nouser
user:运行普通用户挂接设备
nouser:只允许root用户挂接设备
nouser
exec
noexec
exec:允许所挂接设备上的文件
noexec:不允许允许所挂接设备上的程序
exec
Ro 以只读方式挂接文件系统 -
Rw 以读写方式挂接文件系统 -
sync
async
sync:修改文件时,它会同步写入设备中
async:不会同步写入
sync
default rw、suid、dev、exec、auto、nouser、async等的组合 -

⑤dump和fsck order:用来决定控制dump、fsck程序的行为。
dump是一个用来备份文件的程序,fsck是一个用来检查磁盘的程序。
dump程序程序根据dump字段的值来决定这个文件系统是否需要备份,如果没有这个字段,或者其值为0,则dump忽略这个文件系统。
fsck程序根据fsck order字段来决定磁盘的检查顺序,一般来说对于根据根文件系统这个字段设为1,其他文件系统设为2。如果设为0,则fsck忽略这个文件系统。

构建dev目录

使用两种方式创建dev目录。

静态创建设备文件

为简单起见,本书先使用最原始的方法处理设备:在/dev目录下静态创建各种节点。
从系统启动过程可知,涉及的设备有:/dev/mtdblock、/dev/ttySAC、/dev/console、dev/null,只要建立以下设备就可以启动系统。

1
2
3
4
5
6
7
8
mkdir -p /work/nfs_root/fs_mini/dev
cd /work/nfs_root/fs_mini/dev
sudo mknod console c 5 1
sudo mknod null c 1 3
sudo mknod ttySAC0 c 204 64
sudo mknod mtdblock0 b 31 0
sudo mknod mtdblock1 b 31 1
sudo mknod mtdblock2 b 31 2

其他设备文件可以当系统启动后,使用“cat /proc/devices”命令查看内核中注册了哪些设备,然后一一创建相应的设备文件。

使用mdev创建设备文件

mdev是udev的简化版本,也是通过读取内核信息来创建设备文件。
mdev的用途主要有两个:初始化/dev目录、动态更新。动态更新不仅是更新/dev目录,还支持热插拔。
要使用mdev,需要内核支持sysfs文件系统,为了减少对Flash的读写,还要支持tmpfs文件系统。先确保内核已经设置了CONFIG_SYSFS、CONFIG_TMPFS配置项。
使用mdev的命令如下,请参考它们的注释以了解其作用:

1
2
3
4
5
6
mount -t tmpfs mdev /dev                    //使用内存文件系统,减少对Flash的读写
mkdir /dev/pts //devpts用来支持外部网络连接的虚拟终端
mount -t devpts devpts /dev/pts
mount -t sysfs sysfs /sys //mdev通过sysfs文件系统获得设备信息
echo /bin/mdev>/proc/sys/kernel/hotplug //设置内核,当有设备拔插时调用/bin/mdev程序
mdev -s //在/dev目录下生成内核支持的所有设备节点

要在内核启动时,自动运行mdev。这要修改/work/nfs_root/fs_mini中的两个文件:修改etc/fstab来自动挂载文件系统、修改etc/init.d/rcS加入要自动运行的命令。修改后如下所示:
①etc/fstab

1
2
3
4
5
# device        mount-point        type        options      dump        fsck        order
proc /proc proc defaults 0 0
tmpfs /tmp tmpfs defaults 0 0
sysfs /sys sysfs defaults 0 0
tmpfs /dev tmpfs defaults 0 0

②etc/init.d/rcS:加入下面几行

1
2
3
4
5
mount -a
mkdir /dev/pts
mount -t devpts devpts /dev/pts
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s

需要注意的是,开发板上通过mdev生成的/dev目录中,S3C2410、S3C2440的串口名是s3c2410_serial0、s3c2410_serial1、s3c2410_serial2,不是ttySAC0、ttySAC1、ttySAC2。需要修改etc/inittab文件。

1
2
3
4
修改前:
ttySAC0: askfirst:-/bin/sh
修改后:
s3c2410_serial0::askfirst:-/bin/sh

另外,mdev是通过init进程来启动的,在使用mdev构造/dev目录之前,init进程至少要用到设备文件/dev/console、/dev/nul,所以要建立这两个设备文件。

1
2
3
4
mkdir -p /work/nfs_root/fs_mini/dev
cd /work/nfd_root/fs_mini/dev
sudo mknod console c 5 1
sudo mknod null c 1 3

构建其他目录

其他目录可以是空目录,比如proc、mnt、tmp、sys、root等。

1
2
cd /work/nfs_root/fs_mini
mkdir proc mnt tmp sys root

现在,/work/nfs_root/fs_mini目录下就是一个非常小的文件系统。开发板可以将它作为网络根文件系统直接启动。如果要烧入开发板,还要将它制作为一个文件,称为映像文件。

制作/使用yaffs文件系统映像文件

按照前面的方法,在/work/nfs_root目录下构造了两个根文件系统:fs_mini、fs_mini_mdev。前者使用/dev目录中事先建立好的设备文件,后者使用mdev机制来生成dev目录,它们的差别只在于3点:etc/inittab文件、etc/init.d/rcS文件、dev/目录。下面以/work/nfs_root/fs_mini为例制作根文件系统映像。
所谓制作文件系统映像,就是将一个目录下的所有内容按照一定的格式存放到一个文件中,这个文件可以直接烧写到存储设备中去。当系统启动后挂接这个设备,就可以看到与原来目录一样的内容。
制作不同类型的文件系统映像需要使用不同的工具。

修改制作yaffs映像文件的工具

在yaffs源码包有一个utils目录,里面是工具mkyaffsimage和mkyaffs2image的源代码。前者用来制作yaffs1映像文件,后者用来制作yaffs2映像文件。
目前mkyaffsimage工具只能生成老格式的yaffs1映像文件,需要修改才能支持新格式。
yaffs1新、老格式的不同在于oob区的使用发生了变化:一是ECC检验码的位置发生了变化,二是可用空间即标记(tag)的数据结构定义发生了变化。
另外,由于配置内核时没有设置CONFIG_YAFFS_DOES_ECC,yaffs文件系统将使用MTD设备层的ECC校验方法,制作映像文件时也使用与MTD设备层相同的函数计算ECC码。
①oob区中校验码的位置变化:
oob区中使用6个字节来存放ECC校验码,前3个字节对应上半页,后3个字节对应下半页。
由nand_oob_16结构可知,以前的校验码在oob区中存放的位置为8、9、10、13、14和15,现在改为0、1、2、3、6、7。
②oob区中可用空间的数据结构定义变化。
oob区中可用的空间有8个字节,它用来存放文件系统的数据,代码中这些数据被称为标记(tag)。
老格式的yaffs1中,这8个字节的数据结构定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//老格式
typedef struct {
unsigned chunkId:20;
unsigned serialNumber:2;
unsigned byteCount:10;
unsigned objectId:18;
unsigned ecc:12;
unsigned unusedStuff:2;
}yaffs_Tags;

//新格式
typedef struct {
unsigned chunkId:20;
unsigned serialNumber:2;
unsigned byteCount:10;
unsigned objectId:18;
unsigned ecc:12;
unsigned deleted:1;
unsigned unusedStuff:1;
unsigned shouldBeff;
}yaffs_PackedTags1;

新、老结构有细微差别:老结构中有两位没有使用(unusedStaff);新结构中只有一位没有使用,另一位(deleted)被用来表示当前页是否已经删除。
③oob区中ECC码的计算
如果配置内核时设置了CONFIG_YAFFS_DOES_ECC,则yaffs文件系统将使用yaffs2/yaffs_ecc.c文件中的yaffs_ECCCalculate函数来计算ECC码;否则使用drivers/mtd/nand/nand_ecc.c文件中的nand_calculate_ecc函数。
mkyaffsimage工具原来的代码中使用yaffs_ECCCalculate函数。由于上面配置内核时,没有选择CONFIG_YAFFS_DOES_ECC,为了使映像文件与内核保持一致,要修改mkyaffsimage源码,使用nand_calculate_ecc函数。
对mkyaffsiamge的修改如下所示。
①增加头文件
修改文件mkyaffsimage.c,加上下面这行,里面定义了yaffs_PackedTags1结构。

1
#include "yaffs_packedtags1.h"

②修改mkyaffsimages.c文件的write_chunk函数
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
static int write_chunk(__u8 *data,__u32 chunkId,__u32 nBytes)
{
#ifdef CONFIG_YAFFS_9BYTE_TAGS //如果要生成老格式的yaffs1映像文件,定义这个宏
/*原来的代码*/
#else
yaffs_PachedTags pt1;
yaffs_ExtendedTags etags;
__u8 ecc_code[6];
__u8 oobbuf[16];

//写页数据,512个字节
error = write(outFile,data,512);
if(error < 0) return error;

//构造tag
etags.chunkId = chunkId;
etags.serialNumber = 0;
etags.byteCount = nBytes;
etags.objectId = objId;
etags.chunkDeleted = 0;

//重定位oob区中的可用数据(称为tag)
yaffs_PackTags1(&pt1,&etags);

//计算tag本身的ECC码
yaffs_CalcTagsECC((yaffs_Tags *)&pt1);

memset(oobbuf,0xff,16);
memcpy(oobbuf+8,&pt1,8);

//使用与内核MTD层相同的计算方法一页数据(5124字节)的ECC码,并把它们填入oob
nand_calculate_ecc(data,&ecc_code[0]);
nand_calculate_ecc(data+256,&ecc_code[3]);

oobbuf[0] = ecc_code[0];
oobbuf[1] = ecc_code[1];
oobbuf[2] = ecc_code[2];
oobbuf[3] = ecc_code[3];
oobbuf[4] = ecc_code[4];
oobbuf[6] = ecc_code[6];
oobbuf[7] = ecc_code[7];

nPages++;

//写oob数据,169字节
return write(outFile,oobbuf,16);
#endif
}

值得注意的是,第20行设置新tag结构中增加的chunkDeleted成员;第32-41行将计算出来的ECC码填入新的ECC位置,它正是nand_oob_16结构的eccpos数组定义的位置。
其中第32、33行的nand_calculate_ecc函数是从内核源文件drivers/mtd/nand/nand_ecc.c修改而来;在/work/system/Development/yaffs2/utils目录下新建一个同名文件nand_ecc.c,把内核文件nand_ecc.c的nand_calculate_ecc函数、函数中用到的nand_ecc_precalc_table数组摘出来;并去除函数中的第一个形参“struct mtd_info *mtd”。
③添加文件,修改Makefile
第23行的yaffs_PackTags1函数在上一层目录yaffs_packedtags1.c中定义,先将这个文件复制到当前目录。

1
cp ../yaffs_packedtags1.c ./

另外,nand_calculate_ecc函数是在新加的nand_ecc.c中定义的,所以要修改Makefile,把yaffs_packedtags1.c和nand_ecc.c也编译进mkyaffsimage工具中。

1
2
3
4
修改前:
MKYAFFSSOURCES = mkyaffsimage.c
修改后:
MKYAFFSSOURCES = mkyaffsimage.c yaffs_packedtags1.c nand_ecc.c

现在,在/work/system/Development/yaffs2/utils目录下执行“make”命令生成mkyaffsimage工具,将它复制到/usr/local/bin目录。

1
2
sudo cp mkyaffsimage /usr/local/bin
sudo chmod +x /usr/local/bin/mkyaffsimage

制作/烧写yaffs映像文件

使用如下命令将/work/nfs_root/fs_mini目录制作为fs_mini.yaffs文件。

1
2
cd /work/nfs_root
mkyaffsimage fs_mini fs_mini.yaffs

将fs_mini.yaffs放入tftp目录或nfs目录后,在U-Boot界面就可以下载、烧入NAND Flash中,操作命令如下:

1
2
3
tftp 0x30000000 fs_mini.yaffs 或 nfs 0x30000000 192.168.1.57:/work/nfs_root/fs_mini.yaffs
nand erase 0xA00000 0x3600000
nand write.yaffs 0x30000000 0xA00000 $(filesize)

现在可以修改命令行参数以MTD2分区作为根文件系统,比如在U-Boot控制界面如下设置:

1
2
set bootargs noinitrd console=ttySAC0 root=/dev/mtdblock2 rootfstype=yaffs
saveenv