嵌入式Linux基础教程(第2版)

978-7-115-40250-9
作者: 【美】Christopher Hallinan(克里斯托弗 哈利南)
译者: 周鹏
编辑: 傅道坤

图书目录:

详情

本书介绍了引导加载程序、系统初始化、文件系统、闪存和内核、应用程序调试技巧等,还讲述了构建Linux系统的工作原理,用于驱动不同架构的配置,Linux内核源码树的特性,如何根据需求配制内核运行时的行为,如何扩展系统功能,用于构建完整嵌入式Linux发行版的常用构建系统,USB子系统和系统配置工具udev等内容。

图书摘要

版权信息

书名:嵌入式Linux基础教程(第2版)

ISBN:978-7-115-40250-9

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

著    [美] Christopher Hallinan

译    周 鹏

责任编辑 朱 巍

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(76010)81055315


本书是嵌入式Linux的经典教程,介绍了引导加载程序、系统初始化、文件系统、闪存和内核、应用程序调试技巧等,还讲述了构建Linux系统的工作原理,用于驱动不同架构的配置,Linux内核源码树的特性,如何根据需求配制内核运行时的行为,如何扩展系统功能,用于构建完整嵌入式Linux发行版的常用构建系统,USB子系统和系统配置工具udev等内容。更重要的是,本书阐述了如何修改系统使之满足读者自身的需求,确保读者能够从中学习一些嵌入式工程中非常有用的提示和技巧。

本书适合Linux程序员阅读,也可作为高等院校相关专业师生的参考读物。


智能手机、PDA、家用路由器、智能电视、智能蓝光播放器以及智能悠悠球……好吧,或许智能悠悠球算不上。不管是用于工作也好,还是消遣也罢,越来越多的家用和办公用品中嵌入了计算机,这些计算机都运行着GNU/Linux。

你也许是一位习惯了在英特尔架构的台式电脑(或笔记本)上工作的GNU/Linux开发人员,或者你有可能是一位嵌入式系统开发人员,习惯于那些更传统的嵌入式或实时操作系统,无论你的背景如何,一旦进入嵌入式Linux开发的世界,多萝西[1]所说的“托托,我想我们再也回不去堪萨斯了”就会在你身上应验。欢迎来参加这个探险之旅!

[1] 多萝西(Dorothy)是童话故事《绿野仙踪》的主人公。——译者注

多萝西有目标,有一些好朋友,但是没有向导。然而,你的情况要好一些,因为你正拿着一本极好的“野外生存指南”,它能带领你进入嵌入式Linux开发的世界。Christopher Hallinan为你展示了这一切——怎么做,从何处着手,为什么要这样做以及哪些是不用做的。本书将让你免受磨练之苦,带领你轻松而快捷地打造自己的产品。

不必惊讶,本书在这个领域是首屈一指的。第2版更上一层楼,包含了最新的内容,融会了作者更多的经验。

能将本书纳入我们的“开源软件开发系列”,我深感荣幸。但更重要的是,你会为自己感到自豪,因为读了这本书,你会开发出更好的产品。学习愉快!

Arnold Robbins

(著名Linux专家)


计算机无处不在!

在过去大约25年中,只要不是与世隔绝的人就肯定不会对此感到惊讶。现在,计算机不仅占据了我们的桌面,进驻了我们的厨房,而且越来越多地进入到我们的生活,即便是在微波炉、电烤箱、移动电话和便携式数字音乐播放器中也出现了它的身影。

选择本书的读者肯定已经了解了不少,但还想学习更多的嵌入式系统知识。 就在不久前,嵌入式系统还不是很强大,它们运行具有特殊目的、专用的操作系统,而这些操作系统与工业标准的系统有很大不同(而且,它们也更难开发)。现在,嵌入式系统即使在功能上不比家用计算机强大,但至少也与其相当(如高端游戏机)。

伴随着这种强大的功能,运行Linux等成熟操作系统的能力也呼之欲出,在嵌入式产品中使用Linux这样的操作系统意义重大。一个庞大的开发者社区更使这一切成为可能。开发环境和部署环境惊人相似,这也让程序员的生活变得更轻松。现在我们既有由虚拟内存系统提供的保护地址空间的安全性,又有多用户、多进程操作系统的能力和灵活性。这真可谓应有尽有了。

出于这个原因,世界上很多公司都选择把Linux放在自己的设备中,如PDA、家庭娱乐系统,甚至——不管你信不信——手机里!

这本书很令我振奋。它为那些想在嵌入式系统中使用Linux的开发人员提供了极好的学习路线指导。本书内容简洁、准确,组织合理,字里行间渗透出Christopher的学问和见解,你不仅能从中得到很多信息和帮助,也能获得阅读的乐趣。

我希望你在学习的同时也能感受到这种乐趣,我自己已经感受到了。

Arnold Robbins

(著名Linux专家)


虽然讲Linux的好书很多,但是本书汇集了专门针对嵌入式Linux开发人员的各方面信息和建议。实际上,有关Linux内核、Linux系统管理等方面的优秀书籍已经有很多了。本书也参考了我认为在同类书籍中最好的几本。

本书的很多内容来自实际的问题,有些问题是我作为嵌入式Linux顾问时一些开发工程师提出的,还有些问题是我直接参与商业嵌入式Linux开发时遇到的。

嵌入式Linux会给经验丰富的软件工程师带来一些独特的挑战。首先,那些有多年老式实时操作系统(Real Time Operating System,RTOS)开发经验的工程师会发现,他们的思维习惯很难从旧的环境转换到Linux;其次,经验丰富的应用程序开发人员常常难以理解多种开发环境的相对复杂性。

虽然这是一本面向嵌入式Linux开发初学者的基础教程,但我相信即使是经验丰富的嵌入式Linux开发人员也能从中获益,书中包含了我多年积累的实用建议和技巧。

本书介绍了嵌入式工程师怎样才能迅速掌握嵌入式Linux环境的新知识。书中没有重点讲解Linux内核原理,而是在讲解内核的章节侧重从项目角度介绍内核。你可以阅读专门介绍内核原理的优秀图书来了解相关知识。你可以从本书学到内核源码树的组织和布局,了解组成内核镜像的二进制组件和加载它们的方法,以及它们在嵌入式系统中的作用。

在本书中,你会学到Linux内核构建系统的工作原理,以及怎样将满足项目需求的具体变化融合到系统中。你会了解到Linux系统初始化的细节,包括内核空间初始化和用户空间初始化。你还能看到很多对嵌入式项目有益的建议和技巧,涵盖引导加载程序、系统初始化、文件系统和闪存,以及高级的内核与应用程序调试技术。第2版中新增了很多内容,很多章节都有更新,比如讲开源构建系统、USB和udev的几章都是新的,而且有相当篇幅探讨如何在嵌入式Linux项目中配置和使用这些复杂的系统。

本书需要读者具有一定的C语言编程基础,对局域网和因特网有基本的了解,理解IP地址的概念以及IP地址在简单局域网中的用法,还需要理解十六进制和八进制编码方式以及它们常见的用法。

本书也涉及一些C语言编译和链接中较为深入的概念,因此你要是能粗略复习一下C语言链接器的概念就更好了。同时,了解GNU make操作和语法对于阅读本书也很有帮助。

本书不是一本详细介绍硬件的指南。硬件设备种类繁多是嵌入式开发者所面临的一大困难。集成了一些外围设备的现代32位处理器,其用户手册动辄就有3000页。没有捷径可走。作为程序员,要想理解硬件设备就必须花费大量时间研读硬件数据手册和参考指南,同时要花费更多的时间针对这些硬件设备编写和测试代码。

这也不是一本讲述Linux内核或内部原理的书。本书不会深入讨论用来实现Linux虚拟内存管理策略和过程的内存管理单元(MMU)。已经有许多关于这个主题的优秀图书了,建议你读一读每章后面的“补充阅读建议”。

命令和代码语句使用Courier New字体。用户输入的命令使用加粗的Courier New字体。新名词和重要概念使用楷体加以强调。

路径名前面有3个点表示这是大家熟知但未明确指定的顶层目录。上下文不同,顶层目录也会不同,但一般都是指顶层的Linux源码目录。例如,…/arch/powerpc/kernel/setup_32.c指位于Linux源码树体系结构分支中的文件setup_32.c。实际的文件路径有可能是~/sandbox/linux./arch/ power/kernel/setup_32.c。

第1章简要介绍了促使Linux迅速应用于嵌入式环境的因素,同时也介绍了与嵌入式Linux相关的几个重要标准和组织。

第2章介绍了很多嵌入式Linux相关的概念,这些概念是后续几章的基础。

第3章概述了几种较流行的用于搭建嵌入式Linux系统的处理器和平台,介绍了几款主要处理器厂商的重要产品,涉及所有主要的处理器体系结构。

第4章从另一角度探讨了Linux内核。这里没有重点讲解内核理论或其内部原理,只是介绍了内核的结构、布局和构建结构,目的是使读者从一开始就能以自己的方式学习这个庞大的软件工程项目。更重要的是,要知道哪些内容是必须重点关注的。这一章还详细讲解了内核构建系统。

第5章详细说明了Linux内核的初始化过程:把与体系结构和引导加载程序相关的镜像组件拼接成适合下载到闪存的内核镜像,最终通过嵌入式系统的引导加载程序启动。这一章的知识将帮助你定制Linux内核,使之满足你自己的嵌入式应用的需求。

第6章继续详细介绍初始化过程。当Linux内核完成自身的初始化后,应用程序将根据预先确定的方式继续初始化过程。读完这一章,你就具备了定制用户空间应用程序启动顺序的知识。

第7章主要介绍引导加载程序及其在嵌入式Linux系统中的作用。这一章以现在流行的开源引导加载程序U-Boot为例说明了移植的概念;还简要介绍了其他几种现在使用的引导加载程序,以便用户有特殊需求时可以有多种选择。

第8章介绍了Linux设备驱动程序模型,提供了很多开发设备驱动程序的背景资料,这些资料都在每章结尾的“补充阅读建议”中列出了。

第9章列举了目前嵌入式系统中使用的一些流行的文件系统,包括闪存设备上最常用的JFFS2文件系统。这一章还简要介绍了如何创建你自己的文件系统镜像,这也是嵌入式Linux开发人员所面临的一项艰巨任务。

第10章介绍了MTD(Memory Technology Device,内存技术设备)子系统。MTD是Linux文件系统和硬件内存设备(尤其是闪存)之间一种非常有效的抽象层。

第11章介绍了BusyBox,它是构建小型嵌入式系统最常用的工具。这一章讲述如何根据特殊需求来配置和构建BusyBox,随后介绍了仅使用BusyBox环境完成系统初始化的全过程。附录A列举了最新版本BusyBox提供的命令。

第12章详细介绍了典型交叉开发环境的特殊需求。这一章所介绍的一些技术能有效地提高嵌入式开发人员的工作效率,例如强大的NFS根目录挂载开发配置。

第13章介绍了一些有用的开发工具。这一章介绍了使用gdb进行调试,包括核心转储分析;并通过示例介绍了strace、ltrace、top和ps,以及内存性能评测工具mtrace和dmalloc。这一章最后介绍了几个更重要的二进制实用工具,如强大的readelf程序。

第14章深入探讨了很多Linux内核的调试技术,介绍了内核调试器KGDB的用法,提出了许多gdb和KGDB组合使用的调试技巧。这一章涉及的内容还包括硬件JTAG调试器的用法,以及当内核无法启动时的一些故障分析技巧。

第15章把调试环境从内核转移至应用程序。这一章继续完善前两章用到的gdb示例,讲述了多线程和多进程的调试技巧。

第16章取代了第1版的第16章(移植Linux)。那一章的内容已经过时了,如果要在现代内核中恰当地讨论其主题,则需要专门写一本书。我觉得你会对新版的第16章有兴趣的,这一章涵盖了常用的构建完整嵌入式Linux发行版的构建系统。我们会介绍OpenEmbedded,它已经在商业和其他开源项目中获得了极大的关注。

第17章介绍了嵌入式Linux中一个令人激动的发展:通过PREEMPT_RT选项来配置系统的实时性。这里介绍的特性通过RT选项得以实现,同时还介绍了如何在设计中使用这些特性。这一章也介绍了衡量应用程序配置延时的技巧。

第18章以简单易懂的语言描述了USB子系统。我们介绍了一些概念和USB拓扑结构,接着给出几个USB配置的例子。我们会详细分析sysfs和USB的作用,以帮助你理解这个功能强大的系统。我们还会介绍几个有助于理解USB和解决USB故障的工具。

第19章解密了udev这个强大的系统配置工具。我们分析udev的默认行为,并以此为基础来理解如何对它进行定制。我们会给出几个实际的例子。对于BusyBox的使用者,我们会考察BusyBox自带的mdev工具。

本书附录包含U-Boot配置命令、BusyBox命令、SDRAM接口注意事项、开源开发者的资源、BDI-2000调试器的配置文件范例。BDI-2000是目前很流行的硬件JTAG调试器。

如果你能边看书边在你喜欢的Linux工作站上动手实验,将会从书中得到最大的收获。你可以找一台较旧的x86计算机完成嵌入式系统实验。如果有条件能连接其他体系结构的单板计算机进行实验就更好了。BeagleBorad开发板是一个可以进行实验的物美价廉的平台,书中的好几个例子都基于这个平台。通过学习这个大型代码库(Linux内核)的布局和组织结构,你将获益良多,并且能够在研究内核和边学边做的过程中获得大量的知识和经验。

看一下本书使用的代码并试着理解书中的示例,要使用不同的设置方案、配置选项和不同的硬件设备进行实验。除可获得丰富的知识,还充满了乐趣!如果你也这么想,请登录本书网站www.embeddedlinuxprimer.com免费注册一个账号,添加些内容和评论,在这个逐步壮大的Linux社区中分享你自己的成功故事和解决方案。你分享的内容会帮助其他人学习。这是一项不断完善的工作,你的参与会使其成为一个有价值的社区资源。

本书使用的部分开源代码的版权归很多个人或公司所有。复制代码遵循了GPL,即GNU公共许可。


首先,我必须感谢高级策划编辑Debra Williams Cauley,他指导有方,经验丰富,而且很有耐心。没有他,本书就不会出版。

非常感谢我的评审团队:Robert P.J. Day、Sandy Terrace、Kurt Lloyd、Jon Masters和丛书编辑Arnold Robbins。他们对本书质量提升所做的贡献,让我难以言表。

还要感谢Mark A. Yoder教授和他的嵌入式Linux课程,他在课堂上使用了本书的手稿,全面检验了这些内容。

特别感谢飞思卡尔半导体公司提供了硬件平台,书中的很多例子都基于这个平台。如果没有Kalpesh Gala帮忙安排,我就享受不到这样的支持。

还要感谢Embedded Planet和Tim Van de Walle,他们也提供了硬件平台供本书使用。

本书撰写过程中,有几个人给予了我很大帮助,他们提出建议并解答我的问题:Cedric Hommbourger、Klaas van Gend、George Davis、Sven-Thorsten Dietrich、Jason Wessels和Dave Anders(排名没有先后次序)。

我还想感谢这本书的制作团队,我的日程安排不够准确给他们添了麻烦。他们是Alexandra Maurer、Michael Thurston、Jovana San Nicolas Shirley、Gayle Johnson、Heather McNeill、Tricia Bronkella和Sarah Kearns。

出版本书是个很大的项目,有无数人提供了帮助,他们及时回答问题,跟我在交谈中提出想法。这些人太多了,无法一一列举。但他们对我有意或无意的帮助都是我真心感激的。

在本书第1版中,我特别感谢了Cary Dillman,因为每当写完一章,她就会不辞劳苦地审读。她现在是我心爱的妻子了。在第2版的写作过程中,Cary继续支持我,给了我必要的灵感、耐心,作出了不少牺牲。


我由衷地敬佩开源软件工程师的崇高精神,深深地折服于我们社区中远远超过我的天才们。在本书的撰写过程中,我向Linux和开源社区的很多人提出了大量问题,大多数问题都能很快得到回答,而且还经常获得鼓励。我要向Linux和开源社区中帮我解答问题的朋友致以真挚的谢意(排名不分先后):

Dan Malek为第2章的部分内容提供了创作灵感。

Dan Kegel和Daniel Jacobowitz耐心地帮我解答了关于工具链的问题。

Scott Anderson 提供了第14章中gdb宏的最初思想。

Brad Dixon不断用他所掌握的知识挑战和扩展我的技术洞察力。

George Davis帮我解答了ARM的问题。

Jim Lewis为我提供了关于MTD的意见和建议。

Cal Erickson帮我解答了关于gdb用法的问题。

John Twomey就第3章内容给出了建议。

Lee Revell、Sven-Thorsten Dietrich和Daniel Walker就实时Linux的内容提供了建议。

非常感谢AMCC、Embedded Planet、Ultimate Solutions和United Electronic Industries公司,它们提供了示例硬件。感谢我的公司Monta Vista Software,因为写这本书偶尔会从工作中分点儿心,而且公司还为某些示例提供了软件支持。在创作过程中,还有很多人贡献了他们的想法,并给予我鼓励和支持,我也非常感激!

我要诚挚地感谢最初审阅本书的团队,他们迅速阅读了每一章,提供了极好的反馈、意见和想法。谢谢Arnold Robbins、Sandy Terrace、Kurt Lloyd和Rob Farber。还要感谢Arnold教给我这个写作新手撰写技术图书的规则。虽然我已经努力减少错误,但错误肯定还会存在,这都得归咎于我。

感谢Mark L.Taub使本书得以完成,感谢他的鼓励和无限的耐心。还要感谢制作团队,包括Kristy Hart、Jennifer Cramer、Krista Hansing和Cheryl Lenser。

最后,把最特别、最衷心的感谢献给Cary Dillman,在我撰写本书时她阅读了每一章,整个创作过程中都有她的不断鼓励和重要的贡献。


本章内容

很多老牌嵌入式操作系统公司纷纷抛弃专有嵌入式操作系统,而这一举动一般都会在公司内部引发不少争论。出于各方面的考虑,许多产品都采用Linux作为其操作系统,这些产品的种类繁多,超出了Linux占据传统优势的服务器领域。手机、DVD播放器、电子游戏机、数码相机、网络交换机和无线网络设备都在使用嵌入式系统。在你家里或汽车里多半也会有Linux的身影。Linux已经成为很多设备的嵌入式操作系统,包括机顶盒、高清电视、蓝光DVD播放器、汽车的信息娱乐中心和很多其他日常使用的电器。

凭借经济和技术方面的诸多优势,Linux正被越来越多的嵌入式设备所使用。几乎在所有的市场和技术领域都能发现这种趋势。Linux已经被很多重要的嵌入式产品所采用,包括遍布世界的公共电话交换网、全球数据网络、手机、无线基站控制器,以及管理这些无线蜂窝网络的通信基础设施。Linux在众多领域都取得了成功,包括汽车车载设备、消费电子产品(比如游戏机和PDA)、打印机、企业级交换机和路由器以及其他很多产品。全世界内置Linux操作系统的手机数以亿计。Linux在嵌入式系统市场的占有率越来越高,目前来看这一趋势还将继续。

以下是嵌入式Linux增长的几个原因。

出于这些原因,我们看到Linux正加速渗透到众多的日常用品之中,范围涵盖了从高清电视到手机等多种产品。

Linux在嵌入式领域已经取得了长足的进步,这一点并不让人感到惊讶。实际上,阅读本书就已表明Linux已经影响了你的生活。嵌入式Linux的市场规模难以估量,因为很多公司仍然在继续打造它们自己的嵌入式Linux发行版。

LinuxDevice.com是一个广受欢迎的新闻和资讯门户网站(它由Rick Lehrbaum创建,现在属于Ziff Davis),这个网站每年会开展一次嵌入式Linux的市场调查。其最近的调查报告显示,Linux已经成为占据主导地位的嵌入式操作系统,每年都有数千种新产品使用Linux。实际上,有超过半数的调查对象表示他们在嵌入式产品设计中使用了Linux。报告同时显示,仅有大约八分之一的调查对象使用排名第二的操作系统,而那些曾经统治嵌入式市场的商业操作系统的使用率还不到十分之一。即使你有理由怀疑这些调查结果,但没有人能够忽视现今嵌入式Linux市场的蓬勃生机。

Linux是开源软件,这是促使Linux广泛使用的一个重要因素。如果你想了解更多开源运动的历史和文化,请看Eric S. Raymod的书(见本章末尾),该书引人入胜且富有见地。

Linux内核基于GNU GPL[1](General Public License,通用公共许可证)的条款进行授权,这导致了一个常见的误区:Linux是免费的。事实上,GNU GPL第3版[2]的第2段声明:“当我们谈论自由软件时,我们指的是自由,而不是指价格上的免费。”大多数的职业开发经理都同意:你可以免费下载Linux,但是在一个嵌入式平台上开发和部署任何操作系统都是有代价的(这个代价通常很大)。在这方面,Linux并不例外。

[1] See http://www.gnu.org/licenses/gpl.html for complete text of the license.

[2] 目前Linux内核源码仍采用GNU GPL第2版。——编者注

GPL非常简短且通俗易懂。这里列出了它的一些重要特点。

如果软件是基于GPL条款发布的,它必须永远附带这个许可证[3]即使代码被大幅改动(这是许可证允许甚至是鼓励的),GPL要求改动后的代码也必须以相同的许可证发布。这样做的目的是为了保证软件的自由使用,包括修改后的软件(或通常所说的派生软件)的自由使用。

[3] 如果所有的版权持有人能够达成一致的话,理论上,这个软件可以基于新的许可证发布。实际上,出现这种情况的可能性很小,特别是对于那些由数千人共同参与开发的大型软件。

不管软件是如何获取的,GPL允许无限制地分发该软件,而无须支付任何专利费或按件收取的许可费。这并不意味着软件厂商不能够对GPL软件收费——收费是合理和普遍的商业行为。这表明一旦拥有GPL软件,你可以修改和重新分发这个软件,不管这个软件是否被修改过。然而GPL规定,软件的修改者如果决定发布修改后的软件,则必须以GPL的条款发布。无论以什么形式发布派生软件,比如交付给客户,都必须遵守这个规定。

在讨论开源软件的自由特性(free nature)时,常常会提及两个流行的短语:“free as in freedom”和“free as in beer”(本书作者非常喜欢后者)。[4]GPL的存在保证了软件的自由。它确保了你使用、学习和修改这个软件的自由。它也确保了当你分发修改后的代码给某个人时,他同样也获得这些自由。这个概念已经被广泛接受和理解。

[4] 前一个短语中free的意思是自由,后一个短语中free的意思是免费。——译者注

很多人对Linux存在一个误解,那就是Linux是免费的。你可以免费获得Linux,你也可以花几分钟的时间下载一个Linux内核。然而,正如任何一个职业开发经理所理解的,在产品设计中使用任何软件都是有一定代价的。这些代价包括软件的获取、整合、修改、维护和支持。除此之外,你还需要花费其他费用,从而获得和维护一个配置正确的工具链、程序库、应用程序以及和你选择的硬件架构兼容的专用交叉开发工具。很快你会发现,为了开发和部署嵌入式Linux系统,配置其所需的软件工具和开发环境并不是件轻松的事情。

在Linux不断获得桌面、企业和嵌入式等细分市场份额的同时,为推动用户使用和接受Linux,一些新标准和新组织也应运而生。本节介绍一些读者应该了解的标准。

对于一个Linux发行版的维护者来说,也许关系最紧密的标准莫过于Linux标准基础(Linux Standard Base,LSB)。LSB的目标是建立一套设计良好的标准,以提升应用程序在不同Linux发行版之间的互操作性。目前,LSB的标准涵盖了好几种硬件架构,包括IA32/64、32位和64位Power架构,以及AMD64等。标准分为核心部分和单独的硬件架构部分。

LSB规定了Linux发行版的公共属性,包括目标文件的格式、标准库的接口、命令和实用工具的最小集合以及它们的行为、文件系统布局、系统初始化等。

通过本章末尾的网址可以了解有关LSB的详细信息。

据其网站所述,Linux基金会是“一个致力于促进Linux发展的非营利组织”。Linux基金会赞助Linux创始人Linus Torvalds的工作。Linux基金会还赞助了几个工作组,帮助他们制定标准和参与开发针对很多重要Linux平台的新功能。接下来的两节介绍一些由该组织发起的项目。

世界上很多大型的网络和通信设备制造商都在开发或销售采用Linux操作系统的电信级设备。电信级设备的重要特征包括高可靠性、高可用性和快速的可服务性。这些厂商设计的产品采用冗余可热交换的架构、具备容错和集群化特点,并且常常具有实时性能。

Linux基金会的电信级Linux工作组制定了一个规范,其中定义了电信级设备必须满足的一组需求。这个规范的当前版本涵盖了7个功能领域。

全球市场上已经有几款基于嵌入式Linux设计和生产的手机。据各方报道,市面上已有上亿部手机采用Linux作为操作系统平台。唯一可以肯定的是这个数量还在继续增加。原本由专有实时操作系统统治的阵地,有望成为Linux发展最为迅猛的细分市场。Linux已整装待发,吹响了进军商业嵌入式应用领域的号角。

Linux基金会赞助了一个原名为移动Linux计划(Mobile Linux Initiative)的工作组,这个工作组现在叫做Moblin。据Linux基金会的网站介绍,这个工作组的目标是推动Linux在移动设备中的使用,包括下一代手机和其他融合语音/数据的便携设备。这个工作组关注的领域包括开发工具、I/O和网络、内存管理、多媒体、性能、电源管理、安全和存储。Moblin的网址是http://moblin.org。你可以尝试某个Moblin版本,例如Fedora/Moblin(网址为http://fedoraproject.org/wiki/Features/ FedoraMoblin)或者Ubuntu Moblin remix,作者的Dell Mini 10上网本上就安装了后面这个版本。

嵌入式Linux的版图还在不断扩张。在准备这一版的内容时,Moblin项目和Maemo项目已经合并成MeeGo。可以从http://meego.com/了解MeeGo的更多信息,甚至可以下载一个MeeGo镜像试验一下。

如果你正致力于打造需要具备高可靠性、可用性和可服务性(Reliability、Availability、Serviceability, RAS)的产品,你就应该知道服务可用性论坛(SA Forum)。这个组织定义了一组公共接口用于电信设备和其他商业设备的系统管理。这个组织的网址是www.saforum.org

嵌入式Linux已经取得了胜利。实际上,你的汽车或家中很可能就有嵌入式Linux设备。这一章仔细考察了下列现象的产生原因。

The Cathedral and the Bazaar(《大教堂与市集》),Eric S. Raymond,O’Reilly Media公司,2001。

 

Linux 标准基础项目

http://www.linuxfoundation.org/collaborate/workgroups/lsb

 

Linux 基金会

http://www.linuxfoundation.org/


本章内容

通常,理解特定任务的最佳途径是从全局角度认识它。很多基本概念都会给嵌入式系统开发的新手带来挑战。这一章将带领你观摩一个典型的嵌入式系统及其开发环境,重点介绍那些让开发这类系统变得独特和富有挑战的概念和组件。

嵌入式系统具有一些关键属性。你可能不会认为桌面PC是一个嵌入式系统。但在远程数据中心运行的桌面PC很可能就是嵌入式系统,这个平台完成至关重要的监控和报警任务。而且假设这个数据中心是无人值守的,那我们对这个硬件平台的需求就不一样了。例如,如果断电后接着电力恢复,你会希望该硬件平台在没有操作人员干预的情况下继续执行它的任务。

嵌入式系统的种类很多,形状大小各异,大到多机架数据存储中心或网络动力站,小到MP3播放器或手机。下面列出嵌入式系统的一些常见特性。

与传统的桌面PC相比,嵌入式系统的资源很有限。嵌入式系统常常只有很少的内存,小容量的硬盘驱动器(或是没有硬盘驱动器),有时还没有外部的网络连接。常常可以看到,一个系统仅有的用户界面就是一个串行端口加上几个发光二极管。诸如此类的问题会给嵌入式开发者带来挑战。

桌面电脑刚加电时,一个叫做BIOS的软件程序立刻获得了处理器的控制权。(历史上,BIOS是Basic Input/Output Software的缩写,但现在这个单词已经有了自身的含义,因为其完成的功能比以前复杂多了。)BIOS可能实际存储在一块闪存中(稍后会介绍闪存),便于升级BIOS程序。

BIOS是一个复杂的系统配置软件,它拥有硬件架构的底层信息。大多数人都不清楚BIOS涉及的硬件范围和功能,但它是桌面电脑的重要组成部分。当电脑加电时,BIOS首先获得处理器的控制权。它的主要任务是初始化硬件,特别是内存子系统,并且从PC的硬盘驱动器中加载操作系统。

在典型的嵌入式系统中(假设这个系统不是工业标准的x86 PC硬件平台),引导加载程序(bootloader)完成与BIOS相同的功能。对于定制嵌入式系统,你必须在开发计划中预留出时间,开发针对具体硬件板卡的引导加载程序。幸运的是,有几个很好的开源引导加载程序可供选择,你可以按照项目需求进行定制。这些内容将在第7章介绍。

下面列出一些引导加载程序在系统加电时完成的重要任务。

这里只是简单概括了在通常的嵌入式系统中引导加载程序所完成的任务。需要记住的是:如果你的嵌入式系统基于定制的硬件平台,这些引导加载程序的功能必须由你,也就是系统的设计者来提供。如果你的嵌入式系统基于商用现货(Commercial Off-The-Shelf,COTS),比如ATCA机架[1],那么其引导加载程序(通常还有Linux内核)一般就包含在硬件板卡中了。第7章将会更深入地讨论引导加载程序。

[1] ATCA平台会在第3章介绍。

图2-1是一个典型嵌入式系统的框图。这个例子很简单,描述了一个系统的高层硬件架构,无线接入点设备可能就是采用这种硬件构架。这个系统架构以一个32位的RISC处理器为中心,系统中的闪存用于存储非易失性程序和数据,主存储器是SDRAM(同步动态随机存储器),其容量可以从几兆至几百兆字节,视应用而定。一个通常由电池供电的实时时钟模块记录着当前时间(包括日期)。这个例子里面包含以太网和USB接口,也包含串行端口,利用串行端口可基于RS-232标准访问控制台。802.11芯片组或模块实现了无线调制解调器的功能。

图2-1 嵌入式系统

通常,嵌入式系统的处理器完成很多功能,不仅仅是处理传统的核心指令流。图2-1中的假想处理器包含集成的串行接口UART、集成的USB和以太网控制器。很多处理器都包含集成在处理器中的外设,有时这些处理器被称为片上系统(SOC,System On Chip)。第3章会考察几个集成处理器的例子。

嵌入式Linux开发新手经常提出的一个问题,就是开发之前需要准备些什么。为了回答这个问题,图2-2展示了一个典型的嵌入式Linux开发环境。

图2-2 典型的嵌入式Linux开发环境

图中展示了一个主机开发系统,其中运行你最喜欢的桌面Linux发行版,比如Red Hat、SUSE或Ubuntu Linux。嵌入式Linux目标板通过一根RS-232串行端口线与开发主机相连。目标板的以太网接口插接到本地以太网集线器或交换机上,开发主机也通过以太网连接到上面。开发主机包含开发工具和程序以及目标文件,通常这些都可从一个嵌入式Linux发行版中获得。

在这个例子中,主机和嵌入式Linux目标板主要通过一个遵循RS-232标准的串行端口连接。主机上运行的串行端口终端程序用于和目标板通信。minicom是最常用的串行端口通信应用程序之一,几乎所有的桌面Linux发行版中都有这个应用程序[2]。本书使用screen作为串行端口通信程序,这个程序可以取代minicom的功能,而且更灵活,特别是在trace捕捉方面。对于系统启动或解决故障时串行端口线上的垃圾信息,screen也更加宽容。为了在USB转串行端口线上使用screen,可以在主机终端调用它并指定速率:

[2] 你也许需要在自己的发行版上安装minicom。例如,在Ubuntu上,可以使用命令sudo apt-get install minicom来安装minicom。

第一次加电时,目标板上的引导加载程序立即获得处理器的控制权。该程序执行一些非常底层的硬件初始化,包括处理器和内存的设置,初始化UART用于控制串行端口,以及初始化以太网控制器。代码清单2-1显示了目标板加电后从串行端口接收到的字符。在这个例子中,我们选择了飞思卡尔半导体公司的目标板PowerQUICC III MPC8548 可配置开发系统(Configurable Development System,CDS)。这个开发系统包含了PowerQUICC III MPC8548处理器。这个目标板从飞思卡尔出厂时就预装了U-Boot引导加载程序。

代码清单2-1 引导加载程序从串行端口输出的初始信息

当MPC8548CDS目标板加电时,U-Boot执行一些底层的硬件初始化,包括配置串行端口,然后打印标题行,见代码清单2-1中显示的第一行。接着显示了CPU和核心(Core)的型号及版本,接下来是描述时钟配置和缓存配置的数据,再后面是一串文字描述了这个目标板。

初始的硬件配置完成后,U-Boot根据其静态设置来配置其他硬件子系统。这里,我们看到U-Boot配置了I2C、DRAM、闪存(FLASH)、2级缓存(L2 cache)、PCI和网络子系统。最后U-Boot等待来自串行端口控制台的输入,显示为命令行提示符“=>”。

现在U-Boot已经初始化了硬件、串行端口和以太网接口,在其短暂但有益的生命中还剩一件工作:加载并引导Linux内核。所有的引导加载程序都提供了命令用于加载和执行操作系统镜像。代码清单2-2显示了使用U-Boot手动加载并引导Linux内核的一种常用方法。

代码清单2-2 加载Linux内核

代码清单2-2开头的tftp命令指示U-Boot使用TFTP[3]协议将内核镜像uImage通过网络加载到内存。在这个例子中,内核镜像存放于开发工作站(通常,这个开发工作站就是通过串行端口与目标板相连的那台主机)。执行tftp命令时,需要传入一个地址参数,这个地址用于指定内核镜像将要被加载到的目标板内存的物理地址。读者现在不用担心这些细节,第7章将会详细介绍U-Boot。

[3] 我们会在第12章介绍这些协议和相关服务器程序。

第2次执行tftp命令加载了一个目标板配置文件,称为设备树(device tree),这个文件还有其他的名字,包括扁平设备树(flat device tree)和设备树二进制文件(device tree binary)或dtb。你将在第7章了解到这个文件的更多信息。现在,你只要知道这个文件与具体目标板相关,包含了内核所需的用于引导目标板的信息就足够了。这些信息包括内存大小、时钟速率、板载设备、总线和闪存布局。

接着,执行bootm(从内存镜像引导)命令来让U-Boot引导刚才加载至内存的内核,起始地址就是在tftp命令中指定的地址。在这个使用bootm命令的例子中,我们让U-Boot加载放在地址0x600000处的内核,并将加载到地址0xc00000处的设备树二进制文件(dtb)传给内核。bootm命令会将控制权移交给Linux内核。假设内核配置正确,这个命令的结果是引导Linux内核直至在目标板上出现控制台命令行提示符,如同登录提示符所示。

注意bootm命令为U-Boot敲响了丧钟。这是一个重要的概念。与桌面PC的BIOS不同,大多数的嵌入式系统都采用这样一种架构:当Linux内核掌握控制权时,引导加载程序就不复存在了。Linux内核会要求收回那些之前被引导加载程序所占用的内存和系统资源。将控制权交回给引导加载程序的唯一方法就是重启目标板。

最后还需要注意一点。在代码清单2-2的串行端口输出中,下面这行之前的信息(包含这一行)都是由U-Boot引导加载程序产生的:

其余引导信息是由Linux内核产生的。对于这一点,我们在后续章节还要详细说明,但我们需要注意U-Boot是在哪儿离开的以及Linux内核是在哪儿取得控制权的。

当Linux内核开始执行时,它会在其相当复杂的引导过程中输出大量状态消息。在当前讨论的这个例子中,在显示登录提示符之前,Linux内核大约显示了200行printk[4]打印信息(代码清单中省略了这些打印行以便讨论的重点更加清晰)。代码清单2-3再现了登录提示符之前的最后几行输出。这个练习的目的不是要深入到内核初始化的细节中去(第5章会讲述这方面的内容),而是要对正在发生的事情,以及对嵌入式系统中引导Linux内核需要哪些组件有一个概览。

代码清单2-3 Linux内核加载的最后几行引导消息

[4] printk()是一个内核函数,负责将消息打印到系统控制台上。

Linux在串行端口终端上显示登录提示符之前,会挂载一个根文件系统。在代码清单2-3中,Linux通过一系列必要步骤,从一个NFS[5] 服务器来远程(通过以太网)挂载其根文件系统,这个NFS服务器程序运行于IP地址为192.168.0.9的主机之上。通常这个主机就是你的开发工作站。根文件系统包含构成整个Linux系统的应用程序、系统库和工具软件。

[5] 我们会在第12章中介绍NFS和其他相关服务器程序。

重申一下这里讨论的重点:Linux必须有一个文件系统。很多老式的嵌入式操作系统不需要文件系统,因此那些从老式嵌入式操作系统迁移到嵌入式Linux系统的工程师往往会感到惊讶。一个文件系统由一组预定义的系统目录和文件组成,这些目录和文件按照特定的布局存储在硬盘或其他存储介质上,而Linux内核可以挂载这些介质作为其根文件系统。

注意Linux也可以从其他设备挂载根文件系统。最常见的情况当然是挂载一个硬盘分区作为根文件系统,就像笔记本或工作站中的Linux系统所做的那样。实际上,当你将嵌入式Linux小玩意带出房门或远离开发环境时,NFS就没什么用处了。然而,在读这本书的过程中,你会逐渐体会到在开发环境中挂载NFS根文件系统带来的威力和灵活性。

继续探讨其他内容之前,还有一个重点需要强调一下。请注意代码清单2-3中的这一行:

直到这时,内核都是自己在执行代码,它在一个称为内核上下文(kernel context)的环境中完成大量的初始化工作。在这个运行状态下,内核拥有所有的系统内存并且全权控制所有的系统资源。内核能够访问所有的物理内存和所有的I/O子系统。它在内核虚拟地址空间中执行代码,使用一个由内核自己创建和支配的栈。

当内核完成其内部初始化并挂载了根文件系统后,默认会执行一个名为init的应用程序。内核一启动init,它随即进入用户空间(user space)或用户空间上下文运行。在这个运行状态下,用户空间进程对系统的访问是受限的,必须使用内核系统调用(system call)来请求内核服务,比如设备和文件I/O。这些用户空间进程或程序,运行在一个由内核随机[6]选择和管理的虚拟内存空间中。在处理器中专门的内存管理硬件的协助下,内核为用户空间进程完成虚拟地址到物理地址的转换。这种架构的最大好处是某个进程中的错误不会破坏其他进程的内存空间。这是老式嵌入式系统的一个普遍缺陷,会产生那些最难查找的故障。

[6] 实际上,这并不是随机的,但是对于这里讨论的内容,也可能是这样。我们会在后面详细讨论这个主题。

对这些概念不熟悉也不用惊慌。本节的目标只是提纲挈领地作个介绍,在此基础上,你会在阅读本书的过程中逐步获得更加深入的理解。后续章节将详细解释这些概念。

嵌入式Linux开发的一大挑战性源自大多数嵌入式系统的物理资源非常有限。虽然你的台式电脑会拥有酷睿2双核处理器和500 GB大小的硬盘,但很难找到拥有如此巨大硬盘容量的嵌入式系统。多数情况下,硬盘通常被更小和更便宜的非易失性存储设备所取代。硬盘不仅笨重,包含旋转部件,对物理震动敏感,并且要求提供多种供电电压,因此并不适合用在许多嵌入式系统中。

几乎所有人都对消费电子设备,比如数码相机和PDA(这两者都是很好的嵌入式系统的例子)中广泛使用的Compact Flash卡和SD卡很熟悉。这些基于闪存技术的模块可以看做是固态硬盘,它们能够在很小的空间内存储许多兆甚至几吉字节的数据。它们内部没有活动的部件,相对坚固,只需一种供电电压。

生产闪存的厂家有好几家。闪存的类型多种多样,电气规格、物理封装形式以及容量各有不同。只拥有小到4 MB或8 MB非易失性存储容量的嵌入式系统并不罕见。嵌入式Linux系统对存储容量的典型需求是16 MB~256 MB。越来越多的嵌入式Linux系统拥有数吉字节的非易失性存储空间。

闪存可以在软件的控制下写入和擦除数据。采用旋转硬盘驱动器技术的普通硬盘仍然是最快的可写入存储媒介。虽然和普通硬盘相比,闪存的写入和擦除仍然相当慢,但与以前相比,其写入和擦除速度已经有了显著提高。了解硬盘驱动器和闪存技术的根本区别才能正确地使用相应技术。

闪存的存储空间被分割成相对较大的可擦除单元,称为擦除块(erase block)。闪存的一个显著特征就是闪存中的数据写入和擦除的方式。在典型的NOR型[7]闪存芯片中,数据可以在软件的控制下,使用直接向某个存储单元地址写入的简单方法将其从二进制1改为二进制0。然而,要将数据从0改回1,则要擦除整个擦除块,在擦除时需要向闪存芯片写入一串特别的控制指令序列。

[7] 闪存技术有很多种。NOR型闪存是小型嵌入式系统中最常用的一种闪存。

典型的NOR型闪存包含多个擦除块。例如,一个4 MB容量的闪存芯片可能包含64个擦除块,每块大小为64 KB。市面上也有擦除块大小不一致的闪存,以便灵活存放数据。这种闪存常常被称为引导块(boot block)或引导扇区(boot sector)闪存。通常,引导加载程序存储在较小的块中,内核和其他必要的数据则存放在更大的块中。图2-3说明了一个典型的顶部引导(top boot)闪存芯片的块大小布局。

图2-3 引导块闪存的架构

为了修改存储在闪存阵列中的数据,必须完全擦除待修改数据所在的块。即使只修改某个块中的一个字节,都必须擦除并重新写入整个块[8]。相比于传统硬盘的扇区,闪存的块大小相对较大。相对而言,一个典型的高性能硬盘的可写扇区大小为512 B或1024 B。结果显而易见:更新闪存中的数据所耗费的写入时间会是硬盘驱动器的很多倍,部分原因就是每次更新数据时都有相对大量的数据需要被擦除和写回。在最坏的情况下,一个写周期会耗费几秒钟的时间。

[8] 记住,你可以一次将一个字节从1改成0,但如果想将某个比特从0改回1,则必须擦除整个块。

关于闪存,另一个需要考虑的限制是存储单元的写寿命(write lifetime)。NOR型闪存存储单元的可写入次数是有限制的,超出次数限制后写入就会失败。虽然这个次数的数值比较大(典型的写入次数限制是每块100 000次),但不难想象一个设计很差的闪存存储算法(甚至是一个软件故障)会迅速毁坏闪存设备。显然,应该避免配置你的系统日志输出到闪存。

NAND型闪存使用一种相对较新的闪存技术。当NAND型闪存投放市场时,前一节介绍的传统闪存就被称为了NOR型闪存。它们之间的区别与闪存存储单元的内部架构有关。NAND型闪存设备通过提供更小的块尺寸改进了传统(NOR型)闪存的一些限制,它可以更快更有效地进行写操作,同时大大提高了闪存阵列的使用效率。

NOR型闪存为微处理器提供的接口方式与很多微处理器外围设备的做法类似。也就是说,它们有并行的数据和地址总线,直接[9]连接到微处理器的数据/地址总线上。闪存阵列的每个字节或字(word)可以随机寻址。相反,NAND型闪存设备是通过复杂的接口串行访问的,而且这些接口因厂商而异。NAND型闪存设备的操作模式更类似于传统的硬盘驱动器加上附带的控制器。数据是以串行突发(burst)方式访问的,每次突发访问的数据量远远小于NAND型闪存的块大小。相比于NOR型闪存,虽然NAND型闪存的写入时间要少很多,但其写寿命却比NOR型闪存高出一个数量级。

[9] 这里的直接是逻辑上的概念。实际的电路有可能会包含总线缓存(bus buffer)或桥接(bridge)设备等。

总的来说,NOR型闪存可以被微处理器直接访问,甚至于代码可以直接在NOR型闪存中执行。(然而,因为性能方面的原因,很少有人这样做,除非系统资源极其匮乏。)实际上,很多处理器都不能像对待DRAM一样缓存(cache)访问闪存的指令。这进一步降低了代码的执行速度。相反,NAND型闪存更适合于以文件系统的格式大容量存储数据,而不是撇开文件系统,直接存储二进制可执行代码和数据。

有多种闪存布局和使用方法可供嵌入式系统的设计者选择。在最简单的系统中,资源没有过度受限,可以将原始的二进制数据(可能是压缩过的)存储在闪存设备中。系统引导时,存储在闪存中的文件系统镜像被读入Linux内存磁盘(ramdisk)块设备中。这个块设备由Linux挂载为一个文件系统,并且只能从内存中访问。当闪存中的数据几乎不需要更新时,这种方式通常是很好的选择。相比于内存磁盘的容量,需要更新的数据量是很少的。但是,当系统重启或断电时,对内存磁盘中文件的修改会丢失,务必牢记这一点。

图2-4说明了一个简单嵌入式系统中的典型闪存组织结构。在这个系统中,动态数据对非易失性存储的需求很少且更新不频繁。

图2-4 典型的闪存布局

引导加载程序通常存放在闪存阵列的顶部或底部。引导加载程序之后的存储空间被分配给Linux内核和内存磁盘文件系统镜像[10],这个镜像中包含了根文件系统。一般来说,Linux内核和ramdisk文件系统镜像都被压缩过,并由引导加载程序在系统引导时解压。

[10] 我们会在第9章详细介绍内存磁盘文件系统。

可以在闪存中专门开辟一小块区域,或者使用其他类型的非易失性存储设备[11]来存放那些重启或掉电后仍需保留的动态数据。对于需要保存配置数据的嵌入式系统,这种方式很常见。例如,针对消费者市场的无线接入点设备可能采用这种方式。

[11] 如果只需存储少量数据,可以选择实时时钟模块或串行EEPROM作为非易失性存储设备。

刚才描述的简单闪存布局策略有局限性,但可以通过使用闪存文件系统来克服。闪存文件系统以类似于硬盘驱动器组织数据的方式来管理闪存设备中的数据。早期针对闪存设备的文件系统包含简单的块设备层,这个块设备层模拟了普通硬盘驱动器的扇区布局,扇区大小为512 B。这些简单的模拟层允许以文件格式而不是无格式的大容量存储方式来访问数据,但是它们有一些性能上的局限。

对闪存文件系统的一个主要改进就是引入了耗损均衡(wear leveling)算法。如前所述,闪存块的写寿命是有限的。耗损均衡算法用来将写操作均匀分布到闪存的各个物理擦除块上,以延长闪存芯片的寿命。

闪存架构带来的另一个限制是系统掉电或意外关机后存在数据丢失的风险。闪存的块尺寸相对较大,而写入的文件的平均大小相对于块尺寸通常小很多。从前面的内容我们知道闪存块必须一次写入一整块。因此,为了写入一个8 KB的小文件,必须擦除和重写整个闪存块,而这个块的大小可能是64 KB或128 KB;在最坏的情况下,这个写入会花费几秒钟才能完成。这极大增加了系统掉电后丢失数据的风险。

目前比较受欢迎的一种闪存文件系统是JFFS2,或称为第二代日志闪存文件系统(Journaling Flash File System 2)。这个文件系统有很多重要特性,旨在提升整体性能、延长闪存寿命并降低系统掉电时数据丢失的风险。最新的JFFS2文件系统的最重要改进包括完善耗损均衡、压缩和解压缩(将更多的数据挤进有限的闪存空间),以及对Linux硬连接(hard link)的支持。相关主题将在第9章和第10章详细讲述。在第10章中,我们会讨论内存技术设备(Memory Technology Device, MTD)子系统。

老式嵌入式操作系统通常将系统内存看做一大块线性地址空间,并进行管理。也就说,微处理器的地址空间的下限是0,上限是其物理地址范围的顶部。举例来说,如果一个微处理器有24条物理地址线,其内存范围的上限就是16 MB。因此,其地址范围可以用十六进制表示为从0x00000000到0x00ffffff。硬件设计常常将DRAM放置在这个地址范围的底部,并将闪存放置在顶部。位于DRAM顶部和闪存底部之间的那些未使用的地址范围常常被分配给板上的各种外围设备芯片,用于对它们进行寻址。这种设计方法一般是由所选择的微处理器决定的。图2-5显示了一个简单嵌入式系统中的典型内存布局。

图2-5 典型的嵌入式系统内存布局

在基于老式操作系统的嵌入式设备中,操作系统和所有的任务[12]具有相同的权限,能够访问系统的所有资源。某个进程中的一个故障可能会改写系统中任意一块内存的内容,这块内存可能属于这个进程本身、操作系统、其他任务,甚至是地址空间中的一个硬件寄存器。虽然这种内存管理方式有个最大的优点:简单,但它会导致一些很难诊断的故障。

[12] 在这里的讨论中,“任务”这个词代表任何一个执行线程,而不考虑生成、管理和调度它的机制。

高性能的微处理器中都包含一个复杂的硬件引擎,称为内存管理单元(Memory Management Unit,MMU)。MMU的作用是使操作系统能够在很大程度上管理和控制地址空间,包括操作系统自身的地址空间和分配给进程的地址空间。这种控制主要体现为两种形式:访问权限控制(access right)和内存地址转换(memory translation)。访问权限控制允许操作系统将特定的内存访问权分配给特定的进程。内存地址转换允许操作系统将其地址空间虚拟化,从而带来很多好处。

Linux内核利用这些硬件MMU实现了一个虚拟内存操作系统。虚拟内存所带来的最大的一个好处是,它可以让系统的内存看起来比实际的物理内存多,这样能够更加有效地利用物理内存。其他的好处是,内核在为任务或进程分配系统内存时,可以指定这块内存的访问权限,从而防止某个进程错误地访问属于另一个进程或内核自身的内存或其他资源。

下一节将更详细地讨论MMU的工作原理。复杂的虚拟内存系统的内容超出了本书的范围[13]。实际上,我们会从嵌入式系统开发者的角度来考查虚拟内存系统。

[13] 有很多优秀书籍详细讲述了虚拟内存系统。请参考本章最后一节中的推荐书目。

系统引导时,Linux最先要完成一项琐碎工作,即配置处理器中的硬件MMU以及相应的数据结构,并使之能够进行地址转换。这一步完成后,内核运行于自己的虚拟内存空间中,这个空间称为内核空间。在当前的Linux内核版本中,这个虚拟内存空间的起始地址是由内核开发者选择的,其默认值为0xC0000000[14]。对于大多数硬件架构,这是个可以配置的参数[15]。在内核符号表中,可以看到内核符号的链接地址都是以0xC0xxxxxx开头的。所以,当内核在内核空间中执行代码时,处理器的指令指针(程序计数器)所包含的值都在这个范围之内。

[14] 对于32位CPU而言,内核空间的地址范围是从0xC0000000到0xFFFFFFFF。 ——译者注

[15] 不过,一般不需要修改。

Linux中有两个明显分隔开的运行上下文,由线程[16]的执行环境所决定。那些完全在内核中执行的线程被认为运行在内核上下文中,而应用程序运行在用户空间上下文中。用户空间进程只能访问它自己拥有的内存,如果它要访问文件或设备I/O等特权资源,则必须使用内核系统调用。下面举个例子,让你更好地理解这一点。

[16] “线程”这个词的含义在这里比较宽泛,代表任意一个指令流的序列。

假设一个应用程序打开一个文件并读取其中的内容,如图2-6所示。对读函数的调用是从用户空间开始的,由应用程序调用C库中的read()函数。接着,C库向内核发起一个读请求。这个读请求造成一次上下文的切换,从用户程序切换到内核,以服务这个请求并读取文件中的数据。在内核中,这个读请求最终转变成对硬盘驱动器的访问,从包含文件内容的扇区中读取相应数据。

图2-6 简单的文件读请求

通常,这个对硬盘驱动器的读请求是以异步的形式发往硬件自身的。也就是说,处理器将这个请求发给硬件,并不会等待其完成请求。硬件收到请求后读取数据,当数据准备好的时候,通过中断的方式来告知处理器读请求已经完成了。等待数据的应用程序会阻塞在一个等待队列中,直到有数据可用。当硬盘准备好数据时,它将向处理器发送一个硬件中断(这里只是描述了一个简化的过程)。当内核接收到这个硬件中断时,它会挂起正在执行中的任何进程,并从硬盘驱动器中读取应用程序所等待的数据。

下面对我们的讨论做一个概括,我们学习了两个通用的执行上下文——用户空间和内核空间。当应用程序执行系统调用,造成上下文的切换而进入内核时,内核会代表这个进程执行内核代码。你会经常听到,这种情况称为内核运行于进程上下文中。相反,处理IDE驱动器的中断处理程序(ISR)也是内核代码,但在运行时并不代表任何特定的进程。这种情况通常被称为内核运行于中断上下文中。

内核运行于中断上下文中时会受到一些限制,包括中断处理程序不能够阻塞(睡眠)或调用任何可能造成阻塞的内核函数。如果想要更多地了解这些概念,请阅读本章末尾的参考文献。

一个进程产生时——例如,当用户在Linux的命令提示符后面输入ls的时候——内核就会为这个进程分配内存及相应的虚拟内存地址范围。这些地址与内核中的地址或其他正在运行的进程的地址没有固定的关系。此外,这些进程所看到的虚拟地址跟目标板上的物理内存的地址也没有直接的关系。实际上,由于系统中存在分页(paging)和交换(swapping)机制,一个进程在其生命周期中常常会占用内存的多个不同的物理地址。

代码清单2-4是程序员所熟知的“Hello World”程序,这里做了点修改来说明刚刚讨论的一些概念。这个例子的目的是解释说明内核分配给进程的地址空间。这段代码编译后,在一个拥有256 MB DRAM内存的嵌入式系统上运行。

代码清单2-4 嵌入式风格的Hello World

代码清单2-5显示了运行编译后的程序hello时,控制台输出的信息。注意,hello进程认为它的运行地址位于高地址内存的某个地方,刚好超过256 MB的边界(0x10000418)。还需注意,栈的地址大概处于32位地址空间一半的地方,远远超过了内存的大小256 MB(0x7ff8ebb0)。怎么会这样呢?在这种系统中,DRAM通常是一块连续的内存。乍一看,我们几乎有将近2 GB的DRAM可以使用。这些虚拟地址是由内核分配的,并且有嵌入式目标板上的256 MB的物理内存在背后支持。

代码清单2-5 Hello的输出

虚拟内存系统的一个特点是当可用的物理内存的数量低于某个指定的阈值时,内核可以将内存页面交换到大容量存储媒介中,通常是硬盘驱动器。内核检查正在使用中的内存区域,并判断哪些区域最近使用得最少,然后将这些内存区域交换到磁盘中,并释放这些内存区域给当前进程使用。嵌入式系统的开发者常常会因为性能原因或资源限制而禁用嵌入式系统中的交换功能。多数情况下,使用慢速且写寿命有限的闪存设备作为交换设备是很不明智的。如果没有交换设备可用,就必须仔细地设计应用程序,使其能够运行在有限的物理内存中。

开发嵌入式系统应用和设备驱动之前,需要一套工具(编译器、实用工具等)来生成适合目标系统的二进制可执行文件。考虑一个在桌面PC上编写的简单应用,比如传统的“Hello World”。你在电脑上编写好代码后,会使用电脑操作系统自带的编译器(通常是GNU的gcc编译器)来编译代码,以生成一个可执行的二进制镜像文件。这个可执行文件的格式与编译代码的电脑兼容,可以在该电脑上运行。这被称为本地(native)编译。也就是说,使用本机系统中的编译器生成可以在本机上运行的程序。

需要注意的是,本地编译并不意味着我们就能知道用于编译和运行程序的系统架构。其实,如果你有一个可以在目标板上运行的工具链,就可以在目标板上本地编译生成适合此目标板架构的应用程序。实际上,要对一个新的嵌入式内核和定制单板进行压力测试,一个好办法就是在上面反复编译Linux内核。

在交叉开发环境中开发软件要求编译器运行于开发主机上,但生成的二进制可执行文件的格式与开发主机不兼容,不能在上面运行。这类工具存在的主要原因是,在资源(一般指内存大小和CPU性能)受限的嵌入式系统上本地开发和编译代码常常是不现实或不可能的。

这种开发方式隐藏着很多陷阱,嵌入式开发的新手稍不留神就会中招。当编译一个程序时,编译器一般都知道怎样找到所需要的头文件和正确编译代码必需的程序库。为了说明这些概念,我们再看一下“Hello World”。代码清单2-4中的示例代码是使用下面的命令行进行编译的:

在代码清单2-4中,我们看到这个程序代码包含了一个头文件stdio.h。这个文件和我们在gcc命令行中指定的文件hello.c不在同一个目录中。那么,编译器是如何找到它的呢?另外,函数printf()也不是在文件hello.c中定义的。因此,编译hello.c后,它会包含一个对此符号的未解析的引用(unresolved reference)。链接器在链接时是怎样解析这个引用的呢?

编译器使用一些默认的搜索路径来定位头文件。在代码中引用某个头文件时,编译器在默认的几个搜索路径中查找这个文件。类似地,链接器也是以这种方式来解析对外部符号printf()的引用。链接器知道默认在C库(libc-*)中搜索未解析的引用,并且知道在系统中的哪些位置可以找到这些程序库。再说明一下,这种默认行为是内置于工具链中的。

现在假设你为某个采用Power架构的嵌入式系统编写应用程序。显然,你需要一个交叉编译器,用于生成兼容Power架构处理器的二进制可执行文件。如果你使用交叉编译器,并采用类似的编译命令来编译前面的hello.c程序,在解析对外部符号printf()的引用时,链接器很可能会意外地将二进制可执行文件链接到一个x86版本的C库。当然,由于生成的可执行程序混合了Power架构和x86二进制指令,如果运行这个错误的混合体[17],其结果是可以预见的,那就是系统崩溃!

[17] 实际上,这可能都不会编译或链接成功,更不用说运行了。我们只是想说明这个问题。

摆脱这个困境的方法是指引交叉编译器在非标准路径中进行查找,以使用针对目标架构的头文件和程序库。我们将在第12章中详细讨论这个主题。这个例子旨在说明两种开发环境的区别,即本地开发环境和嵌入式系统所需的交叉编译开发环境。这只是交叉开发环境复杂性的一个方面。交叉调试中也会出现相同的问题和解决方案,从第14章开始,你会了解到这些内容。正确地搭建交叉开发环境对于成功至关重要,你将在第12章中看到,这不仅仅涉及编译器,还包括其他很多内容。

到底什么是Linux发行版? Linux内核完成系统引导后,它会找到并挂载一个根文件系统。一旦合适的根文件系统被成功挂载,启动脚本会启动很多系统需要的程序和实用工具。这些程序一般会调用其他程序来完成具体的任务,例如生成一个登录shell、初始化网络设备接口和运行用户的应用程序。每一个程序都有一些必须由系统中其他成员来满足的具体需求(一般称为依赖关系)。大多数的Linux应用程序都依赖一个或多个系统程序库。还有一些程序需要配置文件和日志文件,诸如此类。总的来说,即使一个小型的嵌入式Linux系统也需要很多文件,这些文件分布在根文件系统的合适的目录中。

完整的桌面Linux系统的根文件系统中包含数千个文件。这些文件来自软件包(package),而软件包通常按照功能来组合文件。软件包由一个包管理器负责安装和管理。红帽公司(Red Hat)的rpm就是一个流行的包管理器,它广泛用于Linux系统中安装、删除和更新软件包。如果你的工作站采用了红帽公司的Linux操作系统,包括Fedora系列,你可以使用命令rpmqa来列出系统中已安装的所有软件包。如果你使用的是基于Debian的发行版,例如Ubuntu,使用命令dpkgl即可获得同样的效果。

一个软件包可以包含很多文件,事实上,有的软件包中包含了数百个文件。一个完整的Linux发行版会包含几百个甚至几千个软件包。下面列出一些嵌入式Linux发行版中可能存在的软件包,并说明它们的功能。

[18] 这个软件包非常重要,我们会用单独一章(第11章)专门讲述它。

这就是Linux发行版的作用。一个典型的Linux发行版包含好几张光盘的内容,其中装满了有用的应用程序、程序库、实用工具和文档等。当Linux发行版安装好之后,用户就可以使用功能完备的系统了,这个系统基于一组默认的配置合理的选项,这些选项也可以根据具体需要进行调整。你可能熟悉某个流行的桌面Linux发行版,比如Red Hat或Ubuntu。

针对嵌入式目标的Linux发行版与一般的桌面发行版之间有很多不同之处。首先,嵌入式发行版中的二进制可执行程序是不能在PC上运行的,它们是针对嵌入式系统所使用的硬件架构和处理器而开发的。(当然,如果你的嵌入式系统使用和PC一样的x86架构,情况可能会有所不同。)桌面Linux发行版通常包含很多面向普通桌面用户的图形用户界面(GUI)工具,例如花哨的图形时钟、计算器、个人时间管理工具和电子邮件客户端等。嵌入式Linux发行版往往会省略这类应用程序,而是更多地提供面向开发者的专用工具,如内存分析工具和远程调试工具等。

另外,嵌入式发行版中一般会包含交叉开发工具,而不是本地开发工具。例如gcc工具链,它运行于采用x86架构的桌面PC,但是会生成可以运行于目标系统的二进制代码,而这个目标系统的架构一般不是x86。这个工具链中的很多其他工具也都是按照类似的方式进行配置的:它们运行于开发主机(通常是一台x86架构的PC),但生成针对其他架构(例如ARM或Power)的目标文件。

有好几个厂商提供商业嵌入式Linux发行版。嵌入式Linux的领导厂商已经在此行业经营多年了。获得关于这些厂商的信息还是相对容易些的。在互联网上快速搜索 “嵌入式Linux发行版”(embedded Linux distributions),就会获得不少这方面的信息。关于此类信息,一个比较有用的网址是http://elinux.org/Embedded_Linux_Distributions。

你可以自己组装嵌入式项目所需的所有元件。但要知道这样做的风险,以及是否值得为此付出。如果你纯粹出于兴趣而专注于嵌入式Linux,比如参与一个兴趣小组或大学里的项目,自己做是个不错的选择。然而,如果是做项目你就需要斟酌了,将项目所需的所有工具和实用程序组装在一起,并保证它们之间能够兼容是需要花费大量时间的。

新手需要一个工具链。gcc和binutils都可以从www.fsf.org及遍布世界的镜像网站获得。在一个项目中,这两个工具都是编译内核和用户空间应用程序所必需的。这些工具主要是以源码的方式发布的,所以你必须自行编译,以适合特定的交叉开发环境。在获得这些实用程序的最新 “稳定版”源码后,你通常还需要对这些源码打补丁,特别是当这些程序用于非x86/IA32架构的系统时。这些补丁程序一般都和基本的软件包存放在同一位置。你所面对的挑战就是找到那些合适的补丁程序,以满足特定问题或架构的需要。

准备好工具链以后,你还需要下载和编译很多应用程序软件包,以及它们所依赖的软件包。这是一个不小的挑战,因为很多软件包即使发展到今天也不便于交叉编译。很多软件包都是在x86环境下开发的,如果转换到其他环境下,仍然会出现编译等类似问题。

挑战并未到此结束,你也许想搭建一个全能的开发环境,包含很多工具,比如图形化调试器、内存分析工具、系统跟踪和性能分析工具等。从这里的讨论你可以看到,搭建你自己的嵌入式Linux发行版是一项相当艰巨的任务。

本章简要介绍了很多主题。现在,你可以以恰当的视角来审视后续内容了。在后面的章节中,这种认识会得到扩展,帮助你掌握必要的技能和知识,确保你在今后的嵌入式项目中获得成功。

《Linux内核设计与实现(原书第3版)》,Robert Love,机械工业出版社,2011年1月出版。

 

《深入理解Linux内核(第3版)》,Daniel P. Bovet,Marco Cesati,中国电力出版社,2007年9月出版。

 

《深入理解Linux虚拟内存管理》,Bruce Perens,北京航空航天大学出版社,2006年5月出版。


相关图书

Linux常用命令自学手册
Linux常用命令自学手册
庖丁解牛Linux操作系统分析
庖丁解牛Linux操作系统分析
Linux后端开发工程实践
Linux后端开发工程实践
轻松学Linux:从Manjaro到Arch Linux
轻松学Linux:从Manjaro到Arch Linux
Linux高性能网络详解:从DPDK、RDMA到XDP
Linux高性能网络详解:从DPDK、RDMA到XDP
跟老韩学Linux架构(基础篇)
跟老韩学Linux架构(基础篇)

相关文章

相关课程