uclinux即“微控制器领域中的Linux系统”,主要是针对目标处理器没有存储管理单元MMU的嵌入式系统而设计的。已经被成功地移植到了很多平台上。
uCLinux 表示 micro-control linux.即“微控制器领域中的 Linux 系统”,是 Lineo 公司的主打产品,同时也是开放源码的嵌入式 Linux 的典范之作。uCLinux 主要是针对目标处理器没有存储管理单元 MMU(Memory Management Unit)的嵌入式系统而设计的。它已经被成功地移植到了很多平台上。由于没有 MMU,其多任务的实现需要一定技巧。
简介
Linux 是一种很受欢迎的操作系统,它与 UNIX 系统兼容,开放源代码。它原本被设计为桌面系统,现在广泛应用于服务器领域。而更大的影响在于它正逐渐的应用于嵌入式设备。uClinux 正是在这种氛围下产生的。在 uClinux 这个英文单词中 u 表示 Micro,小的意思,C 表示 Control,控制的意思,所以 uClinux 就是 Micro-Control-Linux,字面上的理解就是”针对微控制领域而设计的 Linux 系统”。
uClinux 是嵌入式 Linux 领域非常重要的分支,已成功应用于路由器、机顶盒、PDA 等领域,与标准 Linux 在内存管理方面有着本质的区别。
uCLinux 是一种优秀的嵌入式 Linux 版本,是 micro-Controller-Linux 的缩写。它秉承了标准 Linux 的优良特性, 经过各方面的小型化改造,形成了一个高度优化的、代码紧凑的嵌入式 Linux。虽然它的体积很小,却仍然保留了 Linux 的大多数的优点:稳定、良好的移植性、优秀的网络功能、对各种文件系统完备的支持和标准丰富的 API。它专为嵌入式系统做了许多小型化的工作,目前已支持多款 CPU。 其编译后目标文件可控制在几百 KB 数量级,并已经被成功地移植到很多平台上。
uClinux 从 Linux 2.0/2.4 内核派生而来,沿袭了 Linux 的绝大部分特性。它是专门针对没有 MMU(内存管理单元)的 CPU,并且为嵌入式系统做了许多小型化的工作。它通常用于具有很少内存或 Flash 的嵌入式操作系统。在 GNU 通用许可证的保证下,运行 uClinux 操作系统的用户可以使用几乎所有的 Linux API 函数。由于经过了裁剪和优化,它形成了一个高度优化,代码紧凑的嵌入式 Linux。它具有体积小、稳定、良好的移植性、优秀的网络功能、完备的对各种文件系统的支持,以及丰富的 API 函数等优点。uClinux 与 Linux 在兼容性方面表现出色,uClinux 除了不能实现 fork()外,其余 uClinux 的 API 函数与标准 Linux 完全相同。
针对没有 MMU 的 CPU
全球每年生产的 CPU 的数量大概在二十亿颗左右,其中大部分是应用于专用性很强的各类嵌入式系统。大部分嵌入式系统为了减少系统复杂程度、降低硬件及开发成本和运行功耗,在硬件设计中取消了内存管理单元(MMU)模块。最初,运行于这类没有 MMU 的 CPU 之上的都是一些很简单的单任务操作系统,或者更简单的控制程序,甚至根本就没有操作系统而直接运行应用程序。在这种情况下,系统无法运行复杂的应用程序,或者效率很低,并且所有的应用程序需要重新开发,还要求开发人员十分了解硬件特性。这些都阻碍了不含 MMU 的嵌入式产品开发的速度和应用水平。
uClinux 专门针对没有 MMU 的 CPU,并且为嵌入式系统做了许多小型化的工作。uClinux 是一个完全符合 GNU/GPL 公约的项目,完全开放代码。
最初的 uClinux 仅仅支持 Palm 硬件系统,基于 Linux 2.0 内核。随着系统的日益改进,支持的内核版本从 2.0、2.2、2.4 一直到现在最新的 2.6。系统的开发人员从两人增加到了目前的 12 人,支持的硬件系统也从一种增加到了目前的十余种(支持的硬件平台如 Motorola 公司的 M68328、M68EN322、MC68360、DragonBall 系列如 68EZ328、68VZ328,ColdFire 系列的如 5272、5307,ARM 7TDMI、MC68EN302、ETRAX、Intel i960、PRISMA、Atari 68k 等等。)
根据 Linuxdevices 网站 2004 年 3 月的调查,uClinux 在全球嵌入式 Linux 市场所占的份额已位居第二,仅仅落后于定制 Linux(即自己下载源码进行修改定制)。同时 Linux 在全球嵌入式操作系统的市场份额依然处于统治地位(占 40%以上),领先第二名微软公司的嵌入式操作系统三倍以上(市场份额约 13%)。
特点
标准 Linux 可能采用的小型化方法
1. 重新编译内核
Linux 内核采用模块化的设计,即很多功能块可以独立的加上或卸下,开发人员在设计内核时把这些内核模块作为可选的选项,可以在编译系统内核时指定。因此一种较通用的做法是对 Linux 内核重新编译,在编译时仔细的选择嵌入式设备所需要的功能支持模块,同时删除不需要的功能。通过对内核的重新配置,可以使系统运行所需要的内核显著减小,从而缩减资源使用量。
2. 制作 root 文件系统映象
Linux 系统在启动时必须加载根(root)文件系统,因此剪裁系统同时包括 root file system 的剪裁。在 x86 系统下,Linux 可以在 Dos 下,使用 Loadlin 文件加载启动,
uClinux 采用的小型化方法
1.uClinux 的内核加载方式
uClinux 的内核有两种可选的运行方式:可以在 flash 上直接运行,也可以加载到内存中运行。这种做法可以减少内存需要。
Flash 运行方式:把内核的可执行映象烧写到 flash 上,系统启动时从 flash 的某个地址开始逐句执行。这种方法实际上是很多嵌入式系统采用的方法。
内核加载方式:把内核的压缩文件存放在 flash 上,系统启动时读取压缩文件在内存里解压,然后开始执行,这种方式相对复杂一些,但是运行速度可能更快(ram 的存取速率要比 flash 高)。同时这也是标准 Linux 系统采用的启动方式。
2.uClinux 的根(root)文件系统
uClinux 系统采用 romfs 文件系统,这种文件系统相对于一般的 ext2 文件系统要求更少的空间。空间的节约来自于两个方面,首先内核支持 romfs 文件系统比支持 ext2 文件系统需要更少的代码,其次 romfs 文件系统相对简单,在建立文件系统超级块(superblock)需要更少的存储空间。Romfs 文件系统不支持动态擦写保存,对于系统需要动态保存的数据采用虚拟 ram 盘的方法进行处理(ram 盘将采用 ext2 文件系统)。
3.uClinux 的应用程序库
uClinux 小型化的另一个做法是重写了应用程序库,相对于越来越大且越来越全的 glibc 库,uClibc 对 libc 做了精简。
uClinux 对用户程序采用静态连接的形式,这种做法会使应用程序变大,但是基于内存管理的问题,不得不这样做(这将在下文对 uClinux 内存管理展开分析时进行说明),同时这种做法也更接近于通常嵌入式系统的做法。
系统特点
嵌入式操作系统比较
由表 1 可以看出,对于嵌入式应用,高端平台可直接采用 Linux 系统,其兼容性和可移植度都较高,但对硬件处理速度和存储空间要求较高。
低端平台的最佳选择是 uClinux,其性能稳定、移植性好、功能强大。
低端平台如果对实时性要求较高、应用相对简单,则可采用 uc/os 或其他操作系统。
基本架构
uClinux 的系统与标准 Linux 的架构完全一致。
文件系统
uClinux 系统多采用 Romfs 文件系统,Romfs 是一种相对简单、占用空间较少的文件系统。空间的节约来自于两个方面:首先内核支持 Romfs 文件系统比支持 ext2 文件系统需要更少的代码;其次 romfs 文件系统相对简单,在建立文件系统超级块(Superblock)需要更少的存储空间。Romfs 是只读的文件系统,禁止写操作,因此系统同时需要虚拟盘(RAMDISK)支持临时文件和数据文件的存储。
随着技术的发展,近年来日志文件系统在 uClinux 系统上得到了较多的应用,其中以支持 NOR FLASH 的 JFFS、JFFS2 文件系统和支持 NAND FLASH 的 YAFFS 最为流行。这些文件系统都支持掉电文件保护,同时支持标准的 MTD 驱动。
开发环境
GNU 开发套件
Gnu 开发套件作为通用的 Linux 开放套件,包括一系列的开发调试工具。主要组件:
Gcc: 编译器,可以做成交叉编译的形式,即在宿主机上开发编译目标上可运行的二进制文件。
Binutils:一些辅助工具,包括 objdump(可以反编译二进制文件),as(汇编编译器),ld(连接器)等等。
Gdb:调试器,可使用多种交叉调试方式,gdb-bdm(背景调试工具),gdbserver(使用以太网络调试)。
uClinux 的打印终端
通常情况下,uClinux 的默认终端是串口,内核在启动时所有的信息都打印到串口终端(使用 printk 函数打印),同时也可以通过串口终端与系统交互。
uClinux 在启动时启动了 telnetd(远程登录服务),操作者可以远程登录上系统,从而控制系统的运行。至于是否允许远程登录可以通过烧写 romfs 文件系统时有用户决定是否启动远程登录服务。
交叉编译调试工具
支持一种新的处理器,必须具备一些编译,汇编工具,使用这些工具可以形成可运行于这种处理器的二进制文件。对于内核使用的编译工具同应用程序使用的有所不同。在解释不同点之前,需要对 gcc 连接做一些说明:
.ld(link description)文件:ld 文件是指出连接时内存映象格式的文件。
crt0.S:应用程序编译连接时需要的启动文件,主要是初始化应用程序栈。
pic:position independence code ,与位置无关的二进制格式文件,在程序段中必须包括 reloc 段,从而使的代码加载时可以进行重新定位。
内核编译连接时,使用 ucsimm.ld 文件,形成可执行文件映象,所形成的代码段既可以使用间接寻址方式(即使用 reloc 段进行寻址),也可以使用绝对寻址方式。这样可以给编译器更多的优化空间。因为内核可能使用绝对寻址,所以内核加载到的内存地址空间必须与 ld 文件中给定的内存空间完全相同。
应用程序的连接与内核连接方式不同。应用程序由内核加载(可执行文件加载器将在后面讨论),由于应用程序的 ld 文件给出的内存空间与应用程序实际被加载的内存位置可能不同,这样在应用程序加载的过程中需要一个重新地位的过程,即对 reloc 段进行修正,使得程序进行间接寻址时不至于出错。(这个问题在 i386 等高级处理器上方法有所不同,本文泪雪网将在后面进一步分析)。
由上述讨论,至少需要两套编译连接工具。在讨论过 uClinux 的内存管理后本文泪雪网将给出整个系统的工作流程以及系统在 flash 和 ram 中的空间分布。
可执行文件格式
先对一些名词作一些说明:
coff(common object file format):一种通用的对象文件格式
elf(excutive linked file):一种为 Linux 系统所采用的通用文件格式,支持动态连接
flat:elf 格式有很大的文件头,flat 文件对文件头和一些段信息做了简化
uClinux 系统使用 flat 可执行文件格式,gcc 的编译器不能直接形成这种文件格式,但是可以形成 coff 或 elf 格式的可执行文件,这两种文件需要 coff2flt 或 elf2flt 工具进行格式转化,形成 flat 文件。
当用户执行一个应用时,内核的执行文件加载器将对 flat 文件进行进一步处理,主要是对 reloc 段进行修正(可执行文件加载器的详见 fs/binfmt_flat.c)。以下对 reloc 段进一步讨论。
需要 reloc 段的根本原因是,程序在连接时连接器所假定的程序运行空间与实际程序加载到的内存空间不同。假如有这样一条指令:
jsr app_start;
这一条指令采用直接寻址,跳转到 app_start 地址处执行,连接程序将在编译完成是计算出 app_start 的实际地址(设若实际地址为 0x10000),这个实际地址是根据 ld 文件计算出来(因为连接器假定该程序将被加载到由 ld 文件指明的内存空间)。但实际上由于内存分配的关系,操作系统在加载时无法保证程序将按 ld 文件加载。这时如果程序仍然跳转到绝对地址 0x10000 处执行,通常情况这是不正确的。一个解决办法是增加一个存储空间,用于存储 app_start 的实际地址,设若使用变量 addr 表示这个存储空间。则以上这句程序将改为:
movl addr, a0;
jsr (a0);
增加的变量 addr 将在数据段中占用一个 4 字节的空间,连接器将 app_start 的绝对地址存储到该变量。在可执行文件加载时,可执行文件加载器根据程序将要加载的内存空间计算出 app_start 在内存中的实际位置,写入 addr 变量。系统在实际处理是不需要知道这个变量的确切存储位置(也不可能知道),系统只要对整个 reloc 段进行处理就可以了(reloc 段有标识,系统可以读出来)。处理很简单只需要对 reloc 段中存储的值统一加上一个偏置(如果加载的空间比预想的要靠前,实际上是减去一个偏移量)。偏置由实际的物理地址起始值同 ld 文件指定的地址起始值相减计算出。
这种 reloc 的方式部分是由 uClinux 的内存分配问题引起的,这一点将在 uClinux 内存管理分析时说明。
针对实时性的解决方案
uClinux 本身并没有关注实时问题,它并不是为了 Linux 的实时性而提出的。另外有一种 Linux–Rt-linux 关注实时问题。Rt-linux 执行管理器把普通 Linux 的内核当成一个任务运行,同时还管理了实时进程。而非实时进程则交给普通 Linux 内核处理。这种方法已经应用于很多的操作系统用于增强操作系统的实时性,包括一些商用版 UNIX 系统,Windows NT 等等。这种方法优点之一是实现简单,且实时性能容易检验。优点之二是由于非实时进程运行于标准 Linux 系统,同其它 Linux 商用版本之间保持了很大的兼容性。优点之三是可以支持硬实时时钟的应用。uClinux 可以使用 Rt-linux 的 patch,从而增强 uClinux 的实时性,使得 uClinux 可以应用于工业控制、进程控制等一些实时要求较高的应用。
内存管理
应该说 uClinux 同标准 Linux 的最大区别就在于内存管理,同时也由于 uClinux 的内存管理引发了一些标准 Linux 所不会出现的问题。本文泪雪网将把 uClinux 内存管理同标准 Linux 的内存管理部分进行比较分析。
标准 Linux 使用的虚拟存储器技术
标准 Linux 使用虚拟存储器技术,这种技术用于提供比计算机系统中实际使用的物理内存大得多的内存空间。使用者将感觉到好像程序可以使用非常大的内存空间,从而使得编程人员在写程序时不用考虑计算机中的物理内存的实际容量。为了支持虚拟存储管理器的管理,Linux 系统采用分页(paging)的方式来载入进程。所谓分页既是把实际的存储器分割为相同大小的段,例如每个段 1024 个字节,这样 1024 个字节大小的段便称为一个页面(page)。
虚拟存储器由存储器管理机制及一个大容量的快速硬盘存储器支持。它的实现基于局部性原理,当一个程序在运行之前,没有必要全部装入内存,而是仅将那些当前要运行的那些部分页面或段装入内存运行(copy-on-write),其余暂时留在硬盘上程序运行时如果它所要访问的页(段)已存在,则程序继续运行,如果发现不存在的页(段),操作系统将产生一个页错误(page fault),这个错误导致操作系统把需要运行的部分加载到内存中。必要时操作系统还可以把不需要的内存页(段)交换到磁盘上。利用这样的方式管理存储器,便可把一个进程所需要用到的存储器以化整为零的方式,视需求分批载入,而核心程序则凭借属于每个页面的页码来完成寻址各个存储器区段的工作。
标准 Linux 是针对有内存管理单元的处理器设计的。在这种处理器上,虚拟地址被送到内存管理单元(MMU),把虚拟地址映射为物理地址。
通过赋予每个任务不同的虚拟–物理地址转换映射,支持不同任务之间的保护。地址转换函数在每一个任务中定义,在一个任务中的虚拟地址空间映射到物理内存的一个部分,而另一个任务的虚拟地址空间映射到物理存储器中的另外区域。计算机的存储管理单元(MMU)一般有一组寄存器来标识当前运行的进程的转换表。在当前进程将 CPU 放弃给另一个进程时(一次上下文切换),内核通过指向新进程地址转换表的指针加载这些寄存器。MMU 寄存器是有特权的,只能在内核态才能访问。这就保证了一个进程只能访问自己用户空间内的地址,而不会访问和修改其它进程的空间。当可执行文件被加载时,加载器根据缺省的 ld 文件,把程序加载到虚拟内存的一个空间,因为这个原因实际上很多程序的虚拟地址空间是相同的,但是由于转换函数不同,所以实际所处的内存区域也不同。而对于多进程管理当处理器进行进程切换并执行一个新任务时,一个重要部分就是为新任务切换任务转换表。我们可以看到 Linux 系统的内存管理至少实现了以下功能:
运行比内存还要大的程序。理想情况下应该可以运行任意大小的程序
◇可以运行只加载了部分的程序,缩短了程序启动的时间
◇可以使多个程序同时驻留在内存中提高 CPU 的利用率
◇可以运行重定位程序。即程序可以方于内存中的任何一处,而且可以在执行过程中移动。
◇写机器无关的代码。程序不必事先约定机器的配置情况。
◇减轻程序员分配和管理内存资源的负担。
◇可以进行共享–例如,如果两个进程运行同一个程序,它们应该可以共享程序代码的同一个副本。
◇提供内存保护,进程不能以非授权方式访问或修改页面,内核保护单个进程的数据和代码以防止其它进程修改它们。否则,用户程序可能会偶然(或恶意)的破坏内核或其它用户程序。
虚存系统并不是没有代价的。内存管理需要地址转换表和其他一些数据结构,留给程序的内存减少了。地址转换增加了每一条指令的执行时间,而对于有额外内存操作的指令会更严重。当进程访问不在内存的页面时,系统发生失效。系统处理该失效,并将页面加载到内存中,这需要极耗时间的磁盘 I/O 操作。总之内存管理活动占用了相当一部分 cpu 时间(在较忙的系统中大约占 10%)。
uClinux 针对 NOMMU 的特殊处理
对于 uClinux 来说,其设计针对没有 MMU 的处理器,即 uClinux 不能使用处理器的虚拟内存管理技术(应该说这种不带有 MMU 的处理器在嵌入式设备中相当普偏)。uClinux 仍然采用存储器的分页管理,系统在启动时把实际存储器进行分页。在加载应用程序时程序分页加载。但是由于没有 MMU 管理,所以实际上 uClinux 采用实存储器管理策略(real memeory management)。这一点影响了系统工作的很多方面。
uClinux 系统对于内存的访问是直接的,(它对地址的访问不需要经过 MMU,而是直接送到地址线上输出),所有程序中访问的地址都是实际的物理地址。操作系统对内存空间没有保护(这实际上是很多嵌入式系统的特点),各个进程实际上共享一个运行空间(没有独立的地址转换表)。
一个进程在执行前,系统必须为进程分配足够的连续地址空间,然后全部载入主存储器的连续空间中。与之相对应的是标准 Linux 系统在分配内存时没有必要保证实际物理存储空间是连续的,而只要保证虚存地址空间连续就可以了。另外一个方面程序加载地址与预期(ld 文件中指出的)通常都不相同,这样 relocation 过程就是必须的。此外磁盘交换空间也是无法使用的,系统执行时如果缺少内存将无法通过磁盘交换来得到改善。
uClinux 对内存的管理减少同时就给开发人员提出了更高的要求。如果从易用性这一点来说,uClinux 的内存管理是一种倒退,退回了到了 UNIX 早期或是 Dos 系统时代。开发人员不得不参与系统的内存管理。从编译内核开始,开发人员必须告诉系统这块开发板到底拥有多少的内存(假如你欺骗了系统,那将在后面运行程序时受到惩罚),从而系统将在启动的初始化阶段对内存进行分页,并且标记已使用的和未使用的内存。系统将在运行应用时使用这些分页内存。
由于应用程序加载时必须分配连续的地址空间,而针对不同硬件平台的可一次成块(连续地址)分配内存大小限制是不同(目前针对 ez328 处理器的 uClinux 是 128k,而针对 coldfire 处理器的系统内存则无此限制),所以开发人员在开发应用程序时必须考虑内存的分配情况并关注应用程序需要运行空间的大小。另外由于采用实存储器管理策略,用户程序同内核以及其它用户程序在一个地址空间,程序开发时要保证不侵犯其它程序的地址空间,以使得程序不至于破坏系统的正常工作,或导致其它程序的运行异常。
从内存的访问角度来看,开发人员的权利增大了(开发人员在编程时可以访问任意的地址空间),但与此同时系统的安全性也大为下降。此外,系统对多进程的管理将有很大的变化,这一点将在 uClinux 的多进程管理中说明。
虽然 uClinux 的内存管理与标准 Linux 系统相比功能相差很多,但应该说这是嵌入式设备的选择。在嵌入式设备中,由于成本等敏感因素的影响,普偏的采用不带有 MMU 的处理器,这决定了系统没有足够的硬件支持实现虚拟存储管理技术。从嵌入式设备实现的功能来看,嵌入式设备通常在某一特定的环境下运行,只要实现特定的功能,其功能相对简单,内存管理的要求完全可以由开发人员考虑。
标准 Linux 系统的进程、线程
进程:进程是一个运行程序并为其提供执行环境的实体,它包括一个地址空间和至少一个控制点,进程在这个地址空间上执行单一指令序列。进程地址空间包括可以访问或引用的内存单元的集合,进程控制点通过一个一般称为程序计数器(program counter,PC)的硬件寄存器控制和跟踪进程指令序列。
fork:由于进程为执行程序的环境,因此在执行程序前必须先建立这个能”跑”程序的环境。Linux 系统提供系统调用拷贝现行进程的内容,以产生新的进程,调用 fork 的进程称为父进程;而所产生的新进程则称为子进程。子进程会承袭父进程的一切特性,但是它有自己的数据段,也就是说,尽管子进程改变了所属的变量,却不会影响到父进程的变量值。
父进程和子进程共享一个程序段,但是各自拥有自己的堆栈、数据段、用户空间以及进程控制块。换言之,两个进程执行的程序代码是一样的,但是各有各的程序计数器与自己的私人数据。
当内核收到 fork 请求时,它会先查核三件事:首先检查存储器是不是足够;其次是进程表是否仍有空缺;最后则是看看用户是否建立了太多的子进程。如果上述说三个条件满足,那么操作系统会给子进程一个进程识别码,并且设定 cpu 时间,接着设定与父进程共享的段,同时将父进程的 inode 拷贝一份给子进程运用,最终子进程会返回数值 0 以表示它是子进程,至于父进程,它可能等待子进程的执行结束,或与子进程各做个的。
exec 系统调用:该系统调用提供一个进程去执行另一个进程的能力,exec 系统调用是??序的堆栈、数据段与程序段都会被修改,只有用户区维持不变。
vfork 系统调用:由于在使用 fork 时,内核会将父进程拷贝一份给子进程,但是这样的做法相当浪费时间,因为大多数的情形都是程序在调用 fork 后就立即调用 exec,这样刚拷贝来的进程区域又立即被新的数据覆盖掉。因此 Linux 系统提供一个系统调用 vfork,vfork 假定系统在调用完成 vfork 后会马上执行 exec,因此 vfork 不拷贝父进程的页面,只是初始化私有的数据结构与准备足够的分页表。这样实际在 vfork 调用完成后父子进程事实上共享同一块存储器(在子进程调用 exec 或是 exit 之前),因此子进程可以更改父进程的数据及堆栈信息,因此 vfork 系统调用完成后,父进程进入睡眠,直到子进程执行 exec。当子进程执行 exec 时,由于 exec 要使用被执行程序的数据,代码覆盖子进程的存储区域,这样将产生写保护错误(do_wp_page)(这个时候子进程写的实际上是父进程的存储区域),
这个错误导致内核为子进程重新分配存储空间。当子进程正确开始执行后,将唤醒父进程,使得父进程继续往后执行。
uClinux 的多进程处理
uClinux 没有 mmu 管理存储器,在实现多个进程时(fork 调用生成子进程)需要实现数据保护。
uClinux 的 fork 和 vfork:uClinux 的 fork 等于 vfork。实际上 uClinux 的多进程管理通过 vfork 来实现。这意味着 uClinux 系统 fork 调用完程后,要么子进程代替父进程执行(此时父进程已经 sleep)直到子进程调用 exit 退出,要么调用 exec 执行一个新的进程,这个时候将产生可执行文件的加载,即使这个进程只是父进程的拷贝,这个过程也不能避免。当子进程执行 exit 或 exec 后,子进程使用 wakeup 把父进程唤醒,父进程继续往下执行。
uClinux 的这种多进程实现机制同它的内存管理紧密相关。uClinux 针对 nommu 处理器开发,所以被迫使用一种 flat 方式的内存管理模式,启动新的应用程序时系统必须为应用程序分配存储空间,并立即把应用程序加载到内存。缺少了 MMU 的内存重映射机制,uClinux 必须在可执行文件加载阶段对可执行文件 reloc 处理,使得程序执行时能够直接使用物理内存。
uClinux 是专门针对没有 MMU 的处理器而设计的,即 uClinux 无法使用处理器的虚拟内存管理技术。实际上 uClinux 采用实存储器管理策略,通过地址总线对物理内存进行直接访问。所有程序中访问的地址都是实际的物理地址,所有的进程都在一个运行空间中运行(包括内核进程),这样的运行机制给程序员带来了不小的挑战,在操作系统不提供保护的情况下必需小心设计程序和数据空间,以免引起应用程序进程甚至是内核的崩溃。
uClinux 仍然采用存储器的分页管理,系统在启动时把实际存储器进行分页,在加载应用程序时程序分页加载。一个进程在执行前,系统必须为进程分配足够的连续地址空间,然后全部载入主存储器的连续空间中。系统不含 MMU 带来的另外一个问题是磁盘交换空间无法使用,对于资源有限的嵌入式系统而言,系统执行时如果缺少内存将无法通过磁盘交换来得到改善。
MMU 的省略虽然带来了系统及应用程序开发的限制,但对于成本和体积敏感的嵌入式设备而言,其应用环境和应用需求并不要求复杂和相对昂贵的硬件体系,对于功能简单的专用嵌入式设备,内存的分配和管理完全可以由开发人员考虑。
多进程管理
由于 uClinux 没有 MMU 管理存储器,在实现多个进程时需要实现数据保护。uClinux 的虽然支持 fork 函数,但其实质是和 vfork:实际上 uClinux 所有的多进程管理都通过 vfork 来实现。
vfork 不拷贝父进程的页面,只是初始化私有的数据结构与准备足够的分页表。调用完成后父子进程事实上共享同一块存储器,因此子进程可以更改父进程的数据及堆栈信息,所有父进程进入睡眠,直到子进程执行 exec。当子进程正确开始执行后,将唤醒父进程,使得父进程继续往后执行。这意味着 uClinux 系统 fork 调用完成后,要么子进程代替父进程执行(此时父进程已经休眠)直到子进程调用 exit 退出,要么调用 exec 执行一个新的进程。
vfork 是 uClinux 与标准 Linux 应用程序的开发中最重要的不同之处,只有对 vfork 与 fork 两个函数的差异和程序处理有详细的了解才能顺利地完成从 Linux 到 uClinux 的程序移植。
缺点
正如中国古语云“人无完人”,uClinux 也有一些不足之处:
文档的不足
与 Linux 及其他自由软件类似,uClinux 的文档十分不足:缺乏组织和一致的文档、热门技术和分类文档众多而杂乱无章、非热点部分文档缺失甚至没有文档。对于开发人员而言,往往要深入程序的源代码找寻有用的资料。
Bug 问题
uClinux 与硬件平台直接相关。对于有商业公司赞助的硬件平台,其相关代码和 Bug 更新较快,编译和执行都十分顺利;但对于非商业支持的硬件平台,其内核和应用程序代码都得不到及时更新和排错。这种现象在内核源代码树还不是十分普遍,但在 uClinux 自带的应用程序库中却经常发生编译错误,往往是增加了一个应用程序或改变了运行库便导致无法编译。这就需要开发者投入足够的时间和精力进行排错和修改,也会导致开发进度的延误。