姓名:陈方园 学号:19020100239 学院:电子工程学院
转自:
https://max.book118.com/html/2021/0122/6011000144003053.shtm
【嵌牛导读】嵌入式Linux开发的一大挑战性源自大多数嵌入式系统的物理资源非常有限。虽然你的台式电脑会拥有酷睿2双核处理器和500 GB大小的硬盘,但很难找到拥有如此巨大硬盘容量的嵌入式系统。多数情况下,硬盘通常被更小和更便宜的非易失性存储设备所取代。硬盘不仅笨重,包含旋转部件,对物理震动敏感,并且要求提供多种供电电压,因此并不适合用在许多嵌入式系统中。
【嵌牛鼻子】虚拟内存、交叉开发环境
【嵌牛提问】如何实现存储的内存空间?
【嵌牛正文】
2.3.5 内存空间
老式嵌入式操作系统通常将系统内存看做一大块线性地址空间,并进行管理。也就说,微处理器的地址空间的下限是0,上限是其物理地址范围的顶部。举例来说,如果一个微处理器有24条物理地址线,其内存范围的上限就是16MB。因此,其地址范围可以用十六进制表示为从0x00000000到0x00ffffff。硬件设计常常将DRAM放置在这个地址范围的底部,并将闪存放置在顶部。位于DRAM顶部和闪存底部之间的那些未使用的地址范围常常被分配给板上的各种外围设备芯片,用于对它们进行寻址。这种设计方法一般是由所选择的微处理器决定的。图2-5显示了一个简单嵌入式系统中的典型内存布局。
在基于老式操作系统的嵌入式设备中,操作系统和所有的任务[12]具有相同的权限,能够访问系统的所有资源。某个进程中的一个故障可能会改写系统中任意一块内存的内容,这块内存可能属于这个进程本身、操作系统、其他任务,甚至是地址空间中的一个硬件寄存器。虽然这种内存管理方式有个最大的优点:简单,但它会导致一些很难诊断的故障。
高性能的微处理器中都包含一个复杂的硬件引擎,称为内存管理单元(MemoryManagement Unit,MMU)。MMU的作用是使操作系统能够在很大程度上管理和控制地址空间,包括操作系统自身的地址空间和分配给进程的地址空间。这种控制主要体现为两种形式:访问权限控制(access right)和内存地址转换(memory translation)。访问权限控制允许操作系统将特定的内存访问权分配给特定的进程。内存地址转换允许操作系统将其地址空间虚拟化,从而带来很多好处。Linux内核利用这些硬件MMU实现了一个虚拟内存操作系统。虚拟内存所带来的最大的一个好处是,它可以让系统的内存看起来比实际的物理内存多,这样能够更加有效地利用物理内存。其他的好处是,内核在为任务或进程分配系统内存时,可以指定这块内存的访问权限,从而防止某个进程错误地访问属于另一个进程或内核自身的内存或其他资源。
下一节将更详细地讨论MMU的工作原理。复杂的虚拟内存系统的内容超出了本书的范围[13]。实际上,我们会从嵌入式系统开发者的角度来考查虚拟内存系统。
2.3.6 执行上下文
系统引导时,Linux最先要完成一项琐碎工作,即配置处理器中的硬件MMU以及相应的数据结构,并使之能够进行地址转换。这一步完成后,内核运行于自己的虚拟内存空间中,这个空间称为内核空间。在当前的Linux内核版本中,这个虚拟内存空间的起始地址是由内核开发者选择的,其默认值为0xC0000000[14]。对于大多数硬件架构,这是个可以配置的参数[15]。在内核符号表中,可以看到内核符号的链接地址都是以0xC0xxxxxx开头的。所以,当内核在内核空间中执行代码时,处理器的指令指针(程序计数器)所包含的值都在这个范围之内。
Linux中有两个明显分隔开的运行上下文,由线程[16]的执行环境所决定。那些完全在内核中执行的线程被认为运行在内核上下文中,而应用程序运行在用户空间上下文中。用户空间进程只能访问它自己拥有的内存,如果它要访问文件或设备I/O等特权资源,则必须使用内核系统调用。下面举个例子,让你更好地理解这一点。
假设一个应用程序打开一个文件并读取其中的内容,如图2-6所示。对读函数的调用是从用户空间开始的,由应用程序调用C库中的read()函数。接着,C库向内核发起一个读请求。这个读请求造成一次上下文的切换,从用户程序切换到内核,以服务这个请求并读取文件中的数据。在内核中,这个读请求最终转变成对硬盘驱动器的访问,从包含文件内容的扇区中读取相应数据。
通常,这个对硬盘驱动器的读请求是以异步的形式发往硬件自身的。也就是说,处理器将这个请求发给硬件,并不会等待其完成请求。硬件收到请求后读取数据,当数据准备好的时候,通过中断的方式来告知处理器读请求已经完成了。等待数据的应用程序会阻塞在一个等待队列中,直到有数据可用。当硬盘准备好数据时,它将向处理器发送一个硬件中断(这里只是描述了一个简化的过程)。当内核接收到这个硬件中断时,它会挂起正在执行中的任何进何进程,并从硬盘驱动器中读取应用程序所等待的数据。
下面对我们的讨论做一个概括,我们学习了两个通用的执行上下文——用户空间和内核空间。当应用程序执行系统调用,造成上下文的切换而进入内核时,内核会代表这个进程执行内核代码。你会经常听到,这种情况称为内核运行于进程上下文中。相反,处理IDE驱动器的中断处理程序(ISR)也是内核代码,但在运行时并不代表任何特定的进程。这种情况通常被称为内核运行于中断上下文中。
内核运行于中断上下文中时会受到一些限制,包括中断处理程序不能够阻塞(睡眠)或调用任何可能造成阻塞的内核函数。如果想要更多地了解这些概念,请阅读本章末尾的参考文献。
2.3.7 进程虚拟内存
一个进程产生时——例如,当用户在Linux的命令提示符后面输入ls的时候——内核就会为这个进程分配内存及相应的虚拟内存地址范围。这些地址与内核中的地址或其他正在运行的进程的地址没有固定的关系。此外,这些进程所看到的虚拟地址跟目标板上的物理内存的地址也没有直接的关系。实际上,由于系统中存在分页(paging)和交换(swapping)机制,一个进程在其生命周期中常常会占用内存的多个不同的物理地址。
代码清单2-4是程序员所熟知的“Hello World”程序,这里做了点修改来说明刚刚讨论的一些概念。这个例子的目的是解释说明内核分配给进程的地址空间。这段代码编译后,在一个拥有256MB DRAM内存的嵌入式系统上运行。
代码清单2-4 嵌入式风格的Hello World
代码清单2-5显示了运行编译后的程序hello时,控制台输出的信息。注意,hello进程认为它的运行地址位于高地址内存的某个地方,刚好超过256MB的边界(0x10000418)。还需注意,栈的地址大概处于32位地址空间一半的地方,远远超过了内存的大小256MB(0x7ff8ebb0)。怎么会这样呢?在这种系统中,DRAM通常是一块连续的内存。乍一看,我们几乎有将近2 GB的DRAM可以使用。这些虚拟地址是由内核分配的,并且有嵌入式目标板上的256MB的物理内存在背后支持。
代码清单2-5 Hello的输出
虚拟内存系统的一个特点是当可用的物理内存的数量低于某个指定的阈值时,内核可以将内存页面交换到大容量存储媒介中,通常是硬盘驱动器。内核检查正在使用中的内存区域,并判断哪些区域最近使用得最少,然后将这些内存区域交换到磁盘中,并释放这些内存区域给当前进程使用。嵌入式系统的开发者常常会因为性能原因或资源限制而禁用嵌入式系统中的交换功能。多数情况下,使用慢速且写寿命有限的闪存设备作为交换设备是很不明智的。如果没有交换设备可用,就必须仔细地设计应用程序,使其能够运行在有限的物理内存中。
2.3.8 交叉开发环境
开发嵌入式系统应用和设备驱动之前,需要一套工具(编译器、实用工具等)来生成适合目标系统的二进制可执行文件。考虑一个在桌面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],其结果是可以预见的,那就是系统崩溃!
摆脱这个困境的方法是指引交叉编译器在非标准路径中进行查找,以使用针对目标架构的头文件和程序库。我们将在第12章中详细讨论这个主题。这个例子旨在说明两种开发环境的区别,即本地开发环境和嵌入式系统所需的交叉编译开发环境。这只是交叉开发环境复杂性的一个方面。交叉调试中也会出现相同的问题和解决方案,从第14章开始,你会了解到这些内容。正确地搭建交叉开发环境对于成功至关重要,你将在第12章中看到,这不仅仅涉及编译器,还包括其他很多内容。