嵌入式Linux驱动程序和系统开发实例精讲
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

第1篇 Linux基础知识

第1章 嵌入式基础入门

随着微电子技术的飞速发展及后PC时代的到来,嵌入式芯片被广泛运用到消费、汽车、电子、微控制、无线通信、数码产品、网络设备、安全系统等领域。越来越多的公司、研究单位、大专院校,以及个人开始进行嵌入式系统的研究与应用,嵌入式系统设计将是未来相当长一段时间内电子领域研究的热点。下面首先对嵌入式操作系统进行概述。

1.1 嵌入式操作系统简介

随着嵌入式操作系统及嵌入式处理器技术的发展,嵌入式操作系统已经被广泛应用到大量以嵌入式处理器为硬件基础的系统中,常见的嵌入式操作系统有:Linux、Windows CE、Symbian、Palm和μC/OS-II等。

这些操作系统都各有自己强劲的优势,Linux以其开源的经济优势被广泛应用到很多嵌入式系统中,得到了中小型企业的青睐;Windows CE 有着全球最大的操作系统厂商Microsoft强大的技术后盾,得到了越来越多的市场份额;Symbian操作系统是全球最大的手机研发制造商NOKIA的手机操作系统,被广泛应用于高端智能手机上。在将来相当长的一段时间内,将存在几个操作系统并存发展、齐头并进的情况,但是,经过一段时间的角逐,常用的嵌入式设备所采用的操作系统将会集中到其中的2~3种。

1.1.1 嵌入式系统的基本概念

业界有多种不同的关于嵌入式系统(Embedded System)的定义,被大多数人所接受的是根据嵌入式系统的特点下的定义:它是“以应用为中心、以计算机技术为基础、软件硬件可裁剪,对功能、可靠性、成本、体积、功耗有严格要求的专用计算机系统。”该定义强调软硬件可裁剪、专用计算机系统的特点,这也是嵌入式系统与通用计算机平台最为显著的差别。

由于嵌入式的应用太广泛,因此,这里仅给出未来发展空间最为看好的嵌入式系统特点,即嵌入式系统是一类在硬件上采用专用(相对于通用的X86来说)的高性能处理器(通常为32位),在软件上以一个多任务的操作系统为基础的专用系统。一方面,它与通用的计算机平台有本质的区别(软硬件可裁剪);另一方面,又与以前的单片机有着本质的区别,因为单片机几乎无法使用移植操作系统,而32 位嵌入式处理器设备能够很便捷地移植操作系统。

实时嵌入式系统也称为实时系统,它反映了嵌入式系统对时间响应要求较高的特点,即如果逻辑和时序出现偏差将会引起严重后果。常见的实时系统有两种类型,即软实时系统和硬实时系统,它们各自任务要求如下。

● 软实时系统。系统的宗旨是使各个任务运行得越快越好,但并不要求限定某一任务必须在多长时间内完成。

● 硬实时系统。各任务不仅要执行无误,而且要做到准时,例如:火星车。

在实际应用中,大多数实时系统是以上二者的结合。常见的实时操作系统分为以下3类。

● 具有强实时特点的操作系统。系统响应时间在毫秒或者微秒级(如数控机床);

● 一般实时特点的操作系统。系统响应时间在毫秒到几秒的数量级上(如电子点菜机);

● 弱实时特点的操作系统。系统响应时间约数十秒以至更长时间(如MP3系统)。

下面列出部分实时操作系统所具有的特点。

(1)高效的任务管理。实时操作系统支持多任务、优先级管理和任务调度,其中任务调度是基于优先级的抢占式调度,并采用时间片轮转调度的算法。

(2)快速灵活的任务间通信。实时操作系统的通信机制采用消息队列和管道等技术,有效地保障快速灵活的任务间通信。

(3)高度的可裁剪性。实时操作系统的系统功能可针对需求对软件进行裁剪、调整。

(4)便捷地实现动态链接与部件增量加载。

(5)快速有效地实现中断和异常事件处理。

(6)动态内存管理。

1.1.2 嵌入式系统的内核介绍

(1)内核(Kernel):多任务系统中,内核负责管理各个任务,或者说为每个任务分配CPU时间,并且负责任务之间的通信。内核提供的基本服务是任务切换。使用实时内核可以大大简化应用系统的设计的原因在于,实时内核允许将应用分成若干个任务,由实时内核来管理它们。内核本身也增加了应用程序的额外负荷,代码空间增加ROM的用量,内核本身的数据结构增加了RAM的用量。但更主要的是,每个任务要有自己的栈空间,这一块消耗起内存来是相当厉害的。内核本身对CPU的占用时间的比例一般是2%~5%。

单片机一般不能运行实时内核,因为单片机的RAM很有限。实时内核通过提供必不可少的系统服务,如信号量管理、邮箱、消息队列、延时等,使得CPU的利用更为有效。一旦用户用实时内核做过系统设计,将绝不再想返回到前后台系统。

(2)调度(Scheduler):这是内核的主要职责之一,决定该轮到哪个任务运行了。多数实时内核是基于优先级调度的。每个任务根据其重要程度的不同被赋予不同的优先级。基于优先级的调度是指 CPU 总是让处在就绪态的优先级最高的任务先运行。然而,究竟何时让高优先级任务掌握 CPU 的使用权,就要看用的是什么类型的内核,是不可剥夺型的还是可剥夺型内核。

(3)可剥夺型内核:当系统响应时间很重要时,要使用可剥夺型内核。因此,μC/OS-Ⅱ及绝大多数商业上销售的实时内核都是可剥夺型内核。最高优先级的任务一旦就绪,总能得到 CPU 的控制权。当一个运行着的任务使一个比它优先级高的任务进入了就绪态,当前任务的 CPU 使用权就被剥夺了,或者说被挂起了,那个高优先级的任务立刻得到了CPU的控制权。如果是中断服务子程序使一个高优先级的任务进入就绪态,中断完成时,中断了的任务被挂起,优先级高的那个任务开始运行,如图1-1所示。

图1-1 可剥夺型内核任务管理示意图

使用可剥夺型内核,最高优先级的任务什么时候可以执行,可以得到 CPU 的控制权是可知的。使用可剥夺型内核使得任务级响应时间得以最优化。

使用可剥夺型内核时,应用程序不应直接使用不可重入型函数。调用不可重入型函数时,要满足互斥条件,这一点可以用互斥型信号量来实现。当调用不可重入型函数时,低优先级的任务 CPU 的使用权被高优先级任务剥夺,不可重入型函数中的数据将有可能被破坏。由此可见,可剥夺型内核总是让就绪态的高优先级的任务先运行,中断服务程序可以抢占CPU,到中断服务完成时,内核让此时优先级最高的任务运行(不一定是那个被中断了的任务)。这样,任务级系统响应时间得到了最优化,并且是可知的。μC/OS-Ⅱ属于可剥夺型内核。

(4)死锁(Deadly Embrace):死锁也称做抱死,指两个任务无限期地互相等待对方控制着的资源。设任务T1正独享资源R1,任务T2在独享资源R2,而此时T1又要独享R2, T2也要独享R1,于是哪个任务都没法继续执行了,发生了死锁。最简单的防止发生死锁的方法是让每个任务都具备以下特征。

● 先得到全部需要的资源再做下一步的工作;

● 用同样的顺序去申请多个资源;

● 释放资源时使用相反的顺序。

内核大多允许用户在申请信号量时定义等待超时,以此化解死锁。当等待时间超过了某一确定值,信号量还是无效状态,就会返回某种形式的出现超时错误的代码,这个出错代码告知该任务,不是得到了资源使用权,而是系统错误。死锁一般发生在大型多任务系统中,在嵌入式系统中不易出现。

1.1.3 嵌入式系统的应用领域

嵌入式系统的应用很广泛,可以这样说,除了通用的计算机系统应用外,其他所有的智能电子设备都属于嵌入式系统。以下简要列出嵌入式系统在数字家电、个人数据处理、通用技术等领域的应用。

嵌入式系统在数字家电领域的应用。当前家庭电子设备中包含了越来越多的嵌入式处理器产品,以下电子设备都属于嵌入式系统:IP电话、PDA、无线音频系统、在线游戏、无线接入设备、微波炉、电冰箱、洗衣机、电视、收音机、CD 播放器、个人电脑、遥控开关。一般来说,每个家庭拥有至少多于20 种以上的电子设备。据有关调查表明,预计2010 年后每个城市家庭都将基本实现电子化,那时每个人所使用的全部设备中包含的MCU数量将会超过100个!

基于嵌入式的多用途PDA(Personal Digital Assistant)解决方案。随着各行各业对信息化要求的日益提高,行业用户对PDA的需求量越来越大,同时对PDA的功能要求也越来越高,而且不同的行业用户所需的功能要求也不同,将呈现多元化和个性化趋势。采用嵌入式系统的智能化多功能 PDA 终端平台可以提供个性化定制业务,可定制与所在行业相对应的专用功能,并可适应不同环境下的功能要求。基于嵌入式的多用途 PDA 可以广泛运用于军警用设备、信息查询、服务行业、石油、地质、电力、水利、GPRS 应用(上网、通话、短信等)、GPS定位、指纹识别技术、GIS(地理信息系统)应用、IC卡应用、蓝牙技术、CCD摄像处理、条形码识别、红外、USB传输、多媒体、MP3等领域。

嵌入式系统在通信领域的应用。根据市场调查,基于 ARM 处理器的嵌入式系统在GSM/UMTS 市场(GSM850、900、1800、1900、GPRS、EDGE、UMTS)中将超过85%的市场占有率,并主要为OEM(Original Equipment Manufacturer)客户,在CDMA系统(IS95A/B、CDMA2000 1X、EV-DO、BREW等)中将超过99%的市场占有率,在Bluetooth系统中将超过75%的市场占有率。

1.2 Linux操作系统概述

1.2.1 嵌入式Linux发展现状

UNIX操作系统于1969年由Ken Thompson在AT&T贝尔实验室的一台DEC PDP-7计算机上实现。后来Ken Thompson和Dennis Ritchie使用C语言对整个系统进行了再加工和编写,使得UNIX能够很容易地移植到其他硬件的计算机上。由于此时AT&T还没有把UNIX 作为它的正式商品,因此研究人员只是在实验室内部使用并完善它。也正是由于UNIX 被作为研究项目,其他科研机构和大学的计算机研究人员也希望能得到这个系统,以便进行自己的研究。AT&T以分发许可证的方法,对UNIX仅仅收取很少的费用,UNIX的源代码就被散发到各个大学,使得科研人员能够根据需要改进系统,或者将其移植到其他的硬件环境中去;另一方面,也培养了大量懂得UNIX、使用UNIX和编程的学生,这使得UNIX的普及更为广泛。

到了20世纪70年代末,在UNIX发展到了版本6之后,AT&T认识到了UNIX的价值,成立了UNIX系统实验室(UNIX System Lab,USL)来继续发展UNIX。此时,AT&T一方面继续发展内部使用的UNIX版本7,一方面由USL开发对外正式发行的UNIX版本,同时AT&T也宣布对UNIX产品拥有所有权。几乎同时,加州大学伯克利分校计算机系统研究小组(CSRG)使用UNIX对操作系统进行研究,他们对UNIX的改进相当多,增加了很多当时非常先进的特性,包括更好的内存管理,快速且健壮的文件系统等,大部分原有的源代码都被重新写过,很多其他UNIX使用者,包括其他大学和商业机构,都希望能得到CSRG改进的UNIX系统。也正因为如此,CSRG中的研究人员把他们的UNIX组成一个完整的UNIX系统——BSD UNIX(Berkeley Software Distribution)向外发行。

而AT&T的UNIX系统实验室同时也在不断改进它们的商用UNIX版本,直到它们吸收了BSD UNIX中已有的各种先进特性,并结合其本身的特点,推出了UNIX System V版本。从此以后,BSD UNIX和UNIX System V形成了当今UNIX的两大主流,目前的UNIX版本大部分都是这两个版本的衍生产品:IBM 的 AIX4.0、HP/UX11、SCO 的 UNIXWare等属于System V,而Minix、freeBSD、NetBSD、OpenBSD等属于BSD UNIX。

Linux从一开始,就决定自由扩散Linux,包括源代码也发布在网上,随即就引起爱好者的注意,他们通过互联网也加入了Linux的内核开发工作,一大批高水平程序员的加入,使得Linux得到迅猛发展。1993年年底,Linux 1.0终于诞生。Linux 1.0已经是一个功能完备的操作系统了,其内核写得紧凑高效,可以充分发挥硬件的性能,在4MB内存的80386机器上也表现得非常好。

Linux加入GNU并遵循公共版权许可证(GPL)。由于不排斥商家对自由软件的进一步开发,不排斥在Linux上开发商业软件,故而使Linux又开始了一次飞跃,出现了很多的Linux发行版,如Slackware、Redhat、TurboLinux、OpenLinux等10多种,而且还在增加,还有一些公司在Linux上开发商业软件或把其他UNIX平台的软件移植到Linux上来。如今很多IT界的大腕,如IBM、Intel、Oracle、Infomix、Sysbase、Netscape、Novell等都宣布支持Linux。商家的加盟弥补了纯自由软件的不足和发展障碍,Linux得以迅速普及。

Linux由UNIX操作系统的发展而来,它的内核由Linus Torvalds及网络上组织松散的黑客队伍一起从零开始编写而成。Linux的目标是保持和POSIX的兼容。Linux操作系统具有以下特点。

● Linux具备现代一切功能完整的UNIX系统所具备的全部特征,其中包括真正的多任务、虚拟内存、共享库、需求装载、共享的写时复制程序执行、优秀的内存管理,以及TCP/IP网络支持等。

● Linux的发行遵守GNU的通用公共许可证(GPL)。

● 在原代码级上兼容绝大部分的UNIX标准(如IEEE POSIX、System V、BSD),它遵从POSIX规范。读者可以在http://www.linuxresources.com/what.htmlhttp://www.linux.org得到更多的信息。

1.2.2 Linux相关的常用术语

1.POSIX及其重要地位

POSIX表示可移植操作系统接口(Portable Operating System Interface,POSIX)。由电气和电子工程师协会(Institute of Electrical and Electronics Engineers,IEEE)开发,主要为了提高UNIX环境下应用程序的可移植性。然而,POSIX并不局限于UNIX,许多其他的操作系统例如DEC OpenVMS和Microsoft Windows NT,都支持POSIX标准,尤其是IEEE STD.1003.1-1990(1995年修订)或POSIX.1。POSIX.1提供了源代码级别的C语言应用编程接口(API)给操作系统的服务程序,例如读写文件。POSIX.1 已经被国际标准化组织(International Standards Organization,ISO)所接受,被命名为ISO/IEC9945- 1:1990标准。现在POSIX已经发展成为一个非常庞大的标准族,某些部分正处在开发过程中。

2.GNU和Linux的关系

GNU是GNU Is Not UNIX的递归缩写,是自由软件基金会的一个项目,该项目的目标是开发一个自由的UNIX版本,这一UNIX版本称为HURD。尽管HURD尚未完成,但GNU项目已经开发了许多高质量的编程工具,包括emacs编辑器、著名的GNU C和C++编译器(gcc和g++),这些编译器可以在任何计算机系统上运行。所有的GNU软件和派生工作均适用GNU通用公共许可证,即GPL。GPL允许软件作者拥有软件版权,但同时授予其他任何人以合法复制、发行和修改软件的权利。

Linux的开发使用了许多GNU工具。Linux系统上用于实现POSIX.2标准的工具几乎都是GNU项目开发的,如Linux内核、GNU工具,以及其他一些由软件组成的人们常说的Linux——C语言编译器和其他开发工具及函数库、X Window窗口系统、各种应用软件(包括字处理软件、图像处理软件等)、其他各种Internet软件(包括FTP服务器、WWW服务器)、关系数据库管理系统等。

3.GPL(General Public License)公共许可协议

GPL的文本保存在Linux系统的不同目录下的命名为COPYING的文件里。例如,键入cd/usr/doc/ghostscript*然后再键入more COPYING可查看GPL的内容。GPL与软件是否免费无关,它的主要目标是保证软件对所有的用户来说是自由的。GPL通过如下途径实现这一目标。

● 要求软件以源代码的形式发布,并规定任何用户能够以源代码的形式将软件复制或发布给别的用户。

● 提醒每个用户,对于该软件不提供任何形式的担保。

● 如果用户的软件使用了受GPL保护的任何软件的一部分,那么该软件就成为GPL软件,也就是说必须随应用程序一起发布源代码。

● GPL并不排斥对自由软件进行商业性质的包装和发行,也不限制在自由软件的基础上打包发行其他非自由软件。

● 遵照GPL的软件并不是可以任意传播的,这些软件通常都有正式的版权,GPL在发布软件或者复制软件时声明限制条件。但是,从用户的角度考虑,这些根本不能算是限制条件,相反用户只会从中受益,因为用户可以确保获得源代码。

尽管Linux内核也属于GPL范畴,但GPL并不适用于通过系统调用而使用内核服务的应用程序,通常把这种应用程序看做是内核的正常使用。假如准备以二进制的形式发布应用程序(像大多数商业软件那样),则必须确保自己的程序未使用GPL保护的任何软件。如果软件通过库函数调用而使用了别的软件,则不必受到这一限制。大多数函数库受另一种GNU公共许可证即LGPL的保护,将在下面介绍。

4.LGPL(Libraray General Public License)程序库公共许可证

GNU LGPL的内容全部包括在命名为COPYING.LIB的文件中。如果安装了内核的源程序,在任意一个源程序的目录下都可以找到COPYING.LIB文件的一个拷贝。

即使在不公开自己源程序的情况下,LGPL 也允许在自己的应用程序中使用程序库。但是,LGPL 还规定用户必须能够获得在应用程序中使用的程序库的源代码,并且允许用户对这些程序库进行修改。

由于大多数Linux程序库,包括C程序库(libc.a)都属于LGPL范畴,因此,如果在Linux环境下使用GCC编译器建立自己的应用程序,程序所链接的多数程序库是受LGPL保护的。如果想以二进制的形式发布自己的应用程序,则必须注意遵循LGPL的有关规定。

遵循LGPL的一种方法是随应用程序一起发布目标代码,并可以发布将这些目标程序与受LGPL保护的、更新的Linux程序库链接起来的makefile文件。

遵循LGPL的比较好的一种方法是使用动态链接。使用动态链接时,即使程序在运行中调用函数库中的函数时,应用程序本身和函数库也是不同的实体。通过动态链接,用户可以直接使用更新后的函数库,而不用对应用程序进行重新链接。

在GPL的保护范围以外,也有GNU dbm和GNU bison的相应的替代程序。例如,对于数据库类的程序库,可以使用Berkeley数据库db来替代gdbm,对于分析器生成器,可以使用yacc来替代bison。

1.3 Linux操作系统的移植

Linux操作系统是一个完全开源的操作系统,用户可以自己下载、阅读、修改并重新编译内核,从而使开发人员能够完全自己定制相关的操作系统功能以适合自己的需要。本节将就以下内容作详细介绍。

BootLoader程序:BootLoader是一个用来初始化嵌入式硬件最小系统,进而引导操作系统的底层程序,其主要代码由汇编语言和C程序编写。在X86上常见的BootLoader有GRUB和LILO,在嵌入式设备中U-boot和VIVI用得比较多。

Linux源代码分开,读者可以在相关网站上下载这些源代码。随着 Linux的发展,目前2.6版内核的Linux源代码已经超过30MB。1.3.2小节将详细介绍Linux源代码目录结构,从而为读者快速阅读Linux内核程序提供参考。

1.3.3小节将详细介绍如何重新编译适合嵌入式ARM处理的Linux内核程序的过程,主要包括如何剪裁Linux内核源程序。

读者通过对本节的学习,将对Linux内核源代码有一个比较清楚的认识,能够独立裁剪Linux内核,并移植Linux内核到ARM处理器中运行。

1.3.1 BootLoader技术详解

在专用的嵌入式板子上运行GNU/Linux系统已经变得越来越流行。一个嵌入式系统从软件角度大致可以分为四个层次(注:有些系统中并没有严格划分),如图1-2所示。

图1-2 嵌入式系统软件层次

(1)引导加载程序。包括固化在固件(firmware)中的boot代码(可选,并不是所有系统都有)和BootLoader程序两个部分。

(2)实时操作系统。特定嵌入式板子的定制内核以及内核的启动参数。

(3)文件系统。包括根文件系统和建立于 Flash内存设备之上的文件系统。通常有RAMFS、ROMFS、JAFFS、YAFFS等文件系统。

(4)上层用户应用程序。有时在用户应用程序和内核层之间可能还会包括一个嵌入式图形用户界面。常用的嵌入式GUI有QT和MiniGUI。

引导加载程序是系统加电后运行的第一段软件代码。例如X86体系结构中,系统的引导加载程序由BIOS(其本质就是一段固件程序)和位于硬盘MBR中的OS BootLoader(比如LILO和GRUB等)一起组成。BIOS在完成硬件检测和资源分配后,将硬盘MBR中的BootLoader读到系统的RAM中,然后将控制权交给OS BootLoader。BootLoader的主要运行任务就是将内核映像从硬盘上读到RAM中,然后跳转到内核的入口点去运行,即开始启动操作系统。而在嵌入式系统中,通常并没有像BIOS那样的固件程序(注:有的嵌入式处理器也会内嵌一段短小的启动程序),因此整个系统的加载启动任务就完全由BootLoader来完成。

1.BootLoader概述

BootLoader是在嵌入式实时操作系统内核启动之前运行的一段小程序。其主要完成初始化硬件设备,建立内存空间的映射图,设置系统的软硬件环境,为最终调用操作系统内核准备好正确的环境。

通常BootLoader是严重依赖于硬件而实现的,因此,建立一个通用的BootLoader几乎是不可能的。每种不同的CPU体系结构都有不同的BootLoader,部分BootLoader也支持多种体系结构的CPU,比如U-Boot可以支持ARM体系结构和MIPS体系结构。

除了依赖于 CPU 的体系结构外,BootLoader同样依赖于具体的嵌入式板级设备的配置。因此,即使基于同一种CPU而构建的嵌入式硬件环境,要想让BootLoader程序互换使用,通常也都需要修改BootLoader源程序。

系统加电或复位后,所有的CPU通常都从某个由CPU制造商预先安排的地址上取指令。基于CPU构建的嵌入式系统通常都有某种类型的固态存储设备(比如ROM、EEPROM或 Flash等)被映射到这个预先安排的地址上。因此在系统加电后,CPU 将首先执行BootLoader程序。图1-3为一个固态存储设备的典 空间分配结构图。

图1-3 固态存储设备的典型空间分配结构

大多数BootLoader都包含两种不同的操作模式:“启动加载”模式和“下载”模式。

启动加载(Bootloading)模式:这种模式也称为“自主”模式。BootLoader从目标机上的某个固态存储设备上将操作系统加载到RAM中运行,整个过程并没有用户的介入。这种模式是BootLoader的正常工作模式,在嵌入式产品发布时,BootLoader必须工作在这种模式下。

下载(Downloading)模式:在这种模式下,目标机上的BootLoader将通过串口连接或网络连接等通信手段从主机(Host)下载文件,比如下载内核映像和根文件系统映像等。从主机下载的文件通常首先被BootLoader保存到目标机的RAM中,然后再被BootLoader写到目标机上的Flash类固态存储设备中。BootLoader的这种模式通常在第一次安装内核与根文件系统时被使用;此外,以后的系统更新也会使用BootLoader的这种工作模式。工作于这种模式下的BootLoader通常都会向它的终端用户提供一个简单的命令行接口。

一般的BootLoader通常同时支持这两种工作模式,而且允许用户在这两种工作模式之间进行切换。比如,VIVI在启动时处于正常的启动加载模式,但是它会延时几秒等待终端用户按下任意键而将 VIVI 切换到下载模式。如果在等待时间内没有用户按键,则继续启动Linux内核。

BootLoader的启动过程有单阶段(Single阶段)和多阶段(Multi-阶段)两种,通常多阶段的BootLoader能提供更为复杂的功能,以及更好的可移植性。从固态存储设备上启动的BootLoader大多都是2阶段的启动过程,即启动过程可以分为阶段1和阶段2两部分。

2.BootLoader运行流程

从操作系统的角度看,BootLoader的主要作用就是正确加载操作系统内核。大多数BootLoader都分为阶段1和阶段2两大部分。依赖于CPU体系结构的代码通常都放在阶段1中,而且通常用汇编语言来实现,以达到短小精悍的目的。阶段2部分通常用C语言来实现,这样可以实现复杂的功能,而且代码会具有更好的可读性和可移植性。

BootLoader的阶段1通常包括以下步骤(以执行的先后顺序)。

● 硬件设备初始化。

● 为加载BootLoader的阶段2准备RAM空间。

● 将BootLoader的阶段2复制到RAM空间中。

● 设置堆栈空间。

● 跳转到阶段2的C入口点。

BootLoader的阶段2通常包括以下步骤(以执行的先后顺序)。

● 初始化本阶段要使用到的硬件设备。

● 检测系统内存映射情况。

● 将kernel映像和根文件系统映像从Flash上读到RAM空间中。

● 为内核设置启动参数。

● 调用内核。

(1)阶段1启动流程

● 基本的硬件初始化:这是 BootLoader一开始就执行的操作,其目的是为阶段2 的执行及kernel的执行准备好一些基本的硬件环境。它通常包括以下步骤。

屏蔽所有的外部中断。为中断提供服务通常是OS设备驱动程序的责任,因此在BootLoader的执行全过程中可以不必响应任何外部中断。中断屏蔽可以通过设置CPU的中断屏蔽寄存器或状态寄存器(比如ARM的CPSR寄存器)来完成。

设置CPU的速度和时钟频率。

RAM空间初始化。包括正确地设置系统的内存控制器的功能寄存器及各内存库控制寄存器等。

初始化某一简单的典型外部接口,表明系统运行正常,通常通过初始化 UART向串口打印BootLoader的信息。另外,可以用GPIO来驱动LED,其目的是表明系统的状态是OK还是Error。

关闭CPU内部指令/数据高速缓存(cache)。

● 为加载阶段2准备RAM空间:为了获得更快的执行速度,通常把阶段2加载到RAM空间中来执行,因此必须为加载BootLoader的阶段2准备好一段可用的RAM空间范围。由于阶段2 通常是 C 语言执行代码,因此在考虑空间大小时,除了阶段2可执行映像文件的大小外,还必须把堆栈空间也考虑进来。

此外,空间大小最好是memorypage大小的倍数。一般而言,1MB的RAM空间已经足够了。具体的地址范围可以任意安排,比如blob就将它的阶段2可执行映像安排到从系统RAM起始地址0xc0200000开始的1MB空间内执行。但是,将阶段2安排到整个RAM空间的最顶1MB(即RamEnd-1MB)是一种值得推荐的方法。

● 将阶段2代码复制到RAM中。复制时要确定两点:(1)阶段2的可执行映像在固态存储设备的存放起始地址和终止地址;(2)RAM空间的起始地址。

● 设置堆栈指针SP:堆栈指针的设置是为了执行C语言代码作好准备。通常可以把SP的值设置为阶段2_end-4。经过上述这些执行步骤后,系统的物理内存布局应该如图1-4所示。

图1-4 物理内存布局

● 跳转到阶段2的C入口点:在上述一切都就绪后,就可以跳转到BootLoader的阶段2去执行了。

(2)阶段2启动流程

阶段2的代码通常用C语言来实现,以便于实现更复杂的功能,并取得更好的代码可读性和可移植性。但是与普通 C 语言应用程序不同的是,在编译和链接 BootLoader这样的程序时,不能使用glibc库中的任何支持函数。这就带来一个问题,那就是从哪里跳转进main()函数呢?直接把main()函数的起始地址作为整个阶段2执行映像的入口点或许是最直接的想法。但是这样做有两个缺点。

无法通过main()函数传递函数参数;

无法处理main()函数返回的情况。

一种更为巧妙的方法是利用汇编语言写一段启动代码作为阶段2可执行映像的执行入口点。然后用CPU跳转指令跳入main()函数中去执行。

初始化本阶段要使用到的硬件设备通常包括:

初始化至少一个串口,以便和终端用户进行I/O输出信息;

初始化计时器等。

● 检测系统的内存映射(memorymap)

所谓内存映射就是指在整个4GB 物理地址空间中有哪些地址范围被分配用来寻址系统的RAM单元。

可以用如下数据结构来描述RAM地址空间中的一段连续(continuous)的地址范围。

        typedefstructmemory_area_struct{
        u32start;           /* the base address of the memory region */
        u32 size;           /* the byte number of the memory region */
        int used;
        } memory_area_t;

这段RAM地址空间中的连续地址范围可以处于下面两种状态之一。

used=1,说明这段连续的地址范围已被实现,即真正地被映射到RAM单元上。

used=0,说明这段连续的地址范围并未被系统所实现,而是处于未使用状态。

基于上述memory_area_t数据结构,整个 CPU 预留的 RAM 地址空间可以用一个memory_area_t类型的数组来表示,如下所示。

        memory_area_t memory_map[NUM_MEM_AREAS] =
        {
        [0 ... (NUM_MEM_AREAS - 1)] =
        {
        .start = 0,
        .size = 0,
        .used = 0
        },
        };
        下面给出一个可用来检测整个RAM地址空间内存映射情况的简单而有效的算法。
        /*数组初始化*/
        for(i=0;i<NUM_MEM_AREAS;i++)
        memory_map[i].used=0;
        /* first write a 0 to all memory locations */
        for(addr=MEM_START;addr<MEM_END;addr+=PAGE_SIZE)
        *(u32*)addr=0;
        for(i=0,addr=MEM_START;addr<MEM_END;addr+=PAGE_SIZE){
        /*
        *检测从基地址MEM_START+i*PAGE_SIZE开始,大小为
        *PAGE_SIZE的地址空间是否是有效的RAM地址空间。
        */
        调用3.1.2节中的算法test_mempage()。
        if(currentmemory page isnot a valid ram page)
        {
        /*noRAM here */
        if(memory_map[i].used )
            i++;
        continue;
        }
            /*当前页已经是一个被映射到 RAM 的有效地址范围* 但是还要看看当前页是否只是4GB 地
            址*//*空间中某个地址页的别名?*/
        if(* (u32 *)addr != 0) { /* alias? */
        /* 这个内存页是4GB 地址空间中某个地址页的别名 */
        if ( memory_map[i].used )
        i++;
        continue;
        }
        /*
        * 当前页已经是一个被映射到 RAM 的有效地址范围
        * 而且它也不是4GB 地址空间中某个地址页的别名。
        */
        if (memory_map[i].used == 0) {
        memory_map[i].start = addr;
        memory_map[i].size = PAGE_SIZE;
        memory_map[i].used = 1;
        } else {
        memory_map[i].size += PAGE_SIZE;
        }
        }   /* end of for (…) */

在用上述算法检测完系统的内存映射情况后,Boot Loader也可以将内存映射的详细信息打印到串口。

● 加载内核映像和根文件系统映像

规划内存占用的布局包括两个方面:(1)内核映像所占用的内存范围;(2)根文件系统所占用的内存范围。在规划内存占用的布局时,主要考虑基地址和映像的大小两方面。对于内核映像,一般将其复制到从(MEM_START+0x8000)这个基地址开始的大约1MB的内存范围内(嵌入式Linux的内核一般都不超过1MB)。为什么要把从MEM_START到MEM_START+0x8000这段32KB大小的内存空出来呢?这是因为Linux内核要在这段内存中放置一些全局数据结构,如启动参数和内核页表等信息。而对于根文件系统映像,则一般将其复制到MEM_START+0x0010,0000开始的地方。如果用Ramdisk作为根文件系统映像,则其解压后的大小一般是1MB。

由于像ARM这样的嵌入式CPU通常都是在统一的内存地址空间中寻址Flash等固态存储设备的,因此从 Flash上读取数据与从 RAM 单元中读取数据并没有什么不同。用一个简单的循环就可以完成从Flash设备上复制映像的工作。

        while(count) {
        *dest++ = *src++;       /* they are all aligned with word boundary */
        count -= 4;             /* byte number */
        };

● 设置内核的启动参数

应该说在将内核映像和根文件系统映像复制到 RAM 空间后,就可以准备启动 Linux内核了。但是在调用内核之前,应该做一步准备工作,即设置Linux内核的启动参数。Linux 2.4.x以后的内核都期望以标记列表(tagged list)的形式来传递启动参数。启动参数标记列表以标记ATAG_CORE开始,以标记ATAG_NONE结束。每个标记由标识被传递参数的tag_header结构及随后的参数值数据结构组成。数据结构tag和tag_header定义在Linux内核源代码的include/asm/setup.h头文件中。

        /* The list ends with an ATAG_NONE node. */
        #define ATAG_NONE 0x00000000
        struct tag_header {
        u32 size;                   /* 注意,这里size是以字数为单位的 */
        u32 tag;
        };
        ……
        struct tag {
        struct tag_header hdr;
        union {
            struct tag_core core;
            struct tag_mem32 mem;
            struct tag_videotext videotext;
            struct tag_ramdisk ramdisk;
            struct tag_initrd initrd;
            struct tag_serialnr serialnr;
            struct tag_revision revision;
            struct tag_videolfb videolfb;
            struct tag_cmdline cmdline;
                    /*  Acorn specific */
            struct tag_acorn acorn;
                /*  DC21285 specific */
        struct tag_memclk memclk;
        } u;
        };

在嵌入式Linux系统中,通常需要由Boot Loader设置的常见启动参数有ATAG_CORE、ATAG_MEM、ATAG_CMDLINE、ATAG_RAMDISK、ATAG_INITRD等。

比如,设置ATAG_CORE的代码如下。

        params = (struct tag *)BOOT_PARAMS;
        params->hdr.tag = ATAG_CORE;
        params->hdr.size = tag_size(tag_core);
        params->u.core.flags = 0;
        params->u.core.pagesize = 0;
        params->u.core.rootdev = 0;
        params = tag_next(params);

其中,BOOT_PARAMS表示内核启动参数在内存中的起始基地址,指针params是一个struct tag类型的指针。宏tag_next()将以指向当前标记的指针为参数,计算紧邻当前标记的下一个标记的起始地址。注意,内核的根文件系统所在的设备ID就是在这里设置的。下面是设置内存映射情况的示例代码。

        for(i = 0; i < NUM_MEM_AREAS; i++) {
        if(memory_map[i].used) {
        params->hdr.tag = ATAG_MEM;
        params->hdr.size = tag_size(tag_mem32);
        params->u.mem.start = memory_map[i].start;
        params->u.mem.size = memory_map[i].size;
        params = tag_next(params);
        }
        }

可以看出,在memory_map[]数组中,每一个有效的内存段都对应一个ATAG_MEM参数标记。Linux内核在启动时可以以命令行参数的形式来接收信息,利用这一点可以向内核提供那些内核不能自己检测的硬件参数信息,或者重载(override)内核自己检测到的信息。比如,用这样一个命令行参数字符串“console=ttyS0,115200n8”来通知内核以ttyS0作为控制台,且串口采用“115200bps、无奇偶校验、8位数据位”这样的设置。下面是一段设置调用内核命令行参数字符串的示例代码。

        char *p;
        /* eat leading white space */
        for(p = commandline; *p == ' '; p++)
        ;
        /* skip non-existent command lines so the kernel will still
        * use its default command line.
        */
        if(*p == '\0')
        return;
        params->hdr.tag = ATAG_CMDLINE;
        params->hdr.size = (sizeof(struct tag_header) + strlen(p) + 1 + 4) >> 2;
        strcpy(params->u.cmdline.cmdline, p);
        params = tag_next(params);

下面是设置ATAG_INITRD的示例代码,它告诉内核在RAM中的什么地方可以找到initrd映像(压缩格式)及它的大小。

        params->hdr.tag = ATAG_INITRD2;
        params->hdr.size = tag_size(tag_initrd);
        params->u.initrd.start = RAMDISK_RAM_BASE;
        params->u.initrd.size = INITRD_LEN;
        params = tag_next(params);

下面是设置ATAG_RAMDISK的示例代码,它告诉内核解压后的Ramdisk有多大(单位是KB)。

        params->hdr.tag = ATAG_RAMDISK;
        params->hdr.size = tag_size(tag_ramdisk);
        params->u.ramdisk.start = 0;
        params->u.ramdisk.size = RAMDISK_SIZE; /* 请注意,单位是KB */
        params->u.ramdisk.flags = 1; /* automatically load ramdisk */
        params = tag_next(params);

最后,设置ATAG_NONE标记,结束整个启动参数列表。

        static void setup_end_tag(void)
        {
        params->hdr.tag = ATAG_NONE;
        params->hdr.size = 0;
        }

● 调用内核

Boot Loader调用Linux内核的方法是直接跳转到内核的第一条指令处,即直接跳转到MEM_START+0x8000地址处。

3.常见BootLoader程序

(1)VIVI

VIVI是专用于ARM9处理器的BootLoader程序,其主要目录结构如下。

        drwxr-xr-x      5 root      root        4096 2005-08-12  arch
        -rw-r--r--      1 root      root        18008 2004-08-05  COPYING
        drwxr-xr-x      2 root      root        4096 2005-08-12  CVS
        drwxr-xr-x      4 root      root        4096 2005-08-12  Documentation
        drwxr-xr-x      5 root      root        4096 2005-08-12  drivers
        drwxr-xr-x      6 root      root        4096 2005-08-12  include
        drwxr-xr-x      3 root      root        4096 2005-08-12  init
        drwxr-xr-x      4 root      root        4096 2005-08-12  lib
        -rw-r--r--      1 root      root        5680 2004-08-05  Makefile
        -rw-r--r--      1 root      root        5547 2004-08-05  Makefile.newSDK
        -rw-r--r--      1 root      root        4348 2004-08-05  Rules.make
        drwxr-xr-x      4 root      root        4096 2005-08-12  scripts

其中各目录内容如下:

arch文件夹主要包括vivi所支持的硬件目标开发板,如S3C2410;

drivers文件夹主要包括引导内核所需要的外部设备驱动程序,其下有MTD文件夹,为MTD设备的驱动程序。

init文件夹为main.c文件所在位置,这是程序的主函数入口。

lib文件夹是一些公共平台的接口代码。

include文件夹为所有头文件位置。

(2)U-Boot

U-Boot是在PPC-Boot的基础上发展起来的一个开源的嵌入式BootLoader程序,其理念是成为嵌入式设备标准的BootLoader程序,其主要目录结构如下。

        [root@yangzongde u-boot-1.1.1]# ls -l
        总用量1276
        drwxr-xr-x  139 yangzongde yangzongde   4096 2004-08-31     board
        -rw-r--r--  1 yangzongde yangzongde     82401 2002-08-15    CHANGELOG
        drwxr-xr-x  2 yangzongde yangzongde     4096 2005-06-28     command
        -rw-r--r--  1 yangzongde yangzongde     5096 2002-08-15     config.mk
        -rw-r--r--  1 yangzongde yangzongde     15127 2002-08-15    COPYING
        drwxr-xr-x  25 yangzongde yangzongde    4096 2002-08-15     cpu
        -rw-r--r--  1 yangzongde yangzongde     8122 2002-08-15     CREDITS
        drwxr-xr-x  2 yangzongde yangzongde     4096 2005-06-28     disk
        drwxr-xr-x  2 yangzongde yangzongde     4096 2002-08-15     doc
        drwxr-xr-x  3 yangzongde yangzongde     4096 2005-06-28     drivers
        drwxr-xr-x  2 yangzongde yangzongde     4096 2005-06-28     dtt
        drwxr-xr-x  2 yangzongde yangzongde     4096 2005-06-28     examples
        drwxr-xr-x  7 yangzongde yangzongde     4096 2002-08-15     fs
        -rw-r--r--  1 yangzongde yangzongde     910 2002-08-15      i386_config.mk
        drwxr-xr-x  16 yangzongde yangzongde    4096 2005-06-28     include
        drwxr-xr-x  2 yangzongde yangzongde     4096 2005-06-28     lib_arm
        drwxr-xr-x  2 yangzongde yangzongde     4096 2005-06-28     lib_generic
        drwxr-xr-x  2 yangzongde yangzongde     4096 2002-08-15     lib_i386
        drwxr-xr-x  2 yangzongde yangzongde     4096 2002-08-15     lib_m68k
        drwxr-xr-x  2 yangzongde yangzongde     4096 2002-08-15     lib_microblaze
        drwxr-xr-x  2 yangzongde yangzongde     4096 2002-08-15     lib_mips
        drwxr-xr-x  2 yangzongde yangzongde     4096 2002-08-15     lib_nios
        drwxr-xr-x  2 yangzongde yangzongde     4096 2002-08-15     lib_ppc
        -rw-r--r--  1 yangzongde yangzongde     934 2002-08-15      m68k_config.mk
        -rw-r--r--  1 yangzongde yangzongde     7321 2002-08-15     MAINTAINERS
        -rwxr-xr-x  1 yangzongde yangzongde     6098 2002-08-15     MAKEALL
        -rw-r--r--  1 yangzongde yangzongde     39112 2005-06-27    Makefile
        -rw-r--r--  1 yangzongde yangzongde     905 2002-08-15      mips_config.mk
        -rwxr-xr-x  1 yangzongde yangzongde     1130 2002-08-15     mkconfig
        drwxr-xr-x  2 yangzongde yangzongde     4096 2005-06-28     net
        -rw-r--r--  1 yangzongde yangzongde     939 2002-08-15      nios_config.mk
        drwxr-xr-x  3 yangzongde yangzongde     4096 2005-06-28     post
        -rw-r--r--  1 yangzongde yangzongde     936 2002-08-15      ppc_config.mk
        -rw-r--r--  1 yangzongde yangzongde     112404 2002-08-15   README
        drwxr-xr-x  2 yangzongde yangzongde     4096 2005-06-28     rtc
        -rw-r--r--  1 root    root              13934 2005-06-28    System.map
        drwxr-xr-x  9 yangzongde yangzongde     4096 2005-06-28     tools

其中:

board文件夹主要存放的是U-Boot所支持的目标板的子目录,也就是相应的硬件处理器分类,另外还需要修改Flash.c文件。

cpu文件主要存入的是u-boot所支持的CPU类型,此文件夹下主要是一段初始可执行环境,包括中断处理等基本设置。其start.S文件是可执行代码的第一阶段代码。

command文件夹存入的是一些公共命令的实现,也就是说,用户进入到u-boot后可以输入运行的命令全部在此文件夹下。

drivers文件夹主要存入的是一些外部设备接口的驱动程序;

fs文件夹为文件系统相关的源代码文件。

1.3.2 Linux内核基本结构

Linux内核是一个 Linux操作系统的核心。它负责管理系统的进程、内存、设备驱动程序、文件和网络系统,决定着系统的性能和稳定性。Linux的一个重要的特点就是其源代码的公开性,所有的内核源程序都可以在/usr/src/linux目录下找到,大部分应用软件也都是遵循GPL而设计的,读者都可以获取相应的源程序代码。

任何一个软件工程师都可以将自己认为优秀的代码加入到其中,由此引发的一个明显的好处就是Linux修补漏洞的快速及对最新软件技术的利用。而Linux的内核则是这些特点的最直接的代表。拥有了内核的源程序可以让读者了解系统是如何工作的,通过通读源代码,读者可以了解系统的工作原理,另外可以针对自己的需要定制适合自己的系统,这是经常提到的重新编译内核工作。

Linux作为一个自由软件,在广大爱好者的支持下,内核版本不断更新。新的内核修订了旧内核的缺陷,并增加了许多新的特性。如果用户想要使用这些新特性,或想根据自己的系统量身定制一个更高效、更稳定的内核,就需要重新编译内核。通常更新的内核会支持更多的硬件,具备更好的进程管理能力,运行速度更快、更稳定,并且一般会修复老版本中发现的许多漏洞等,经常性选择升级更新的系统内核是Linux使用者的必要操作内容。为了正确合理地设置内核编译配置选项,从而只编译系统需要的功能的代码,一般主要有如下四方面的考虑。

● 自己定制编译的内核运行更快(具有更少的代码)。

● 系统将拥有更多的内存(内核部分将不会被交换到虚拟内存中)。

● 不需要的功能编译进入内核可能会增加被系统攻击者利用的漏洞。

● 将某种功能编译为模块方式会比编译到内核内的方式速度要慢一些。

要增加对某部分功能的支持,比如网络之类,可以把相应部分编译到内核中(build-in),也可以把该部分编译成模块(module),实现动态调用。如果编译到内核中,在内核启动时就可以自动支持相应部分的功能,这样的优点是方便、速度快,机器一启动就可以使用这部分功能;缺点是会使内核变得庞大起来,不管是否需要这部分功能,它都会存在。如果编译成模块,就会生成对应的.o文件,在使用的时候可以动态加载,优点是不会使内核过分庞大,缺点是需要每次自己来调用这些模块。

1.Linux内核结构

Linux核心源程序通常都安装在/usr/src/linux目录下,都是一个稳定地发行的核心,而任何奇数核心源程序的文件按树形结构进行组织,在源程序树的最上层。目录/usr/src/linux下有这样一些目录和文件。

(1)COPYING:GPL版权声明。对具有GPL版权的源代码改动而形成的程序,或使用GPL工具产生的程序,具有使用GPL发表的义务,如公开源代码。

(2)CREDITS:光荣榜。对Linux做出过很大贡献的一些人的信息。

(3)MAINTAINERS:维护人员列表,对当前版本的内核各部分都由谁负责。

(4)Makefile:用来组织内核的各模块,记录了模块间的相互联系和依托关系,编译时使用;仔细阅读各子目录下的Makefile文件对弄清各个文件之间的联系和依托关系很有帮助。

(5)ReadMe:核心及其编译配置方法的简单介绍。

(6)Rules.make:各种Makefilemake所使用的一些共同规则。

(7)REPORTING-BUGS:有关报告Bug的一些内容。

(8)Arch/:arch子目录包括了所有和体系结构相关的核心代码。它的每一个子目录都代表一种支持的体系结构,例如i386就是关于Intel CPU及与之相兼容体系结构的子目录。PC一般都基于此目录,在2.6版本以后,增加了ARM目录。

(9)Include/:include子目录包括编译核心所需要的大部分头文件。与平台无关的头文件在include/linux子目录下,与Intel CPU相关的头文件在include/asm-i386子目录下,而include/scsi目录则是有关SCSI设备的头文件目录。

(10)Init/:这个目录包含核心的初始化代码(注:不是系统的引导代码),包含两个文件main.c和Version.c,是研究核心如何工作的好的起点之一。

(11)Mm/:这个目录包括所有独立于CPU体系结构的内存管理代码,如页式存储管理内存的分配和释放等;而和体系结构相关的内存管理代码则位于arch/*/mm/目录下,例如arch/i386/mm/Fault.c。

(12)Kernel/:主要的核心代码,此目录下的文件实现了大多数 Linux系统的内核函数,其中最重要的文件当属sched.c;同样,和体系结构相关的代码在arch/*/kernel中。

(13)Drivers/:放置系统所有的设备驱动程序;每种驱动程序又各占用一个子目录,如/block下为块设备驱动程序,比如ide(ide.c)。如果希望查看所有可能包含文件系统的设备是如何初始化的,可以查看drivers/block/genhd.c中的device_setup()。它不仅初始化硬盘,也初始化网络,因为安装NFS文件系统的时候需要网络。

(14)Documentation/:文档目录,没有内核代码,只是一套有用的文档,。

(15)Fs/:所有的文件系统代码和各种类型的文件操作代码,它的每一个子目录支持一个文件系统,例如fat和ext。

(16)ipc/:这个目录包含核心的进程间通信的代码。

(17)Lib/:放置核心的库代码。

(18)Net/:核心与网络相关的代码。

(19)Modules/:模块文件目录,是个空目录,用于存放编译时产生的模块目标文件。

(20)Scripts/:描述文件、脚本,用于对核心的配置。

另外,在每个子目录下一般都有一个Makefile和一个Readme文件。

2.Linux内核配置

首先分析Linux内核中的配置系统结构,然后解释内核中的Makefile和配置文件的格式及配置语句的含义,最后通过一个简单的例子TEST Driver具体说明如何将自行开发的代码加入到Linux内核中。

(1)配置系统的基本结构

Linux内核的配置系统由三部分组成,分别如下所示。

● Makefile:分布在 Linux内核源代码中的 Makefile(一般在每个文件夹下都有一个Makefile文件),定义Linux内核的编译规则;

● 配置文件(config.in):给用户提供配置选择的功能;

● 配置工具:包括配置命令解释器(对配置脚本中使用的配置命令进行解释)和配置用户界面(提供基于字符界面、基于Ncurses图形界面及基于Xwindows图形界面的用户配置界面,各自对应于Make config、Make menuconfig和make xconfig)。

这些配置工具都是使用脚本语言的,如Tcl/TK、Perl编写的(也包含一些用C编写的代码)。

(2)内核Makefile相关文件

在内核中,Makefile的作用是根据配置的情况构造出需要编译的源文件列表,然后分别编译,并把目标代码链接到一起,最终形成Linux内核二进制文件。由于Linux内核源代码是按照树形结构组织的,所以Makefile也被分布在目录树中。Linux内核中的Makefile及与Makefile直接相关的文件如下所示。

● Makefile:顶层Makefile,是整个内核配置、编译的总体控制文件。

● .config:内核配置文件,包含由用户选择的配置选项,用来存放内核配置后的结果(如make config)。

● arch/*/Makefile:位于各种CPU体系目录下的Makefile,如arch/arm/Makefile,是针对特定平台的Makefile。

● 各个子目录下的Makefile:比如drivers/Makefile,负责所在子目录下源代码的管理。

● Rules.make:规则文件,被所有的Makefile使用。

用户通过make config配置后,产生了.config。顶层Makefile读入.config中的配置选择。顶层 Makefile有两个主要的任务:产生vmlinux文件和内核模块(module)。为了达到此目的,顶层Makefile递归进入到内核的各个子目录中,分别调用位于这些子目录中的Makefile。至于到底进入哪些子目录,取决于内核的配置。在顶层Makefile中include arch/$(ARCH)/Makefile,包含了特定CPU体系结构下的Makefile,这个Makefile中包含了平台相关的信息。

位于各个子目录下的Makefile同样也根据.config给出的配置信息,构造出当前配置下需要的源文件列表,并在文件的最后有include $(TOPDIR)/Rules.make。

Rules.make定义了所有Makefile共用的编译规则。比如,如果需要将本目录下所有的C程序编译成汇编代码,需要在Makefile中有以下的编译规则。

        %.s: %.c
        $(CC) $(CFLAGS) -S $< -o $@

有很多子目录下都有同样的要求,就需要在各自的Makefile中包含此编译规则,这会比较麻烦。而 Linux内核中则把此类的编译规则统一放置到 Rules.make中,并在各自的Makefile中包含进了Rules.make(include Rules.make),这样就避免了在多个Makefile中重复同样的规则。对于上面的例子,在Rules.make中对应的规则为:

        %.s: %.c
        $(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$(*F)) $(CFLAGS_$@) -S $< -o
    $@

(3)内核中Makefile变量

顶层Makefile定义并向环境中输出了许多变量,为各个子目录下的Makefile传递一些信息。有些变量比如 SUBDIRS,不仅在顶层 Makefile中定义并且赋初值,而且在arch/*/Makefile还作了扩充。常用的变量有以下几类。

● 版本信息:版本信息有VERSION、PATCHLEVEL、SUBLEVEL、EXTRAVERSION、KERNELRELEASE。版本信息定义了当前内核的版本,比如 VERSION=2, PATCHLEVEL=4,SUBLEVEL=18,EXATAVERSION=-rmk7,它们共同构成内核的发行版本KERNELRELEASE:2.4.18-rmk7。

● CPU体系结构:在顶层Makefile的开头,用ARCH定义目标CPU的体系结构,比如ARCH: =arm等。许多子目录的Makefile中,要根据ARCH的定义选择编译源文件的列表。

● 路径信息:TOPDIR定义了Linux内核源代码所在的根目录。例如,各个子目录下的Makefile通过$(TOPDIR)/Rules.make就可以找到Rules.make的位置。SUBDIRS定义了一个目录列表,在编译内核或模块时,顶层Makefile就是根据SUBDIRS来决定进入哪些子目录。SUBDIRS 的值取决于内核的配置,在顶层 Makefile中SUBDIRS 赋值为kernel drivers mm fs net ipc lib;根据内核的配置情况,在arch/*/Makefile中扩充了SUBDIRS的值,参见下面的例子。

● 内核组成信息:有HEAD、CORE_FILES、NETWORKS、DRIVERS、LIBS。Linux内核文件vmlinux是由以下规则产生的。

        vmlinux: $(CONFIGURATION) init/main.o init/version.o linuxsubdirs
        $(LD) $(LINKFLAGS) $(HEAD) init/main.o init/version.o \
        --start-group \
        $(CORE_FILES) \
        $(DRIVERS) \
        $(NETWORKS) \
        $(LIBS) \
        --end-group \
        -o vmlinux

可以看出,vmlinux是由 HEAD、main.o、version.o、CORE_FILES、DRIVERS、NETWORKS和LIBS组成的。这些变量(如HEAD)都是用来定义连接生成vmlinux的目标文件和库文件列表。其中,HEAD 在arch/*/Makefile中定义,用来确定被最先链接进vmlinux的文件列表。比如,对于ARM系列的CPU,HEAD定义为:

        HEAD := arch/arm/kernel/head-$(PROCESSOR).o \
        arch/arm/kernel/init_task.o

表明head-$(PROCESSOR).o和init_task.o需要最先被链接到vmlinux中。PROCESSOR为armv或armo,取决于目标CPU。CORE_FILES、NETWORK、DRIVERS和LIBS在顶层Makefile中定义,并且由arch/*/Makefile根据需要进行扩充。CORE_FILES对应着内核的核心文件,有kernel/kernel.o、mm/mm.o、fs/fs.o、ipc/ipc.o。可以看出,这些是组成内核最为重要的文件。同时,arch/arm/Makefile对CORE_FILES进行了扩充。

        # arch/arm/Makefile
        # If we have a machine-specific directory, then include it in the build.
        MACHDIR := arch/arm/mach-$(MACHINE)
        ifeq ($(MACHDIR),$(wildcard $(MACHDIR)))
        SUBDIRS += $(MACHDIR)
        CORE_FILES := $(MACHDIR)/$(MACHINE).o $(CORE_FILES)
        endif
        HEAD := arch/arm/kernel/head-$(PROCESSOR).o \
        arch/arm/kernel/init_task.o
        SUBDIRS += arch/arm/kernel arch/arm/mm arch/arm/lib arch/arm/nwfpe
        CORE_FILES := arch/arm/kernel/kernel.o arch/arm/mm/mm.o $(CORE_FILES)
        LIBS := arch/arm/lib/lib.a $(LIBS)

● 编译信息:CPP、CC、AS、LD、AR、CFLAGS、LINKFLAGS

在Rules.make中定义的是编译的通用规则,具体到特定的场合,需要明确给出编译环境,编译环境就是在以上的变量中定义的。针对交叉编译的要求,定义了 CROSS_COMPILE。例如:

        CROSS_COMPILE = arm-linux-
        CC = $(CROSS_COMPILE)gcc
        LD = $(CROSS_COMPILE)ld
          . .

CROSS_COMPILE定义了交叉编译器前缀arm-linux-,表明所有的交叉编译工具都是以arm-linux-开头的,所以在各个交叉编译器工具之前,都加入了$(CROSS_COMPILE),以组成一个完整的交叉编译工具文件名,比如arm-linux-gcc。

CFLAGS定义了传递给C编译器的参数。

LINKFLAGS 是链接生成vmlinux时,由链接器使用的参数。LINKFLAGS 在arm/*/Makefile中定义,比如:

        # arch/arm/Makefile
        LINKFLAGS :=-p -X -T arch/arm/vmlinux.lds

● 配置变量CONFIG_*

.config文件中有许多配置变量等式,用来说明用户配置的结果。例如CONFIG_MODULES=y表明用户选择了 Linux内核的模块功能。.config被顶层 Makefile包含后,就形成许多配置变量,每个配置变量具有确定的值。

y表示本编译选项对应的内核代码被静态编译进Linux内核;

m表示本编译选项对应的内核代码被编译成模块;

n表示不选择此编译选项;如果根本就没有选择,那么配置变量的值为空。

(4)Rules.make变量

前面讲过,Rules.make是编译规则文件,所有的 Makefile中都会包括 Rules.make。Rules.make文件定义了许多变量,最重要的是那些编译、链接列表变量。

● O_OBJS,L_OBJS,OX_OBJS,LX_OBJS:本目录下需要编译进Linux内核vmlinux的目标文件列表,其中 OX_OBJS 和 LX_OBJS 中的“X”表明目标文件使用了EXPORT_SYMBOL输出符号。

● M_OBJS,MX_OBJS:本目录下需要被编译成可装载模块的目标文件列表。同样, MX_OBJS中的“X”表明目标文件使用了EXPORT_SYMBOL输出符号。

● O_TARGET,L_TARGET:每个子目录下都有一个 O_TARGET 或 L_TARGET, Rules.make首先从源代码编译生成O_OBJS和OX_OBJS中所有的目标文件,然后使用$(LD) -r把它们链接成一个O_TARGET或L_TARGET。O_TARGET以.o结尾,而L_TARGET以.a结尾。

(5)子目录Makefile

子目录Makefile用来控制本级目录以下源代码的编译规则。下面通过一个例子来讲解子目录Makefile的组成。

        #
        # Makefile for the linux kernel.
        #
        # All of the (potential) objects that export symbols.
        # This list comes from 'grep -l EXPORT_SYMBOL *.[hc]'.
        export-objs := tc.o
        # Object file lists.
        obj-y :=
        obj-m :=
        obj-n :=
        obj- :=
        obj-$(CONFIG_TC) += tc.o
        obj-$(CONFIG_ZS) += zs.o
        obj-$(CONFIG_VT) += lk201.o lk201-map.o lk201-remap.o
        # Files that are both resident and modular: remove from modular.
        obj-m := $(filter-out $(obj-y), $(obj-m))
        # Translate to Rules.make lists.
        L_TARGET := tc.a
        L_OBJS := $(sort $(filter-out $(export-objs), $(obj-y)))
        LX_OBJS := $(sort $(filter $(export-objs), $(obj-y)))
        M_OBJS := $(sort $(filter-out $(export-objs), $(obj-m)))
        MX_OBJS := $(sort $(filter $(export-objs), $(obj-m)))
        include $(TOPDIR)/Rules.make

此文件中包含以下内容。

● 注释:对Makefile的说明和解释,由#开始。

● 编译目标定义:类似于obj-$(CONFIG_TC) += tc.o的语句是用来定义编译的目标,是子目录 Makefile中最重要的部分。编译目标定义那些在本子目录下需要编译到Linux内核中的目标文件列表。为了只当用户选择了此功能后才编译,所有的目标定义都融合了对配置变量的判断。

前面说过,每个配置变量取值范围是y、n、m和空,obj-$(CONFIG_TC)分别对应着obj-y、obj-n、obj-m、obj-。如果CONFIG_TC配置为y,那么tc.o就进入了obj-y列表。obj-y为包含到Linux内核vmlinux中的目标文件列表;obj-m为编译成模块的目标文件列表;obj-n和obj-中的文件列表被忽略。配置系统就根据这些列表的属性进行编译和链接。

export-objs中的目标文件都使用EXPORT_SYMBOL()定义了公共的符号,以便可装载模块使用。在tc.c文件的最后部分,有“EXPORT_SYMBOL(search_tc_card);”,表明tc.o有符号输出。

这里需要指出的是,对于编译目标的定义存在着两种格式,分别是老式定义和新式定义。老式定义就是前面 Rules.make使用的那些变量,新式定义就是obj-y、obj-m、obj-n和obj-。Linux内核推荐使用新式定义,不过由于 Rules.make不理解新式定义,需要在Makefile中的适配段将其转换成老式定义。

● 适配段:适配段的作用是将新式定义转换成老式定义。在上面的例子中,适配段就是将obj-y和obj-m转换成Rules.make能够理解的L_TARGET、L_OBJS、LX_OBJS、M_OBJS、MX_OBJS。

L_OBJS:=$(sort$(filter-out$(export-objs), $(obj-y))) 定义了L_OBJS的生成方式,在obj-y的列表中过滤掉export-objs(tc.o),然后排序并去除重复的文件名。这里使用到GNU Make的一些特殊功能,具体的含义可参考Make的文档(info make)。

● include $(TOPDIR)/Rules.make:包含上层Rules.make文件。

(6)config.in配置功能概述

除了Makefile的编写,另外一个重要的工作就是把新功能加入到Linux的配置选项中,提供此项功能的说明,让用户有机会选择此项功能。所有的这些都需要在config.in文件中用配置语言来编写配置脚本,在Linux内核中,配置命令有多种方式。

        Make config, make oldconfig scripts/Configure
        Make menuconfig scripts/Menuconfig
        Make xconfig scripts/tkparse

以字符界面配置(make config)为例,顶层 Makefile调用scripts/Configure,按照arch/arm/config.in来进行配置。命令执行完后产生文件.config,其中保存着配置信息。下一次再做make config将产生新的.config文件,原.config被改名为.config.old。

(7)配置语言

● 顶层菜单:mainmenu_name /prompt/,其中/prompt/ 是用单引号'或双引号"包围的字符串,单引号'与双引号"的区别是'…'中可使用$引用变量的值。mainmenu_name设置最高层菜单的名字,它只在make xconfig时才会显示。

● 询问语句

        bool /prompt/ /symbol/
        hex /prompt/ /symbol/ /word/
        int /prompt/ /symbol/ /word/
        string /prompt/ /symbol/ /word/
        tristate /prompt/ /symbol/

询问语句首先显示一串提示符/prompt/,等待用户输入,并把输入的结果赋给/symbol/所代表的配置变量。不同的询问语句的区别在于它们接受的输入数据类型不同,比如bool接受布尔类型(y或n),hex接受16进制数据。有些询问语句还有第三个参数/word/,用来给出默认值。

● 定义语句

        define_bool /symbol/ /word/
        define_hex /symbol/ /word/
        define_int /symbol/ /word/
        define_string /symbol/ /word/
        define_tristate /symbol/ /word/

不同于询问语句等待用户输入,定义语句显式地给配置变量/symbol/赋值/word/。

● 依赖语句

        dep_bool /prompt/ /symbol/ /dep/ ...
        dep_mbool /prompt/ /symbol/ /dep/ ...
        dep_hex /prompt/ /symbol/ /word/ /dep/ ...
        dep_int /prompt/ /symbol/ /word/ /dep/ ...
        dep_string /prompt/ /symbol/ /word/ /dep/ ...
        dep_tristate /prompt/ /symbol/ /dep/ ...

与询问语句类似,依赖语句也是定义新的配置变量。不同的是,配置变量/symbol/的取值范围将依赖于配置变量列表/dep/ …。这就意味着被定义的配置变量所对应功能的取舍取决于依赖列表所对应功能的选择。以dep_bool为例,如果/dep/ …列表的所有配置变量都取值y,则显示/prompt/,用户可输入任意的值给配置变量/symbol/,但是只要有一个配置变量的取值为n,则/symbol/被强制成n。不同依赖语句的区别在于它们由依赖条件所产生的取值范围不同。

● 选择语句

choice /prompt/ /word/ /word/:choice语句首先给出一串选择列表,供用户选择其中一种。比如Linux for ARM支持多种基于ARM core的CPU,Linux使用choice语句提供一个CPU列表,供用户选择。

        choice 'ARM system type' \
        "Anakin CONFIG_ARCH_ANAKIN \
        Archimedes/A5000 CONFIG_ARCH_ARCA5K \
        Cirrus-CL-PS7500FE CONFIG_ARCH_CLPS7500 \
        ……
        SA1100-based CONFIG_ARCH_SA1100 \
        Shark CONFIG_ARCH_SHARK" RiscPC

Choice首先显示/prompt/,然后将/word/分解成前后两个部分,前部分为对应选择的提示符,后部分是对应选择的配置变量。用户选择的配置变量为y,其余的都为n。

● if语句

        if [ /expr/ ] ; then
        /statement/
        fi
        if [ /expr/ ] ; then
        /statement/
        else
        /statement/
        fi

if语句对配置变量(或配置变量的组合)进行判断,并做出不同的处理。判断条件/expr/可以是单个配置变量或字符串,也可以是带操作符的表达式。操作符有=、!=、-o、-a等。

● 菜单块(menu block)语句

        mainmenu_option next_comment
        comment '……'
        …
        endmenu

以上代码用来引入新的菜单。在向内核增加新的功能后,需要相应地增加新的菜单,并在新菜单下给出此项功能的配置选项。Comment后带的注释就是新菜单的名称。所有归属于此菜单的配置选项语句都写在comment和endmenu之间。

● Source语句:source /word/

/word/是文件名,source的作用是调入新的文件。

(8)默认配置

Linux内核支持非常多的硬件平台,对于具体的硬件平台而言,有些配置是必需的,有些配置不是必需的。另外,新增加功能的正常运行往往也需要一定的先决条件,针对新功能,必须做相应的配置。因此,特定硬件平台能够正常运行对应着一个最小的基本配置,这就是默认配置。

Linux内核中针对每个ARCH都会有一个默认配置。在向内核代码增加了新的功能后,如果新功能对于这个ARCH是必需的,就要修改此ARCH的默认配置。修改方法如下(在Linux内核根目录下)。

备份.config文件

        cp arch/arm/deconfig .config

修改.config

        cp .config arch/arm/deconfig

恢复.config

如果新增的功能适用于许多的ARCH,只要针对具体的ARCH,重复上面的步骤就可以。

(9)帮助文件

在配置Linux内核时,遇到不懂含义的配置选项,可以查看它的帮助,从中可得到选择的建议。所有配置选项的帮助信息都在Documentation/Configure.help中,它的格式为:

        <description>
        <variable name>
        <help file>

<description>给出本配置选项的名称,<variable name>对应配置变量,<help file>对应配置帮助信息。在帮助信息中,首先简单描述此功能,其次说明选择了此功能后会有什么效果,不选择又有什么效果,最后,不要忘了写上“如果不清楚,选择N(或者)Y”,给不知所措的用户以提示。

3.内核配置实例

对于一个开发者来说,将自己开发的内核代码加入到Linux内核中,需要3个步骤。

● 首先,确定把自己开发代码放入到内核的位置;

● 其次,把自己开发的功能增加到Linux内核的配置选项中,使用户能够选择此功能;

● 最后,构建子目录Makefile,根据用户的选择,将相应的代码编译到最终生成的Linux内核中去。

以下通过一个简单的例子test driver,结合前面学到的知识,来说明如何向Linux内核中增加新的功能。

(1)源代码目录结构

添加自己的源代码,将test driver文件夹放置在drivers/test/目录下,其目录结构如下。

        $cd drivers/test
        $tree
        |-- Config.in
        |-- Makefile
        |-- cpu
        | |-- Makefile
        | `-- cpu.c
        |-- test.c
        |-- test_client.c
        |-- test_ioctl.c
        |-- test_proc.c
        |-- test_queue.c
        `-- test
        |-- Makefile
        `-- test.c

(2)配置文件说明

● drivers/test/Config.in文件

        #
        # TEST driver configuration
        #
        mainmenu_option next_comment
        comment 'TEST Driver'
        bool 'TEST support' CONFIG_TEST
        if [ "$CONFIG_TEST" = "y" ]; then
        tristate 'TEST user-space interface' CONFIG_TEST_USER
        bool 'TEST CPU ' CONFIG_TEST_CPU
        fi
        endmenu

test driver对于内核来说是新的功能,所以首先创建一个菜单TEST Driver;然后,显示“TEST support”,等待用户选择;接下来判断用户是否选择了 TEST Driver,如果是(CONFIG_TEST=y),则进一步显示子功能——用户接口与CPU功能支持;由于用户接口功能可以被编译成内核模块,所以这里的询问语句使用了tristate(因为tristate的取值范围包括y、n和m,m就是对应的模块)。

● arch/arm/config.in

在文件的最后加入source drivers/test/Config.in,将TEST Driver子功能的配置纳入到Linux内核的配置中。

(3)Makefile文件说明

● drivers/test/Makefile文件

        # drivers/test/Makefile
        #
        # Makefile for the TEST.
        #
        SUB_DIRS :=
        MOD_SUB_DIRS := $(SUB_DIRS)
        ALL_SUB_DIRS := $(SUB_DIRS) cpu
        L_TARGET := test.a
        export-objs := test.o test_client.o
        obj-$(CONFIG_TEST) += test.o test_queue.o test_client.o
        obj-$(CONFIG_TEST_USER) += test_ioctl.o
        obj-$(CONFIG_PROC_FS) += test_proc.o
        subdir-$(CONFIG_TEST_CPU) += cpu
        include $(TOPDIR)/Rules.make
        clean:
        for dir in $(ALL_SUB_DIRS); do make -C $$dir clean; done
        rm -f *.[oa] .*.flags

drivers/test目录下最终生成的目标文件是test.a。在test.c和test-client.c中使用了EXPORT_SYMBOL输出符号,所以test.o和test-client.o位于export-objs列表中。然后,根据用户的选择(具体来说,就是配置变量的取值),构建各自对应的obj-*列表。由于TEST Driver中包含一个子目录cpu,当CONFIG_TEST_CPU=y(即用户选择了此功能)时,需要将cpu目录加入到subdir-y列表中。

● drivers/test/cpu/Makefile文件说明

        # drivers/test/test/Makefile
        #
        # Makefile for the TEST CPU
        #
        SUB_DIRS :=
        MOD_SUB_DIRS := $(SUB_DIRS)
        ALL_SUB_DIRS := $(SUB_DIRS)
        L_TARGET := test_cpu.a
        obj-$(CONFIG_test_CPU) += cpu.o
        include $(TOPDIR)/Rules.make
        clean:
        rm -f *.[oa] .*.flags

● drivers/Makefile文件修改

        ……
        subdir-$(CONFIG_TEST) += test
        ……
        include $(TOPDIR)/Rules.make

在drivers/Makefile中加入subdir-$(CONFIG_TEST)+= test,使得在用户选择 TEST Driver功能后,内核编译时能够进入test目录。

● 顶层Makefile文件修改

        ……
        DRIVERS-$(CONFIG_PLD) += drivers/pld/pld.o
        DRIVERS-$(CONFIG_TEST) += drivers/test/test.a
        DRIVERS-$(CONFIG_TEST_CPU) += drivers/test/cpu/test_cpu.a
        DRIVERS := $(DRIVERS-y)
        ……

在顶层Makefile中加入DRIVERS-$(CONFIG_TEST) += drivers/test/test.a和DRIVERS-$(CONFIG_TEST_CPU) += drivers/test/cpu/test_cpu.a。如果用户选择了TEST Driver,那么CONFIG_TEST和CONFIG_TEST_CPU都是y,test.a和test_cpu.a就都位于DRIVERS-y列表中,然后又被放置在DRIVERS列表中。在前面曾经提到过,Linux内核文件vmlinux的组成中包括DRIVERS,所以test.a和test_cpu.a最终可被链接到vmlinux中。

关于驱动程序源代码这里不再介绍。

1.3.3 移植Linux操作系统

本小节将介绍如何编译可以运行于ARM处理器的Linux操作系统可执行文件。由于前面已经详细介绍构建嵌入式Linux交叉编译环境,这里仅介绍如何重新编译Linux内核源代码程序。

1.配置前准备工作

Linux内核版本发布的官方网站是http://www.kernel.org。编译内核需要root权限,以下以Linux-2.4.0版本内核为例介绍编译过程。

(1)准备工作,解压源代码

把需要升级的内核复制到/usr/src/下,命令为

        #cp linux-2.4.0test8.tar.gz /usr/src

首先来查看一下当前/usr/src的内容,有一个 Linux的符号链接,它指向一个类似于Linux-X.X.X(对应于现在使用的内核版本号)的目录。首先删除这个链接。

        #cd /usr/src
        #rm -f linux

接着解压下载的源程序文件。如果所下载的是.tar.gz(.tgz)文件,请使用下面的命令。

        #tar -xzvf linux-2.4.0test8.tar.gz

如果所下载的是.bz2文件,例如linux-2.4.0test8.tar.bz2,请使用下面的命令。

        #bzip2 -d linux-2.4.0test8.tar.bz2
        #tar -xvf linux.2.4.0.test8.tar

现在再来看一下/usr/src下的内容,会发现有一个名为Linux的目录,里面就是需要升级到的版本的内核的源程序。之所以使用前面删除的那个链接就是防止在升级内核的时候会不慎把原来版本内核的源程序覆盖掉。这里需要同样处理。

        #mv linux linux-2.4.0test8
        #ln -s linux-2.4.0test8 linux

另外,如果还下载了patch文件,比如patch-2.4.0test8,就可以进行patch操作(下面假设patch-2.4.0test8已经位于/usr/src目录下了,否则需要先把该文件复制到/usr/src下)。

        #patch -p0 < patch-2.4.0test8

(2)准备工作

运行的第一个命令是:

        #cd /usr/src/linux;make mrproper

该命令确保源代码目录下没有不正确的.o文件及文件的互相依赖。

确保/usr/include/目录下的asm、Linux和scsi等链接是指向要升级的内核源代码的。它们分别链向源代码目录下该计算机体系结构(对于PC来说,使用的体系结构是i386)所需要的真正的include子目录。如asm指向/usr/src/linux/include/asm-i386等。若没有这些链接,就需要手工创建,按照下面的步骤进行。

        # cd /usr/include/
        # rm -r asm linux scsi
        # ln -s /usr/src/linux/include/asm-i386 asm
        # ln -s /usr/src/linux/include/linux linux
        # ln -s /usr/src/linux/include/scsi scsi

这是配置中非常重要的一部分。删除/usr/include下的asm、Linux和scsi链接后,再创建新的链接指向新内核源代码目录下的同名的目录。这些头文件目录包含着保证内核在系统上正确编译所需要的重要的头文件。现在应该明白为什么上面又在/usr/src下“多余”地创建名为linux的链接。

(3)配置内核

接下来的内核配置过程比较烦琐,但是配置的适当与否与日后Linux的运行直接相关,所以有必要了解一下一些主要的且经常用到的选项的设置。配置内核可以根据需要与爱好使用下面命令中的一个。

● #make config(基于文本的最传统的配置界面,不推荐使用)。

● #make menuconfig(基于文本选单的配置界面,字符终端下推荐使用)。

● #make xconfig(基于图形窗口模式的配置界面,Xwindow下推荐使用)。

● #make oldconfig(如果只想在原来内核配置的基础上修改一些小地方,会省去不少麻烦)。

这4个命令中,make xconfig的界面最为友好,如果可以使用Xwindow,那么就推荐使用这个命令。如果不能使用Xwindow,那么就使用make menuconfig。界面虽然比make xconfig差一些,总比make config要好得多。

选择相应的配置时,有三种选择,它们分别代表的含义如下。

● Y——将该功能编译进内核;

● N——不将该功能编译进内核;

● M——将该功能编译成可以在需要时动态插入到内核中的模块。

如果使用的是make xconfig,使用鼠标就可以选择对应的选项。如果使用的是make menuconfig,则需要使用空格键进行选取。会发现在每一个选项前都有个括号,但有的是方括号有的是尖括号,还有一种圆括号。用空格键选择时可以发现,方括号里要么是空,要么是"*",而尖括号里可以是空。"*"和"M"表示前者对应的项要么不要,要么编译到内核里;后者则多一种选择,可以编译成模块。而圆括号的内容是要在所提供的几个选项中选择一项。

在编译内核的过程中,最繁杂的事情就是配置工作,很多初学者不清楚到底该如何选取这些选项。实际上在配置时大部分选项可以使用其默认值,只有小部分需要根据用户不同的需要选择。选择的原则是将与内核其他部分关系较远且不经常使用的部分功能代码编译成为可加载模块,有利于减小内核的长度,减小内核消耗的内存,简化该功能相应的环境改变时对内核的影响;不需要的功能就不要选;与内核关系紧密而且经常使用的部分功能代码直接编译到内核中。以下就常用的选项分别加以介绍。

2.Linux内核配置选项说明

(1)Code maturity level options:代码成熟等级。此处只有一项prompt for development and/or incomplete code/drivers,如果要试验现在仍处于实验阶段的功能,比如khttpd、IPv6等,就必须把该项选择为Y了,否则可以把它选择为N。

(2)Loadable module support:对模块的支持。这里面有三项,如下所示。

● Enable loadable module support:除非准备把所有需要的内容都编译到内核里面,否则该项应该是必选的。

● Set version information on all module symbols:可以不选它。

● Kernel module loader:让内核在启动时有自己装入必需模块的能力,建议选上。

(3)Processor type and features:CPU类型。内容很多,有关的几个如下所示。

● Processor family:根据自己的情况选择CPU类型。

● High Memory Support:大容量内存的支持。可以支持到4GB、64GB,一般可以不选。

● Math emulation:协处理器仿真。协处理器是在386时代的宠儿,现在早已不用了。

● MTTR support:MTTR支持。可不选。

● Symmetric multi-processing support:对称多处理支持。除非有多个CPU,否则不用选。

(4)General setup:这里是对最普通的一些属性进行设置。这部分内容非常多,一般使用默认设置就可以了。下面介绍经常使用的一些选项。

● Networking support:网络支持。必选,没有网卡也建议选上。PCI support:PCI支持。如果使用了PCI的卡,当然必选。

● PCI access mode:PCI存取模式。可供选择的有BIOS、Direct和Any,选择Any。

● Support for hot-pluggabel devices:热插拔设备支持。支持得不是太好,可不选。

● PCMCIA/CardBus support:PCMCIA/CardBus支持。有PCMCIA就必选。

● System V IPC:System V进程间通信。

● BSD Process Accounting:BSD的进程处理。

● Sysctl support:这三项是有关进程处理/IPC调用的,主要是System V和BSD两种风格。如果不是使用BSD,就可以按照默认设置。

● Power Management support:电源管理支持。Advanced Power Management BIOS support:高级电源管理BIOD支持。

(5)Memory Technology Device(MTD):MTD设备支持。如果是Flash设备,则需要选择。

(6)Parallel port support:并口支持。

(7)Plug and Play configuration:即插即用支持。

(8)Block devices:块设备支持。

● Normal PC floppy disk support:普通PC软盘支持。

● XT hard disk support:XT硬件支持。

● Compaq SMART2 support:Compaq SMART阵列控制器支持

● Mulex DAC960/DAC1100 PCI RAID Controller support:RAID镜像使用。

● Loopback device support:回环设备支持。

● Network block device support:网络块设备支持。如果想访问网上邻居,就选择。

● Logical volume manager(LVM)support:逻辑卷管理支持。

● Multiple devices driver support:多设备驱动支持。

● RAM disk support:RAM盘支持。

(9)Networking options:网络选项。这里配置的是网络协议。主要包括TCP/IP、ATM、IPX、DECnet、Appletalk等,支持的协议很多,IPv6也支持。

(10)Telephony Support:电话支持。Linux下可以支持电话卡,这样就可以在IP上使用普通的电话提供语音服务了。

(11)ATA/IDE/MFM/RLL support:这个是有关各种接口的硬盘/光驱/磁带/软盘支持的。

(12)SCSI support:SCSI设备的支持。

(13)IEEE 1394(FireWire)support:IEEE 1394支持。

(14)I2O device support:在智能Input/Output(I2O)体系接口中使用。

(15)Network device support:网络设备支持。根据选择的协议相应设备有:ARCnet设备、Ethernet(10 or 100 Mb/s)、Ethernet(1000Mb/s)、Wireless LAN(non-hamradio)、Token Ring device、WAN interfaces、PCMCIA network device support几大类。

(16)Amateur Radio support:配置业余无线广播支持。

(17)IrDA(infrared)support:红外支持。

(18)ISDN subsystem:支持ISDN上网。

(19)Old CD-ROM drivers(not SCSI、not IDE):老式光盘。

(20)Character devices:字符设备。包括的设备如下所示。

I2C support:I2C是Philips极力推动的微控制应用中使用的低速串行总线协议。如果要选择下面的Video For Linux,该项必选。

Mice:鼠标。现在可以支持总线、串口、PS/2、C&T 82C710 mouse port、PC110 digitizer pad。

Joysticks:手柄。

Video For Linux:支持有关的音频/视频卡。

(21)File systems:文件系统。包括的内容如下所示。

Quota support:Quota可以限制每个用户可以使用的硬盘空间的上限,在多用户共同使用一台主机的情况中十分有效。

DOS FAT fs support:DOS FAT文件格式的支持,可以支持FAT16、FAT32。

ISO 9660 CD-ROM file system support:光盘使用的就是ISO 9660的文件格式。

NTFS file system support:NTFS是NT使用的文件格式。

/proc file system support:/proc文件系统是Linux提供给用户和系统进行交互的通道,建议选上,否则有些功能没法正确执行。

另外还有Network File Systems(网络文件系统)、Partition Types(分区类型)、Native Language Support(本地语言支持)。值得一提的是Network File Systems中的两种——NFS和SMB分别是Linux和Windows相互以网络邻居的形式访问对方所使用的文件系统,根据需要加以选择。

(22)Console drivers:控制台驱动。

(23)Sound:声卡驱动。

(24)USB supprot:USB支持。很多USB设备比如鼠标、调制解调器、打印机、扫描仪等,在Linux中都可以得到支持,根据需要自行选择。

(25)Kernel hacking:配置这个选项,即使在系统崩溃时,也可以进行一定的工作。

3.编译内核

在繁杂的配置工作完成以后,就可以耐心等候了。与编译有关的命令有如下几个。

        #make dep
        #make clean
        #make zImage
        #make bzImage
        #make modules
        #make modules_install
        #depmod -a

第一个命令make dep实际上读取配置过程生成的配置文件,来创建对应于配置的依赖关系树,从而决定哪些需要编译而哪些不需要;

第二个命令make clean完成删除前面步骤留下的文件,以避免出现一些错误;

第三个命令make zImage和第四个命令make bzImage实现完全编译内核,二者生成的内核都是使用gzip压缩的,只要使用一个就可以,它们的区别在于使用make bzImage可以生成大一点的内核,比如在编译2.4.0版本的内核时如果使用make zImage命令,那么就会出现system too big的错误提示。建议大家使用make bzImage命令。

后面三个命令只有在进行配置的过程中,回答 Enable loadable module support (CONFIG_MODULES)时选择“Yes”才是必要的,make modules和make modules_install分别生成相应的模块和把模块复制到需要的目录中。

1.4 本章总结

本章简单介绍了嵌入式的基础入门知识,主要包括嵌入式系统的基本概念、内核结构及操作系统的移植。通过本章的学习,读者对嵌入式的类型和应用有一个大致的了解。