最近由于工作上的需要,研究了一下嵌入式系统的开发。

嵌入式系统的大概流程是:Bootloader---->内核---->文件系统。

1、Bootloader

    Bootloader主要负责加载内核,尽管它在系统启动期间执行的时间非常短,不过它却是非常重要的系统组件。在一定程度上,设置Bootloader是所有Linux系统的一项常见工作。尽管如此,对嵌入式系统来说,这却是一件特别的工作,因为将Bootloader用在这类系统中,可能完全不同于将它们用在一般的系统中,即使相同,它们的配置和操作方式也可能相去甚远。由于不同的硬件架构以及基于相同架构的不同电路板之间有很大的差异,所以Bootloader的选用、设置以及配置跟所使用的硬件有很大的关系。Bootloader有很多种,主要有:LILO、GRUB、ROLO、Loadlin、Etherboot、LinuxBIOS、Compaq的bootldr、blob、PMON、sh-boot、U-Boot、RedBoot,其中用于x86架构的是LILO和GRUB,用于ARM架构的是U-Boot,用于PowerPC架构的也是U-Boot。下面详细介绍一下U-Boot,U-Boot被认为是功能最多、最具弹性以及开发最积极的开发源码Bootloader,它支持多种嵌入式操作系统内核(如Linux、NetBSD, VxWorks, QNX, RTEMS, ARTOS, LynxOS),而且支持多种架构的CPU(如PowerPC、ARM、x86、MIPS、XScale)。

    U-Boot可支持的主要功能有:系统引导;支持NFS挂载根文件系统;支持RAMDISK(压缩或非压缩)形式的根文件系统;支持NFS挂载系统内核;支持从FLASH中引导压缩或非压缩系统内核;可灵活设置、传递多个关键参数给操作系统,适合系统在不同开发阶段的调试要求与产品发布,尤其对Linux支持最为强劲;支持多种存储方式,如FLASH、NVRAM、EEPROM;CRC32校验,可校验FLASH中内核、RAMDISK镜像文件是否完好;多种设备驱动支持,如串口、SDRAM、FLASH、以太网、LCD、NVRAM、EEPROM等设备的驱动支持;上电自检功能,比如SDRAM、FLASH大小自动检测,SDRAM故障检测,CPU型号;XIP内核引导。

    U-Boot的启动过程分为两个过程:

    1、 stage1(start.s代码结构)
    U-boot的stage1代码通常放在start.s文件中,它用汇编语言写成,其主要代码部分如下:
  (1) 定义入口。由于一个可执行的image必须有一个入口点,并且只能有一个全局入口,通常这个入口放在rom(Flash)的0x0地址,因此,必须通知编译器以使其知道这个入口,该工作可通过修改连接器脚本来完成。
  (2)设置异常向量(exception vector)。
  (3)设置CPU的速度、时钟频率及中断控制寄存器。
  (4)初始化内存控制器 。
  (5)将rom中的程序复制到ram中。
  (6)初始化堆栈 。
  (7)转到ram中执行,该工作可使用指令ldrpc来完成。
    2、 stage2(C语言代码部分)
  lib_arm/board.c中的start armboot是C语言开始的函数,也是整个启动代码中C语言的主函数,同时还是整个u-boot(armboot)的主函数,该函数主要完成如下操作:
  (1)调用一系列的初始化函数。
  (2)初始化flash设备。
  (3)初始化系统内存分配函数。
  (4)如果目标系统拥有nand设备,则初始化nand设备。
  (5)如果目标系统有显示设备,则初始化该类设备。
  (6)初始化相关网络设备,填写ip,c地址等。
  (7)进入命令循环(即整个boot的工作循环),接受用户从串口输入的命令,然后进行相应的工作。

    U-Boot提供了多种命令,在上电启动时,如果在U-Boot等待时间内按下了任何键,则会进入U-Boot的交互模式,在提示符后输入help,可得到如下命令:

        askenv - get envrionment variables from stdin
        autoscr - run script from memory
        base - print or set address offset
        bdinfo - print board info structure
        bootm - boot applicatioin image from memory
        bootp - boot image via network using BootP/TFTP protocol
        bootd - bood default, i.e. run 'bootcmd'
        cmp - memory compare
        coninfo - print console devices and informations
        cp - memory copy
        crc32 - checksum calculation
        date - get/set/reset date & time
        dhcp - invoke DHCP client to obtain IP/boot params
        diskboot - boot from IDE device
        echo - echo args to console
        erase - erase FLASH memory
        flinso - print FLASH memory information
        go - start application at address 'addr'
        help - print online help
        ide - IDE sub-system
        iminfo - print header information fro application image
        loadb - load binary file over serial line(kermit mode)
        loads - load S-Record file over serial line
        loop - infinite loop on address range
        md - memory display
        mm - memory modify(auto-incrementing)
        mtest - simple RAM test
        mw - memory write(fill)
        nm - memory modify(constant address)
        printenv - print environment variables
        protect - enable or disable FLASH write protection
        rarpboot - boot image via network using RARP/TFTP protocol
        reset - Perform RESET of the CPU
        run - run commands in an environment variable
        saveenv - save environment variables to persistent storage
        setenv - set environment variables
        sleep - delay executin for some time
        tftpboot - boot image via network using TFTP protocol and env variables ipaddr and serverip
        version - print monitor version
        ? - alias for 'help'

    U-Boot还为每个命令提供了辅助说明,输入help command即可,例如输入help cp,会得到如下输出:cp [.b, .w, .l] source target count - copy memory,当U-Boot在命令之后附加[.b, .w, .l]表达式时,表示需要根据后面附加的字符串调用相应的命令版本,cp.b、cp.w和cp.l分别可用来复制byte,word和long类型的数据。


2、内核

    内核是所有Linux系统的中心软件组件。整个系统的能力完全受内核本身能力的限制。例如,倘若你使用的内核无法支持目标板上的某个硬件组件,当在目标板上运行此内核时,该硬件组件将会变得毫无用处。要让目标板取得可用的内核,必须找到专门负责开发相应处理器架构的团队所提供的内核版本。在为目标板建立内核的过程中,配置属于最初的阶段,内核配置的方法有很多,一般使用make menuconfig方法。内核的编译包括三步:建立内核源码的依存关系,建立内核映像,以及建立内核模块,此三个步骤使用的make命令都不同。下面分别说明一下这三步:

    建立依存关系:内核源码树种大多数文件都会与一些头文件有依存关系,要想顺利建立内核,内核源码树立里各个Makefile必须知道这些依存关系。依存关系建立期间会在内核源码树中每个目录里产生一个隐藏的.depend文件。此文件内含子目录里各个文件所依存的头文件清单。如同其他靠make建立的软件,自从上一次完成建立以来,如果要重新建立内核,只有在与头文件有依存关系的文件被改动后才需要经过重新编译。可在内核源码树的根目录,用以下命令来建立内核源码的依存关系(此命令需根据具体环境改变):make ARCH=arm CROSS_COMPILE=arm-linux- clean dep

    建立内核镜像:建立依存关系后,接着编译内核映像:make ARCH=arm CROSS_COMPILE=arm-linux- zImage。zImage整个建立目标用来指示Makefile建立经gzip算法压缩过的内核映像,不过还有其他方法可用来建立内核映像。

    建立模块:内核映像正确建立后,接着建立内核模块:make ARCH=arm CROSS_COMPILE=arm-linux- modules


    如果需要清理内核的源码,让它恢复到配置设定,依存关系加你或编译之前的初始状态,可以使用如下的命令:make ARCH-arm CROSS_COMPILE=arm-linux- distclean,但务必在执行此命令之前将内核的配置文件保存备份起来,因为make distclean会清除前面这几个阶段产生的文件,包括.config文件、所有目标文件以及内核映像。


    内核失败的最常见形式是内核恐慌(panic)。

3、文件系统

    Linux内核在系统启动期间进行的最后操作之一就是安装根文件系统,文件系统的主要工作包括建立根文件系统的基本结构,安装系统链接库、设备节点、主系统应用程序。下面分别进行介绍。

    根文件系统的基本结构

    一般按照Filesystem Hieranchy Standard(FHS)规则建立,顶层目录包括bin、boot、dev、etc、home、lib、mnt、opt、proc、root、sbin、tmp、usr、var。一般为多用户提供可扩展环境的所以目录(例如/home、/mnt、/opt和/root)都应该省略,根据引导加载程序和它的配置情况,可能不需要/boot目录,这取决于引导加载程序是否会在内核被启动之前从根文件取回内核映像。其余的目录,/bin、/dev、/etc、/lib、/proc、/sbin和/usr,都是不可或缺的。

    链接库

    链接库按照目标板的需求在主机建立好后,可根据需要把其安装到根文件系统中。

    设备节点

    首先应该在/dev目录中加入一些基本条目:mem、null、zero、random、tty0、tty1、ttyS0、tty、console,

    建立过程是:

        mknod -m 600  mem c 1 1

        mknod -m 666 null c 1 3

        mknod -m 666 zero c 1 5

        mknod -m 644 random c 1 8

        ...

    除了基本的设备文件,/dev目录中海必须包含若干必要的符号链接,如fd---->/proc/selflfd,stdin---->fd/0,stdout---->fd/1,stderr---->fd/2。

    主系统应用程序

    由于主系统应用程序数量众多,不可能一个一个的进行移植,而BusyBox的出现就是为了解决这个问题的,一般采用Busybox的根文件系统,其主系统的应用程序一般都是Busybox的链接符号。


    内核启动的最后一个动作就是启动init程序,而采用BusyBox的系统中,BusyBox除了提供缺省支持的命令,还能提供与init类似的能力,如同原始的主流init,BusyBox也可以处理系统的启动工作。BusyBox的init尤其适合在嵌入式系统中使用,因为它可以为嵌入式系统提供所需要的大部分init功能,却不会让嵌入式系统被System V init的额外特性拖累。此外,因为BusyBox是单个套件,所以当你要开发或维护系统时,不需要注意额外的软件套件,然而有些时候系统可能不适合使用BusyBox的init,例如它不提供运行级别的支持。因为/sbin/init是/bin/busybox的符号链接,所以BusyBox是目标板系统上执行的第一个应用程序。当BusyBox知道调用它的目的是要执行init,它会立即跳转到init进程。

    BusyBox的init进程会依次进行以下工作:

    1、为init设置信号处理进程

    2、初始化控制台

    3、剖析inittab文件、/etc/inittab文件。

    4、执行系统初始化命令行。BusyBox在缺省情况下会使用/etc/init.d/rcS脚本文件

    5、执行所有会导致init暂停的inittab命令(动作类型:wait)

    6、执行所有仅执行一次的inittab命令(动作类型:once)

    一旦完成以上工作,init进程便会循环执行以下工作:

    1、执行所有终止时必须重新启动的inittab命令(动作类型:respawn)

    2、执行所有终止时必须重新启动但启动前必须先询问用户的inittab命令(动作类型:askfirst)

    在控制台初始化期间,BusyBox会判断系统是否被设成在串行端口上执行控制台。控制台初始化后,BusyBox会检查/etc/inittab文件是否存在。如果此文件不存在,BusyBox会使用缺省的inittab配置。它主要会为系统重引导、系统挂起以及init重启设置缺省的动作。此外,它还会为头四个虚拟控制台(/dev/tty1到/dev/tty4)设置启动shell动作。如果并未建立这些设备文件,BusyBox将会报错。

    如果存在/etc/inittab文件,BusyBox会予以剖析,并将其中的命令记录在内部的数据结构中个,以便适时执行。inittab文件中每一行的格式如下所示:

    id:runlevel:action:process

    尽管此格式与传统的System V init类似,但请注意,id在BusyBox的init中具有不同的意义。对BusyBox而言,id用来指定所启动进程的控制tty。如果所启动的进程不是个可以交互的shell,大可以将可以此字段空着不填。可以交互的shell,例如BusyBox的sh,应该会有个控制tty。如果控制tty不存在,BusyBox的sh将会报错。BusyBox将会完全忽略ruanlevel字段,所以你可以将它空着。process字段用来指定所执行程序的路径,包括命令行选项。action字段用来指定如下所示八个可应用到process的动作之一。

    systinit    为init提供初始化命令行的路径

    respawn    每当相应的进程终止执行便重新启动

    askfirst    类型respawn,不过它的主要用途是减少系统上执行的终端应用程序的数量。它将会促使init在控制台上显示“Please press Enter to activate this console."的信息,并在重新启动进程之前等待用户按Enter键

    wait    告诉init必须等到相应的进程完成后才能继续执行

    once    仅执行相应的进程一次,而且不会等待它完成

    ctrlaltdel    当按下Ctrl-Alt-Delete组合键时,执行相应的进程

    shutdown    当系统关机时,执行相应的进程

    restart    当init重新启动时,执行相应的进程。通常此处所执行的进程就是init本身