Embedded Linux Primer----嵌入式Linux基础教程--2.3.5~8节--存

2019-07-13 07:37发布

内存空间

事实上,所有的传统嵌入式操作系统查看与管理系统内存的时候都作为单一大型的平行的地址空间。也就是说,一个微处理器的地址空间范围从0到物理地址空间的最大值。例如,如果一个微处理器有24条地址线,那么它的内存最大是16MB。因此,它的十六进制地址空间范围从0x00000000到0x00ffffff。硬件设计中,DRAM从底部开始,闪存是自顶向下的。未使用的地址空间在DRAM的顶部和FLASH的底部之间,这些空间将分配给板子上的各种各样的外围芯片。这种设计方法经常由微处理器的选择指定。图2-5是一个简单的嵌入式系统的典型内存布局的例子。

图2-5典型嵌入式系统内存映射 基于传统操作系统的嵌入式系统,系统和任务(task)12具有相同的存取系统内部资源的权利。程序当中的一个错误可能摧毁系统中任何地方的存储内容。不论它属于它本身还是系统还是其它任务,或者甚至于属于一个地址空间中的硬件寄存器。虽然这种方法有最宝贵的特征,但是它可能会导致难以诊断的错误。 高性能微处理器包括复杂的硬件引擎—内存管理单元(MMUs),它的意图是让操作系统高度的管理和控制地址空间并且分配给处理器。这种控制有两种原形:访问权限(accessrights)和存储器转换(memory translation)。访问权限允许操作系统分配特殊的处理器访问特权给特殊的任务。存储器转换允许操作系统虚拟地址空间,这样能够带来很多好处。
Linux内核利用这些内存管理单元创建虚拟存储操作系统。其中最大的好处就是让物理内存得到更有效的利用。其它的好处内核能够强制访问权限进入系统内存分配的每一个的任务或者进程的每一个地方,这样做的目的在于防止非法内存的进程权限或者使用其他进程的资源。 下一个章节将详细介绍这一工作。关于虚拟内存的介绍已经超过了本书的范围13。我们这里主要是介绍一个在嵌入式系统开发者看来有用的虚拟内存系统的分支。

可执行上下文

Linux执行的第一个事就是配置处理器上的硬件MMU和支持它的数据结构以及地址转换。当这一步骤完成,内核运行于它自己的虚拟内存空间。最新版本的Linux内核中内核开发者选择的虚拟内核地址默认是0xC0000000。在大部分的体系结构当中,这是一个可配置的参数14。如果我们看了内核符号表,我们将会找到内核符号链接到起始于oxC0xxxxxx的地址。结果,任何时候,内核执行代码都在内核空间,处理器的指令指示器(程序计数器)包含这一范围内的值。 在Linux中,基于一个给定的可执行线程,我们参考两个明显的分开的操作上下文。内核里面的完整的可执行线程据说运行在内核上下中(kernelcontext)。应用程序运行在用户空间。用户空间程序只能访问自己的内存,并且要求使用内核系统访问特殊的文件或I/O资源。下面的例子可能让你对它更清楚。
考虑一个打开文件和发出读请求的应用程序,图2-6显示如下。读功能开始在用户空间调用C库函数中的read()函数。C标准库随后发出一个读请求给内核。这个读请求导致上下文从用户程序转换到内核中,为文件数据提供服务请求。在内核之外,读请求导致一个硬件驱动接受包含文件数据的扇区的请求。

图2-6 简单的文件读请求
通常,硬盘读请求发出异步信号给硬件自己。也就是说,请求被传递给硬件,当数据准备好,硬件中断处理器。当数据可用时,应用程序等待一个等待队列中的数据被锁。之后,当硬盘的数据准备好,它发出硬件中断。(为了方便叙述,故意简单化描述。)当内核接收到硬件中断,内核阻塞正在运行的程序并且开始读硬盘中的等待数据。 通过上面的讨论,我们已经知道了两种通用的可执行上下文—用户空间和内核空间。当一个应用程序执行系统调用,将导致上下文切换并进入内核区,可执行内核代码代表一个程序。你会经常听到内核中的进程上下文(processcontext)。与此相反,处理IDE盘的中断服务程序(ISR) (或其他ISR,就此而言)是不代表任何一个特殊进程执行的内核代码。这就是典型的中断上下文。 在可操作的上下文中有几个限制存在,包括ISR不能锁定(block)或者调用任何一个可能导致锁定的内核函数。想学习更多,请参考本章最后的指引。

进程虚拟内存

当一个进程产生—例如,当用户在Linux命令提示行中输入ls,内核为每个进程分配内存空间并且分配一个虚拟内存地址空间。地址值支撑内核中的未定位的关系,也不是针对其它的正在运行的进程。此外,板上的物理内存地址和进程中可见的虚拟内存之间是没有直接的相关性的。事实上,一个进程在生命期内由于分页和交换占用多个不同主存储器中的物理内存地址是常见的。 表2-4是珍贵的“Hello World,”仅仅是为了讨论概念被修改。这个例子的目的是阐述内核分配给进程的地址空间。这个代码在包含256MB的DRAM内存的嵌入式系统中编译并运行。 表2-4 Hello World,嵌入式风格

表2-5展示了上面的程序执行的终端输出。注意到这个hello程序运行在256MB(0x10000418)的RAM中。同样注意到栈地址大概占到一个32位地址空间的一半,超过了RAM(0x7ff8ebb0)的256MB。怎么会这样呢?DRAM通常在系统中是连续的。对于漫不经心的人来说,这表示我们有2GB的DRAM可利用。这些虚拟地址通过内核被分配并通过我们的嵌入式板子上的可利用的256MB范围的物理RAM索回。 表2-5 Hello Output

虚拟存储系统的特征之一是当可利用的物理RAM在指定的临界值之下,内核能够置换内存页到一个大容量设备中,通常是一个硬盘。内核检查有效的内存区域来决定内存中那块区域是最小最近使用过的,并且置换出这些内存到磁盘中,为当前的进程释放它们。由于性能和资源约束的原因,嵌入式开发者经常禁用嵌入式系统中的这些交换。例如,使用一个相对较慢的有限的写生命周期的闪存设备作为一个交换设备是很荒谬的。如果没有一个交换设备,那么你必须小心地设计你的应用程序存在于有限的可用物理内存当中。

交叉编译环境

在我们为一个嵌入式系统开发应用程序和设备驱动之前,我们需要一系列的工具(compiler,utilities, and so on)用来产生目标系统下的可执行的二进制文件。想一想在你的台式电脑当中写一个简单的应用程序,譬如经典的“HelloWorld”。在你已经写好源代码之后,你调用编译器(通常是GNU gcc)来产生一个可执行的二进制镜像。这个镜像文件在编译之后能够在你的机器上运行。这种方法指的是本地(native)编译。换句话说,使用你电脑系统中的编译器产生的代码将能够在你的电脑中运行。 需要注意的是,这里的本地并不意味着一个体系结构。确实,如果你有一个运行在你的目标板的工具链,你将能够为你的目标板开发本地的应用程序。事实上,一个好的测试一个新的嵌入式内核和定制的板子的方法就是在它上面多次编译Linux内核。 在交叉开发环境下开发软件要求开发主机上的编译器产生可执行的二进制文件,这个二进制文件在你的主机上是没法运行的。这些工具存在的主要原因就是由于资源的限制(尤其是内存和CPU),在嵌入式系统上开发和编译软件是不切实际的。 对于粗心的嵌入式开发新手来说,这种方法存在大量的隐藏的陷阱。当一个程序被编译,编译器知道如何找到头文件(includefiles)以及去哪里找可能需要的库文件。为了阐述这些概念,让我们再来看下“HelloWorld”程序。这个例子是用表2-4的程序和下面的命令编译: gcc –Wall –o hello hello.c       在表2-4中,我们可以看到stdio.h头文件,这个文件跟hello.c不属于同一目录。那么编译器是如何找到它们的呢?同样,printf()函数并没有在hello.c中定义。因此,当hello.c被编译时,它将包含这些不认识的引用。链接器是如何在链接时处理这些引用的呢?       编译器已经默认的嵌入了这些头文件。当遇到这些头文件时,编译器去默认的定位列表(list oflocations)中找到这些文件。一个链接器中类似的程序用来处理外部引用printf()。默认情况下,链接器都知道为这些未辨别的应用查找C库(libc-*)并且知道在你的系统当中的神什么位置找到这些引用。再次强调下,这些默认的行为都绑定于在工具链中。       现在假定在一个PowerArchitecture embedded system中构建了一个应用程序。显而易见,你需要一个交叉编译器(cross-compiler)产生能够兼容Power Architecture处理器的可执行二进制文件。如果你使用交叉编译器并使用类似的编译命令来编译hello.c,很有可能产生的二进制文件会链接到你的开发系统的x86版本的C库而终止。 这种困境的解决方案就是通知交叉编译器通过不标准的定位来获得头文件和目标特定的库。我们将在第十二章详细介绍这一主题。这个例子的目的是为了阐述本地开发环境和嵌入式系统交叉编译环境的区别。这只是交叉编译的复杂性之一。关于交叉调试的问题和解决方案,将在第十四章介绍。