构建嵌入式Linux系统
1.取本实验相关的软件包
到ftp下载以下软件包:
文件名 |
备注 |
binutils-2.16.1.tar.bz2gcc-4.0.2.tar.bz2 glibc-2.3.6.tar.bz2glibc-linuxthreads-2.3.6.tar.bz2 linux-2.6.15.4.tar.bz2linux-libc-headers-2.6.12.0.tar.bz2 |
制作交叉编译工具链所需的源码包 |
crosstool-0.43.tar.gz |
制作交叉编译工具链的脚本和补丁 |
crosstool.tar.bz2 |
已制作好的交叉编译工具链,需要解包到/opt目录使用 |
linux-2.6.27.tar.bz2 |
内核2.6.27 |
patch-2.6.27-aka |
针对2440开发板制作的内核patch |
zImage |
已编译好的内核,可直接下载到开发板 |
rootfs-basic.tar |
基本根文件系统打包,由于有设备文件,所以解包需要root权限 |
rootfs-basic.cramfs |
采用cramfs文件系统格式的基本根文件系统映像,可直接下载到开发板 |
busybox-1.9.1.tar.bz2 |
busybox源码包,嵌入式根文件系统基本命令集 |
sqlite-3.4.0.tar.gz |
sqlite源码包,嵌入式数据库 |
libcgi-1.0.tar.gz |
libcgi源码包,用于编写CGI程序的C函数库 |
qtopia-core-opensource-src-4.2.2.tar.gz |
QtopiaCore源码包,Qt的嵌入式版本 |
2.交叉编译工具链
要做嵌入式Linux开发,首先需要有交叉编译工具链(crosscompiling toolchain),也就是在主机(x86PC)上可编译生成目标板可执行文件(ARM指令)的编译工具。和普通的编译工具链一样,交叉编译工具链也包括编译、链接、修改目标文件的各种程序,如gcc、glibc、ld、gas、objdump、readelf等等。交叉编译工具链本身也是需要编译生成的,要得到一套完整的交叉编译工具链需要编译以下源码包:
binutils
gcc
glibc
linux(内核)
linux-libc-headers
glibc-linuxthreads
这些源码包具有非常强的版本依赖关系,如果源码包的版本不匹配,在编译过程中会出现各种各样的问题,有些问题需要对源码做些修改或者打补丁,有些问题则是没有解决办法的,只能更换匹配较好的版本。编译的步骤也是比较复杂的,主要有以下几步:
根据目标平台配置内核源代码,生成内核头文件
编译binutils
编译器的自举(bootstrap),也就是先编译出gcc的部分功能(没有glibc支持,只有C编译器没有C++编译器)
编译glibc
编译完整的gcc
幸运的是,已经有人写出了制作交叉编译器的脚本(http://www.kegel.com/crosstool),该脚本中提供了一系列源码包版本的匹配方案,使用者需要选择目标平台和源码包的版本匹配方案,执行该脚本会自动从官方网站(http://www.gnu.org/和http://www.kernel.org/)下载源代码,自动打补丁,然后自动完成所有的编译步骤。
首先从http://www.kegel.com/crosstool下载crosstool-0.43.tar.gz,在主目录下解包
~$tar xf crosstool-0.43.tar.gz; cd crosstool-0.43
现在选择一种源码包的版本组合,各种版本组合的兼容性可以参考http://www.kegel.com/crosstool/crosstool-0.43/buildlogs/,在此我们选择在arm平台上glibc版本较高,兼容性较好的一个组合:
gcc-4.0.2
cgcc-4.0.2
glibc-2.3.6
binutils-2.16.1
linux-2.6.15.4
hdrs-2.6.12.0
修改crosstool-0.43目录下的脚本demo-arm.sh,取消这一行开头的#号注释符:
#eval`cat arm.dat gcc-4.0.2-glibc-2.3.6.dat` sh all.sh --notest
同时将原本没有注释的这一行注释掉(前面加#号):
eval`cat arm.dat gcc-4.1.0-glibc-2.3.2-tls.dat` sh all.sh --notest
注意该脚本开头有
TARBALLS_DIR=$HOME/downloads
RESULT_TOP=/opt/crosstool
这说明,该脚本运行时,自动从http://www.gnu.org/和http://www.kernel.org/下载相关的源码包到主目录的downloads目录下,如果你已经下载过这些源码包,将它们拷到downloads目录下就可以不必再次下载了。对应于我们选择的版本组合,downloads目录下的源码包有:
binutils-2.16.1.tar.bz2
gcc-4.0.2.tar.bz2
glibc-2.3.6.tar.bz2
glibc-linuxthreads-2.3.6.tar.bz2
linux-2.6.15.4.tar.bz2
linux-libc-headers-2.6.12.0.tar.bz2
整个编译结束后,交叉编译工具链将放在/opt/crosstool目录下,因此脚本需要在/opt下建子目录,如果不希望使用root权限运行该脚本,则需要事先给/opt目录设置写权限:
~$sudo chmod a+w /opt
然后修改arm.dat,其中有
TARGET=arm-unknown-linux-gnu
这是按标准的命名规则为工具链命名的,但是通常我们都采用更简单的命名,很多软件的Makefile中交叉编译器默认也都采用简单的命名,为此我们把它改为TARGET=arm-linux。
由于编译过程需要用到patch、bison、flex,确认你的系统中安装了这些软件包。
在编译过程中可能还会遇到脚本的兼容性问题,如果你的Linux发行版将sh指向dash(例如Ubuntu),应将其改指向bash:
$cd /bin
$sudo ln -sf bash sh
虽然用dash执行脚本时非常高效并且其实现完全遵守POSIX标准,然而现存的很多脚本(比如glibc中的脚本)有不符合POSIX标准的用法,所以仍需改用bash执行。
准备就绪后,在crosstool-0.43目录下运行demo-arm.sh脚本开始编译。编译完成后,交叉编译工具链的可执行文件位于/opt/crosstool/gcc-4.0.2-glibc-2.3.6/arm-linux/bin目录,我们可以把这个路径添加到PATH环境变量中,例如将以下命令添加到~/.bashrc启动脚本:
exportPATH=$PATH:/opt/crosstool/gcc-4.0.2-glibc-2.3.6/arm-linux/bin
此外,网上也可下载到已编译好的交叉编译工具链,比如http://www.snapgear.org/snapgear/downloads.html,但是使用别人编译好的交叉编译工具链有很多限制,不能按自己的特殊需要对其定制,比如有些项目需要特定版本的gcc和glibc,再比如需要soft-float的编译器(生成指令模拟浮点数运算)。
以上编译步骤需要很长时间,也可以直接从ftp下载已制作好的交叉编译工具链安装到主机:
~$sudo chmod a+w /opt
~$tar xf crosstool.tar.bz2 -C /opt
然后在~/.bashrc启动脚本中修改PATH环境变量。
3.熟悉开发环境
3.1U-boot的基本使用方法
连接好开发板的网线和串口线,启动minicom(配置成1152008N1,无FlowControl)。按下开发板电源,立刻按除回车以外的任意键,进入bootloader提示符AKAE2440#。如果没来及按键就已经启动了内核,可以按开发板上的RESET键重来。
注意:u-boot的终端不能处理控制字符,不要用移动光标键或翻页键等,退格键可以用。按了产生控制字符的键再输入命令就会产生错乱,这时可以按下回车输入当前这条已经错乱的命令,然后在新的提示符下重新键入命令。
输入printenv命令显示bootloader的参数。
更改参数使用命令set或者setenv,如下图所示:
上图中,
setenv ipaddr 192.168.1.22表示设置开发板的IP地址为192.168.1.22。
setenv netmask 255.255.255.0表示设置子网掩码为255.255.255.0。
setenv serverip 192.168.1.21表示设置与开发板相连的PC机IP是192.168.1.21。
可以根据你的情况重新设置。为了统一起见,在后面的实验中,我们统一设置为:
AKAE2440#set ipaddr 192.168.2.100
AKAE2440# setserverip 192.168.2.21 (这一项应该和你的主机IP一致)
AKAE2440#set netmask 255.255.255.0
AKAE2440# set gateway 192.168.2.21 (网关ip设置成什么无所谓,一般都设为主机ip)
AKAE2440#save
save将这些设定写入flash中,下次开发板上电时仍然有效。如果不save则仅改变内存中的参数值,reset后会恢复flash中原来的参数值。
设置完成后在开发板上ping主机的ip地址,检查网络是否正常,如下所示:
AKAE2440#ping 192.168.2.21
host192.168.2.21 is alive
这表示主机能ping通,网络正常。如果显示如下:
AKAE2440#ping 192.168.2.21
pingfailed; host 192.168.2.21 is not alive.
这就表示主机不能ping通,网络异常,需要检查你的网络。
还有一些常见的参数:
AKAE2440#set bootdelay 3
Bootdelay设为3,表示开发板上电后等待3秒再启动,这3秒时间内按任意键会进入u-boot命令行状态,如下图所示:
另外,还有:
AKAE2440#set bootmode nfs
或者
AKAE2440# set bootmode nand
Bootmode可以设为nfs或者nand两者之一,这个参数表明了u-boot启动时,根文件系统在哪里寻找,如果是nand表示根文件系统保存在开发板本身的flash某个分区中,要到flash上去找;如果是nfs则表示u-boot要去你的PC机的nfs服务的目录(PC机硬盘上)去找。
在开发板bootloader输入以下命令查看64Mflash是如何分区的。
AKAE2440#mtdparts
Devicenand0 <akae2440-nand>. # parts = 4
#:name size offset mask_flags
0:u-boot 0x00100000 0x00020000 0
1:kernel 0x00200000 0x00100000 0
2:ext2 0x01000000 0x00300000 0
3:temp 0x02d00000 0x01300000 0
u-boot分区保存bootloader程序,Linux内核保存在kernel分区,根文件系统会保存到ext2分区,temp分区本实验中没有用到。
如果mtdparts命令未显示上述图样,可能是flash未作分区操作,可以使用mtdpartsdefault命令来创建上述那样的默认分区。如下图所示:
如果要保存新的分区表,使用命令save保存到flash中,否则reset后将恢复flash中原来的分区表。
下载文件到开发板内存中,需要通过tftp协议。开发板是tftp客户端,主机是tftp服务器。在配置好主机的tftp服务之后,主机上会有一个tftp服务的主目录/tftproot,我们把要下载到开发板上的文件都先拷贝到这个目录中,之后才能下载。tftp命令的格式为tftp <内存地址> 文件名。注意我们都是把内核zImage文件下载到0x30008000地址上,把根文件系统img文件下载到0x30800000地址上。
当我们用命令tftp0x30008000 zImage把内核zImage下载到内存地址0x30008000上之后,先使用nand erase kernel命令擦除掉flash的kernel分区上的数据(否则在读出flash数据时会报失败),然后可以用nandwrite 0x30008000 kernel0x200000命令把内存中的内核文件烧写到flash的对应kernel分区中去,如下图所示:
同理,当我们用命令tftp 0x30800000 rootfs.cramfs把根文件系统rootfs.cramfs映像文件下载到内存地址0x30800000上之后,用nandwrite 0x30800000 ext2 0x1000000命令把它烧写到flash的ext2分区中去,当然,在烧写之前也要先使用 nand erase ext2擦除掉flash的ext2分区,否则烧写后读出数据会失败。如下图所示:
现在,内核和cramfs根文件系统都烧写到flash上去了,只要设置好正确的内核启动参数就可以启动开发板了。
AKAE2440#set bootcmd nand read 0x30008000 kernel 0x200000\; myboot 0x30008000
AKAE2440#set bootargs noinitrd console=ttySAC0,115200
AKAE2440#set bootmode nand
AKAE2440#set init /linuxrc
AKAE2440#set root /dev/mtdblock2
AKAE2440#save
bootcmd参数表示u-boot上电后会自动执行的命令,第一条nandread 0x30008000 kernel0x200000表示从flash上kernel分区去读取Linux内核到开发板内存地址0x30008000处,大小为0x200000字节;第二条命令是myboot0x30008000表示跳转到地址0x30008000去启动Linux内核。这两条命令要用分号“;”隔开,但是设置bootcmd参数时设置给它的是一个字符串,所以分号要用转义字符“\;”代替。
bootargs就是我们常说的Linux内核的启动命令行参数(Linux_cmd_line)。U-boot通过这个参数告知内核一些关键的启动参数。
实际上,在Uboot中,最后的完整的启动命令行参数是由前面设置那些一个一个的小项组合而成的。真正完整的命令行参数是:
noinitrd console=ttySAC0,115200 init=/linuxrc cs89x0_media=rj45root=/dev/mtdblock2mtdparts=akae2440-nand:1M(u-boot),2M(kernel),16M(ext2),45M(temp)ip=192.168.2.100:192.168.2.21:192.168.2.21:255.255.255.0::eth0:off
noinitrd表示内核启动时不需要初始化一个内存磁盘ramdisk;而root=/dev/mtdblock2表示根文件系统在flash的编号为2的分区上(从0开始编号的,所以是第三个分区,回忆前面的u-boot的分区信息,flash上分区依次是u-boot,kernel,ext2,temp,我们的根文件系统就在第三个分区ext2上);console=ttySAC0,115200表示打开一个串行终端ttySAC0,它是通过串口1工作的,波特率为115200;init=/linuxrc表示内核启动好后第一个执行的应用程序是/linuxrc,根目录下的linuxrc;mtdparts=akae2440-nand:1M(u-boot),2M(kernel),16M(ext2),45M(temp)表示u-boot传递给内核的分区信息。
我们在选择使用nfs根文件系统还是cramfs根文件系统的时候,bootargs、bootcmd和init参数基本都一样,不用改变,需要改变的是bootmode参数,由bootmode参数的设置决定去选择root参数(描述cramfs根文件系统所在位置)还是nfsroot参数(描述nfs根文件系统所在位置),即bootmode为nand时选择root参数,bootmode为nfs时选择nfsroot参数。
现在启动。
AKAE2440# reset
这时屏幕上出现很多内核启动信息,之后提示登录,输入root即可登录。
(none)login: root
现在可以试试各种Linux基本命令。注意cramfs是只读文件系统,在开发板上运行时不能改动里面的文件。
有些时候我们可能会尝试配置不同的内核选项,会得到不同的几个内核zImage文件,这样都烧写到flash上再reset启动太浪费时间,可以用tftp把内核zImage加载到内存0x30008000地址上,不烧写,直接用myboot0x30008000去启动内存上的内核。如下图示:
3.2将自己定制的根文件系统下载到开发板运行
“根文件系统”是一种不严格的说法,其实是指文件系统中的文件和目录,这些文件和目录构成了一个Linux系统运行所需的基本框架。ftp上的rootfs-basic.tar是根文件系统的打包,由于包含设备文件,需要root权限才能解包:
~$sudo tar xf rootfs-basic.tar
解包后可以根据自己的需要修改其中的目录和文件,然后将根文件系统目录制作成cramfs映像再下到开发板运行,这样就达到了修改根文件系统中的目的。
首先确认你的主机上安装了软件包cramfsprogs,然后制作cramfs文件系统映像:
~$mkcramfs ~/rootfs rootfs.cramfs
然后按照前面的步骤把rootfs.cramfs下载到开发板的ext2分区,运行一下试试。
3.3设置开发板的启动方式为NFSRoot
基本思想是:在主机上开NFS服务器,把主机上的~/rootfs目录导出为NFS服务目录,使开发板一启动就自动加载主机的~/rootfs目录为根文件系统,省去了下载和烧写映像的麻烦,在主机上修改根文件系统中的文件立刻在开发板的系统上生效,在开发过程中十分便利。
首先在主机上安装和配置NFS服务,确认你的主机安装了nfs-kernel-server软件包。然后修改配置文件/etc/exports,添加下面一行(注意*号后面紧跟左括号,无空格,每个逗号后面也不能有空格):
/home/akaedu/rootfs*(rw,sync,no_root_squash)
这表示我们把上一节中通过解压rootfs-basic.tar得到的/home/akaedu/rootfs/目录设置为nfs服务目录。
更改配置后需要重启NFS服务:
~$sudo /etc/init.d/nfs-kernel-server restart
用rpcinfo-p命令看一下portmapper和nfs服务是否正常启动了,用exportfs命令看一下导出目录的设置是否正确。
进入开发板的bootloader,修改内核启动参数:
AKAE2440#set bootcmd nand read 0x30008000 kernel 0x200000\; myboot 0x30008000
AKAE2440#set bootargs noinitrd console=ttySAC0,115200
AKAE2440#set init /linuxrc
AKAE2440#set nfsroot /home/akaedu/rootfs
AKAE2440# set bootmode nfs
AKAE2440# save
AKAE2440# reset
真正完整的内核启动命令行参数是:
noinitrdconsole=ttySAC0,115200 init=/linuxrc root=/dev/nfsnfsroot=192.168.2.21:/home/akaedu/rootfsmtdparts=akae2440-nand:1M(u-boot),2M(kernel),16M(ext2),45M(temp)ip=192.168.2.100:192.168.2.21:192.168.2.21:255.255.255.0::eth0:off
注意,如果某一项参数很长,而minicom提供的终端却不能自动换行,一行写不下的部分只好盲打了,注意不要打错。如果是图形界面的console,可以把字体调小使得一行可以容得下这么长的参数,或者在gedit写字板程序中写好后直接粘贴到console窗口(我们推荐采用这种方法,把经常用到的启动参数都集中写好,以后可以选择对应的参数直接粘贴就好了,非常方便)。关于NFS根文件系统的内核启动参数参考内核源代码的Documentation/nfsroot.txt文档。
现在可以自己交叉编译一些小程序,然后拷贝到主机的~/rootfs目录,我们可以立刻在开发板上运行该程序,而不需要重新下载和烧写了。
3.4配置YAFFS文件系统
cramfs是只读文件系统,在Flash上压缩存储,解压并加载到RAM运行。由于只读因而功能上很受限制,但可以保护系统文件不被意外的写操作损坏。实际产品往往配置两个Flash分区,一个只读分区用于存放系统文件(程序文件和库文件),另一个可写分区,采用JFFS或YAFFS文件系统,用于存放应用程序数据,为了简便起见,我们只分一个区,使用可写的YAFFS文件系统。
注意,完成这个实验之前先要完成上一节内容,能启动到nfs根文件系统。
首先用NFS方式启动系统。YAFFS文件系统和我们熟悉的ext2不同,不需要格式化,只需要全部擦除就可以用了。用根文件系统中的flash_eraseall命令擦除root分区并mount上来。以下命令在开发板上执行
#flash_eraseall /dev/mtd2
Erasing16 Kibyte @ 6b8000 -- 10 % complete.
Skippingbad block at 0x006bc000
Erasing16 Kibyte @ 3dfc000 -- 99 % complete.
#mount -t yaffs /dev/mtdblock2 /mnt
yaffs:dev is 32505859 name is "mtdblock2"
yaffs:Attempting MTD mount on 31.3, "mtdblock2"
block432 is bad
#ls /mnt
lost+found
flash_eraseall命令是从mtd项目(http://www.linux-mtd.infradead.org/)的源代码中交叉编译得来的。
现在可以把根文件系统中的所有文件拷贝到Flash分区。以下命令在开发板上执行:
#cp -a bin etc lib linuxrc opt sbin srv tmp usr /mnt/
#mkdir -p /mnt/proc /mnt/sys /mnt/dev /mnt/mnt /mnt/var/run
#mknod /mnt/dev/console -m 600 c 5 1
#mknod /mnt/dev/null -m 666 c 1 3
#umount mnt
#reboot
注意:像/proc、/sys这样正mount着特殊文件系统的目录和/dev目录下的设备文件一定不可以用cp命令拷贝。/mnt目录显然也不能拷贝。这些目录和设备文件需要手动创建。
重启目标系统之后进入U-boot,修改内核启动参数使之从ext2分区启动即可(如果下次还用NFS启动,这里就不save了):
AKAE2440#set bootmode nand
AKAE2440#set root /dev/mtdblock2
AKAE2440#set bootargs noinitrd rootfstype=yaffs rw console=ttySAC0,115200
AKAE2440#save
AKAE2440# reset
现在试试能不能修改Flash根文件系统中的文件。
4.编译内核
将内核释放到~/linux-2.6.27目录下
~$tar xf linux-2.6.27.tar.bz2
进入内核目录:
~$cd linux-2.6.27
首先修改Makefile,找到
ARCH ?= $(SUBARCH)
CROSS_COMPILE ?=
改为
ARCH ?= arm
CROSS_COMPILE ?= arm-linux-
注意ARCH?=arm后面不能有空格,要确保空格已删除干净,并且不要漏了arm-linux-末尾的“-”。下面对内核打patch(注意打patch不要重复打):
~/linux-2.6.27$patch -p1 < ../patch-2.6.27-aka
然后以内核源代码自带的smdk2440基本配置文件为基础,使用menuconfig定制内核(确认你的主机安装了libncurses-dev软件包)。
~/linux-2.6.27$cp arch/arm/configs/akae2440_defconfig .config
~/linux-2.6.27$make menuconfig
查看配置菜单中的如下选项(有些已经默认选中):
1、在Floatingpoint emulation中选择NWFPEmath emulation。由于ARM920T没有浮点协处理器,浮点数指令需要用其它指令仿真实现。
有两种方法可以仿真浮点指令:一种方法如先前所说的,使交叉编译器支持soft-float,也就是一遇到C代码里的浮点运算就自动生成仿真指令,另一种方法是交叉编译器仍然生成浮点运算指令(对于ARM平台是指协处理器指令),然而ARM920T没有浮点协处理器,无法识别浮点指令,因而产生undefined异常,这个Floatingpointemulation内核选项就是在undefined异常的处理函数中仿真实现浮点运算。也就是说,这里我们使用非soft-float的交叉编译器并在内核中实现浮点仿真。这样的运行效率比使用soft-float编译要低,因为每次浮点运算都需要处理异常和进入内核,但是兼容性好,便于我们利用别人已编译好的程序。
2、选中Filesystems -> Pseudo filesystems -> /proc file system support; Sysctl support (/proc/sys); sysfs file system support 和Virtualmemory file system support (former shm fs); Tmpfs POSIX Accesscontrol Lists。 /procfilesystem(或者叫procfs)是内核提供给用户程序的接口,很多Linux程序都需要在procfs中读写数据,比如busybox,因此这个选项一般是不能少的。Virtualmemory filesystem(或者叫tmpfs)用于内存虚拟磁盘,后面我们在做根文件系统时要mount一个tmpfs到/dev目录下。
3、不需要改变Bootoptions -> Default kernel commandstring,因为bootloader中的内核启动参数会取代这里的设置。
4、选中DeviceDrivers -> Network device support -> Ethernet (10 or 100Mbit)-> CS8900A support,添加对网卡芯片的支持。在Filesystems -> Network File Systems菜单中选上NFSclient support;NFSclient support for NFS version3;NFSclient support for the NFSv3 ACL protocol extension和Rootfile system on NFS。
5、选中DeviceDrivers -> Memory Technology Devices (MTD) -> MTD partitioningsupport,在DeviceDrivers -> Memory Technology Devices (MTD),选中NANDDevice Support和它下面的NANDFlash support for S3C2410/S3C2440 SoC; S3C2410 NAND driverdebug,不要选择S3C2410NAND Hardware ECC,因为S3C2410硬件生成ECC码的算法和我们所需要的不一致。
6、在Filesystems -> Miscellaneous filesystems中选中YAFFS2file system support; Auto select yaffs2 format; cache short names inRAM, 再选上CompressedROM file system support (cramfs)。
7、在DeviceDrivers -> Graphics support中选中Supportfor frame buffer devices, 再选中它下面的Enablefirmware EDID; Enable video Mode Handling Helpers; S3C2410 LCDframebuffer support,不选择VirtualFrame Buffer support因为它是一个用于调试的虚拟设备驱动而不是实际硬件的驱动。如果还选中了BootupLogo,那么在内核启动过程中初始化framebuffer时会在屏幕上看到Linux的企鹅logo,这是测试framebuffer驱动是否正常工作最直接的方法。BootupLogo有三种规格,我们的开发板支持VGA输出,可以选择224色的漂亮logo。
8、在DeviceDrivers -> USB support中选中Supportfor Host-side USB,然后选择OHCIHCD support,在DeviceDrivers -> Input device support中选中Mouseinterface,设置屏幕分辨率Horizontalscreen resolution和Verticalscreen resolution为1024和768。
9、选中DeviceDrivers ->Real time clock -> Set system time from RTC onstartup and resume; /sys/class/rtc/rtcN (sysfs); /proc/driver/rtc(procfs for rtc0); /dev/rtcN (character devices); Test driver/device.
10、如果不希望在/dev目录下生成大量伪终端设备文件,可以取消选择DeviceDrivers -> Character devices -> Legacy (BSD) PTY support。
配置完成后用make命令编译内核,这个过程需要较长时间,编译好的内核位于~/linux-2.6.27/arch/arm/boot/zImage。现在将自己编译的内核下载到开发板,看系统能不能正常启动。
5.制作根文件系统
5.1根文件系统框架和busybox
首先用mkdir手动创建如下的基本目录结构:
~$tree rootfs/
rootfs/
|--bin
|--dev
|--etc
| |-- init.d
| `-- network
| |-- if-down.d
| |-- if-post-down.d
| |-- if-pre-up.d
| `-- if-up.d
|--lib
|--mnt
|--opt
|--proc
|--sbin
|--srv
|--sys
|--tmp
|--usr
| |-- bin
| |-- lib
| `-- sbin
`--var
`--run
/bin/sbin /usr/bin /usr/sbin |
系统的基本命令,系统关键组件的可执行文件位于/bin和/sbin,其它应用程序的可执行文件位于/usr/bin和/usr/sbin,bin和sbin的区别在于,bin目录下的可执行文件用于日常操作,例如ls、cp,sbin目录下的可执行文件用于管理操作,例如ifconfig,执行管理操作通常需要root权限 |
/lib/usr/lib |
共享库,也是分为系统关键组件的共享库和其它应用程序的共享库 |
/proc |
proc文件系统的挂载点 |
/sys |
sys文件系统的挂载点 |
/dev |
设备文件 |
/etc |
配置文件、启动脚本 |
/var |
运行时产生的记录文件、锁文件、日志文件 |
/tmp |
运行时产生的临时文件 |
/mnt |
一般用作挂载点 |
/opt |
一般存放第三方软件 |
/srv |
一般用作Web服务、ftp服务的服务目录 |
接下来安装busybox到根文件系统中。busybox是专为嵌入式Linux设计的,它把大多数常用命令(如ls、cp、tar等等)的常用选项剪裁出来拼在一起。在根文件系统中只有一个可执行文件就是/bin/busybox,而其它的命令都创建为/bin/busybox的链接文件,busybox通过命令行第0个参数(也就是命令名)判断应该执行哪个命令。这样使得嵌入式Linux系统有完整的命令集却占用很小的存储空间。
首先从官方网站http://www.busybox.net/下载源码包busybox-1.9.1.tar.bz2并解包到主目录下。
~$tar xf busybox-1.9.1.tar.bz2; cd busybox-1.9.1/
busybox的配置系统和内核源代码很相似,因此配置方法也和内核相似,首先修改Makefile,找到
ARCH ?= $(SUBARCH)
CROSS_COMPILE ?=
改为
ARCH ?= arm
CROSS_COMPILE ?= arm-linux-
然后我们以一个缺省配置为起点来做进一步的配置:
~/busybox-1.9.1$make defconfig
~/busybox-1.9.1$make menuconfig
在菜单中做如下配置:
选择BusyBoxSettings -> Installation Options -> BusyBox installationprefix,设置为/home/akaedu/rootfs,编译后将安装到这个目录下。
根据需要裁剪各命令。但是Shells-> Choose your defaultshell选项一定要保留一个默认shell(例如ash),如果没有默认shell,将不会创建/bin/sh这个链接,而一般的shell脚本都是以#!/bin/sh开头的,如果找不到/bin/sh就不能执行。
然后编译和安装busybox:
~/busybox-1.9.1$make
~/busybox-1.9.1$make install
busybox文件和一系列的链接文件将安装到/home/user/rootfs下。busybox文件位于根文件系统的/bin目录,其它链接文件位于/bin、/sbin、/usr/bin、/usr/sbin目录,有一个链接文件linuxrc位于根文件系统的根目录/,它是系统的启动程序,bootloader中内核的启动参数有init=/linuxrc,也就是说内核启动后首先执行/linuxrc(也是busybox的一个符号链接)。linuxrc负责完成系统的初始化工作:
设置信号处理程序
初始化console
解析/etc/inittab文件
执行系统初始化脚本,缺省的是/etc/init.d/rcS
执行inittab中类型为wait的程序
执行inittab中类型为once的程序
上述步骤完成后系统启动完成,此后init程序将循环执行以下步骤:
执行inittab中类型为respawn的程序,如果所执行的程序终止,则再次执行它
如果收到用户请求,则执行inittab中类型为askfirst的程序
linuxrc需要读取设备文件/dev/console和/dev/null,我们手动创建这些设备文件:
~$cd rootfs/dev; sudo mknod -m 600 console c 5 1; sudo mknod -m 666null c 1 3
注意,创建设备文件需要root权限,常见设备文件的设备号可以从内核代码的Documentation/devices.txt文件中查到。
然后创建一个启动配置文件~/rootfs/etc/inittab:
::sysinit:/etc/init.d/rcS
::respawn:/sbin/getty115200 s3c2410_serial0
::restart:/sbin/init
::shutdown:/bin/umount-a -r
启动的过程中首先执行rcS启动脚本,我们创建这个脚本~/rootfs/etc/init.d/rcS:
#!/bin/sh
mount-a
注意这个脚本需要加可执行权限:chmod+xrcS。这个脚本做了一件事情,根据/etc/fstab配置文件提供的信息mount一些文件系统,我们创建这个配置文件~/rootfs/etc/fstab:
#<file name> <mount point> <type> <options><dump> <pass>
proc/proc proc defaults 0 0
sys/sys sysfs defaults 0 0
/proc和/sys都是pseudo文件系统,它们并不在磁盘或flash上存储任何数据,虽然看起来/proc和/sys目录下有很多文件,但这些文件都是内核导出的接口,对这些文件进行读写会读写到内核中的一些运行时参数,而不会读写到磁盘。/proc文件系统是busybox中的许多程序所需要的接口,/sys文件系统则是后面要讲的mdev所需要的接口。
mount完文件系统后,rcS执行结束。下面将执行inittab文件中类型为respawn的命令,也就是
/sbin/getty115200 s3c2410_serial0
我们的PC在启动时执行getty打开几个虚终端,例如/dev/tty1~/dev/tty6,嵌入式开发板也是用getty打开虚终端,这个虚终端对应的是串口,因此需要访问串口的设备文件/dev/s3c2410_serial0,以上命令还指定了波特率为115200。现在我们创建这个设备文件:
~$cd rootfs/dev; sudo mknod -m 600 s3c2410_serial0 c 204 64
这个设备号在内核代码的Documentation/arm/Samsung-S3C24XX/Overview.txt中可以查到,在内核代码中,S3C2410的第一个串口表示为ttySAC0,但是后面介绍的mdev程序将使用s3c2410_serial0这个名字,因此这里把文件名指定为s3c2410_serial0。
系统启动打开虚终端后会提示输入用户名和密码然后才启动shell,我们需要创建帐号文件passwd和group。~/rootfs/etc/passwd文件内容为:
root::0:0:root:/:/bin/sh
~/rootfs/etc/group文件内容为:
root::0:
这样就创建了一个root帐号,没有密码。如果需要密码,可以把主机上的/etc/shadow文件中root帐号的那一行放到开发板的根文件系统中。
5.2glibc
由于busybox需要glibc共享库的支持,下面的步骤将在根文件系统中安装glibc。在此之前首先解释一下应用程序如何找到所需的共享库。通过ld命令的参数-rpath可以在链接时将共享库的路径记在应用程序中,但是通常不推荐这样做,通常应用程序中只记录共享库的文件名(稍后提到的soname),启动程序时由动态链接器/lib/ld-linux.so根据以下规则查找共享库(摘自ld.so(8)):
首先在环境变量LD_LIBRARY_PATH所记录的路径中查找
然后从缓存文件/etc/ld.so.cache中查找。将共享库的路径写进/etc/ld.so.conf然后执行ldconfig命令就可以生成缓存文件/etc/ld.so.cache
如果上述步骤都找不到,则到默认的系统路径中查找,先是/usr/lib然后是/lib
注意,LD_LIBRARY_PATH是不推荐使用的,尽量不要把它设定为环境变量,详细解释参见WhyLD_LIBRARY_PATH is bad(http://www.visi.com/~barr/ldpath.html)。
每个共享库有三个文件名:realname、soname、linkername。realname是库文件的名字,包含完整的共享库版本号;soname是库文件的一个符号链接的名字,只包含主版本号,主版本号一致即可保证库函数的接口一致,因此应用程序只需确认soname与所需的共享库一致;linkername仅由链接器使用,有的是库文件的一个符号链接的名字,有的是一段链接脚本的名字。库文件的文件头记录了soname,因此ldconfig命令可以读取文件头生成相应的符号链接文件。综上,ldconfig命令读取ld.so.conf后做两件事,一是创建适当的soname链接,二是生成ld.so.cache,包含共享库的软件包在安装过程中都会执行一下ldconfig命令。下面以libc为例说明共享库的三个文件名:
-rwxr-xr-x1 akaedu akaedu 1387379 2008-03-16 11:34 libc-2.3.6.so
lrwxrwxrwx1 akaedu akaedu 13 2008-03-16 11:06 libc.so.6 ->libc-2.3.6.so
-rw-r--r--1 akaedu akaedu 195 2008-03-16 11:10 libc.so
$cat libc.so
/*GNU ld script
Usethe shared library, but some functions are only in
thestatic library, so try that secondarily. */
OUTPUT_FORMAT(elf32-littlearm)
GROUP( libc.so.6 libc_nonshared.a )
在我们先前制作交叉编译工具链时已经交叉编译了glibc,位于/opt/crosstool/gcc-4.0.2-glibc-2.3.6/arm-linux/arm-linux/lib目录下,我们把这些共享库拷到根文件系统~/rootfs/lib目录下:
~$cd /opt/crosstool/gcc-4.0.2-glibc-2.3.6/arm-linux/arm-linux/lib; cp-dp * /home/akaedu/rootfs/lib/
然后到~/rootfs/lib目录下删掉所有静态库和一些不常用的库,剩下这些库文件:
~/rootfs/lib$ls
ld-2.3.6.so libm.so.6 libpcprofile.so
ld-linux.so.2 libnsl-2.3.6.so libpthread-0.10.so
libanl-2.3.6.so libnsl.so libpthread.so
libanl.so libnsl.so.1 libpthread.so.0
libanl.so.1 libnss_compat-2.3.6.so libresolv-2.3.6.so
libBrokenLocale-2.3.6.so libnss_compat.so libresolv.so
libBrokenLocale.so libnss_compat.so.2 libresolv.so.2
libBrokenLocale.so.1 libnss_dns-2.3.6.so librt-2.3.6.so
libc-2.3.6.so libnss_dns.so librt.so
libcrypt-2.3.6.so libnss_dns.so.2 librt.so.1
libcrypt.so libnss_files-2.3.6.so libSegFault.so
libcrypt.so.1 libnss_files.so libstdc++.so
libc.so libnss_files.so.2 libstdc++.so.6
libc.so.6 libnss_hesiod-2.3.6.so libstdc++.so.6.0.6
libdl-2.3.6.so libnss_hesiod.so libthread_db-1.0.so
libdl.so libnss_hesiod.so.2 libthread_db.so
libdl.so.2 libnss_nis-2.3.6.so libthread_db.so.1
libgcc_s.so libnss_nisplus-2.3.6.so libutil-2.3.6.so
libgcc_s.so.1 libnss_nisplus.so libutil.so
libm-2.3.6.so libnss_nisplus.so.2 libutil.so.1
libmemusage.so libnss_nis.so
libm.so libnss_nis.so.2
ldconfig是很常用的一个命令,如果某些程序把共享库安装到非标准目录(/lib和/usr/lib),则需要执行ldconfig更新缓存文件/etc/ld.so.cache,比如后面安装QtopiaCore时就会用到。但是busybox不包含ldconfig命令,因为它和glibc密切相关,是随glibc一起发布的,先前我们制作交叉编译工具链时已经生成了ldconfig,现在把它拷到根文件系统:
~$cpcrosstool-0.43/build/arm-linux/gcc-4.0.2-glibc-2.3.6/build-glibc/elf/ldconfigrootfs/sbin/
现在通过NFS启动开发板,应该可以成功启动到shell了。
5.3mdev
先前我们在根文件系统的/dev目录下手动创建了三个设备文件:console、null和s3c2410_serial0,内核里还有很多设备驱动需要通过设备文件提供接口给应用程序访问,如果一个一个手动创建太麻烦而且容易出错,busybox提供的mdev程序可以从sysfs中读出内核提供的设备驱动信息,在/dev目录下自动创建相应的设备文件并为这些设备文件命名。在PC上完成这一功能的是udev,mdev是busybox为嵌入式系统裁剪过的udev。
把以下几行附加到启动脚本~/rootfs/etc/init.d/rcS的末尾:
mount-t tmpfs mdev /dev
mdev-s
mkdir/dev/input
ln-s /dev/mice /dev/input/mice
mdev还需要一个配置文件mdev.conf,用于指定各设备文件的所有者和权限,其格式可参考busybox源代码中的docs/mdev.txt。我们不做特别的设置,但这个文件是需要存在的,因此创建一个空文件:
~$touch rootfs/etc/mdev.conf
现在重新启动开发板,从minicom可以看到开发板的/dev目录下创建了很多设备文件,而从主机上查看NFS目录~/rootfs/dev却仍然只有先前的三个设备文件,想一想这是为什么。
5.4系统时钟
首先将主机上的时区文件拷到开发板根文件系统:
~$cp /etc/localtime ~/rootfs/etc/
然后在启动脚本~/rootfs/etc/init.d/rcS中添加一条命令:
hwclock--hctosys
选项--hctosys的意思是读取硬件RTC时钟(hc)成为当前的系统时钟(sys)。在系统运行期间,调用time(2)等函数读取的都是内核维护的系统时钟,而不会直接去读硬件时钟,所以,如果用date命令修改了系统时钟:
#date 031915232007
MonMar 19 15:23:00 CST 2007
这时关闭系统,时钟信息还是会丢失。如果在上述命令后用hwclock命令写到硬件RTC时钟:
#hwclock --systohc
在系统断电期间RTC时钟由电池来维持,下次启动通过hwclock--hctosys仍然可以将系统时钟设定正确。
5.5网络配置
在bootloader中设置的开发板IP地址仅在bootloader阶段与主机通信时起作用。如果是NFSRoot方式启动,系统启动后的IP地址是由内核启动参数中的NFS设置决定的,这样系统启动后仍然可以读取主机的NFS导出目录。如果是Flash根文件系统启动,则启动之后需要重新配置IP地址和激活网络接口,我们可以把配置写在启动脚本~/rootfs/etc/init.d/rcS中:
ifup-a
并且添加一个配置文件~/rootfs/etc/network/interfaces:
autolo
ifacelo inet loopback
autoeth0
ifaceeth0 inet static
address192.168.1.25
netmask255.255.255.0
gateway192.168.1.125
ifup命令根据配置文件激活网络接口lo(loopback设备)和eth0(以太网接口)。
5.6httpd
busybox自带了一个Web服务器程序httpd,我们在启动脚本~/rootfs/etc/init.d/rcS中添加如下一行:
httpd-h /srv/www
这样httpd在启动时以daemon方式运行,使用默认的80端口(使用-p选项可以自己指定端口)。服务目录是/srv/www,如果浏览器请求/srv/www/cgi-bin目录下的可执行文件,则被httpd当作CGI程序执行。我们在根文件系统中创建服务目录:
~$mkdir -p rootfs/srv/www/cgi-bin
现在启动开发板系统,试验以下功能:
编辑一个index.html放在~/rootfs/srv/www下,从主机上打开浏览器,输入开发板的IP地址看能否浏览。
编辑一个shell脚本,加可执行权限,放在~/rootfs/srv/www/cgi-bin下,从主机打开浏览器,输入该脚本的地址(例如http://192.168.1.25/cgi-bin/script.sh)看能否得到执行结果。脚本如下所示:
#!/bin/sh
echo"Content-Type:text/plain"
echo
ls-a
最终我们的启动脚本~/rootfs/etc/init.d/rcS是这样的:
#!/bin/sh
mount-a
mount-t tmpfs mdev /dev
mdev-s
mkdir/dev/input
ln-s /dev/mice /dev/input/mice
hwclock--hctosys
ifup-a
httpd-h /srv/www
6.参考资料
<BuildingEmbedded Linux Systems>byKarim Yaghmour,O'Reilly出版