在无MMU的Linux下编程

原文:http://nommu.org/

在无MMU的Linux下编程

nommu系统不使用 内存管理单元,它是将虚拟地址转换为物理地址的处理器的一部分。在nommu系统中,所有地址都是物理地址,这意味着:

没有需求错误:内存是由malloc()直接分配的,它会更频繁地返回NULL。

在具有mmu的系统中,malloc()实际上不分配内存。新的虚拟地址范围以“零页面”的冗余写时复制映射开始(因此所有读取都返回零但实际上一遍又一遍地从同一物理页读取),然后写入被截获并分配新内存根据页面错误处理程序的需要保存它们。

在mmu系统上,当进程耗尽虚拟地址空间时,malloc()只返回零(这在32位系统上才真正发生,即使这样,每个进程也有几千兆字节)。实际的内存耗尽由OOM杀手处理。因此,mmu Linux程序员经常摆脱检查NULL的习惯,因为它几乎不会发生。(有关 如何在mmu情况下工作的详细信息,请参阅内存常见问题解答。)

一个nommu系统没有虚拟地址,任何“映射”只是跟踪分配了什么物理内存和什么是免费的内核。相反,对nommu的分配必须找到物理内存的连续区域,将其设置为零,并返回指向它开头的指针。这意味着如果malloc()找不到足够大的连续内存块,它将返回NULL。

这意味着大量的推测映射,例如兆字节的堆栈“以防万一”,对于nommu来说是个坏主意。

所有程序文本都必须是可重定位的:所以我们不能使用标准的ELF二进制文件。

nommu系统中的每个进程都使用/看到相同的地址。大多数ELF程序指定映射每个段的地址,并且这些段中的代码/数据使用固定地址(以及其他详细信息,如程序的入口点)。这对nommu不起作用,因为您不知道系统上正在运行的其他程序,如果您想运行同一程序的两个实例,则每个实例都需要自己的bss和数据段。

因此,我们不同的连接方案,把.o文件到无论是一个binflt 二进制文件(文件格式,这是有点老的a.out二进制文件的重定位版本,使用内置 elf2flt工具),或FDPIC二进制文件(修改变种ELF所有东西都是可重定位的,基本上是静态PIE,扩展用于在进程之间共享文本和rodata段。无论哪种方式都需要知道如何生成这些输出格式的工具链,以及启用了相关二进制格式加载器(CONFIG_BINFMT_FLAT或CONFIG_BINFMT_ELF_FDPIC)的内核。

例如,对于sh2目标, 原住民Linux 项目使用uClibc构建binflt交叉编译器-sh2eb,而 musl-libc项目 使用musl构建fdpic工具链。

修复了编译时指定的堆栈大小

两种nommu输出格式都要求您在编译时指定显式堆栈大小,因为nommu系统无法自动增加堆栈(没有“保护页面”错误)。这些堆栈应该尽可能小,因为整个大小是在程序启动时分配的(需要连续的不可共享分配),如果系统无法获取该内存,则exec失败。默认大小(8k)基本上是内核堆栈。兆字节堆栈的标准Linux假设不适合用于nommu系统。

内存碎片是一个大问题

具有mmu的系统可以使用分散的物理页面填充虚拟映射,而不仅仅是对间隙进行光泽处理,而是将它们按顺序映射。

一个nommu系统需要一个连续的物理地址范围,这在具有大量现有分配的系统上可能很难实现。即使有大量的总可用内存,从中间分配的一小块将使可以满足的最大分配的大小减半。过了一会儿,最大可用内存块往往远远小于可用内存总量。

系统需要对程序代码,进程堆栈和环境空间进行连续分配,并满足malloc()。这些分配越小,它们就越有可能适应分散系统上的可用块空间。(相反,您制作的长期分配越多,内存碎片就越多。)

因此,你的libc可能不会在堆中放置大的映射(这本身就是一个大的连续映射),但可能会要求内核mmap()更大的分配,以便它们可以适应可用空间。(权衡是通过四舍五入到页面大小来扩大这种分配。)

没有fork():vfork()而不是

使用fork会很昂贵,即使它几乎不可能:你必须将所有程序的可写数据复制到新的分配,并且很可能通过执行exec()立即再次丢弃它们。

“几乎不可能”的部分是因为堆/堆栈的新副本中的任何指针都指向OLD堆/堆栈中的内存,因为每个进程都看到相同的地址。通过堆和堆栈重新定位所有这些指针结果非常困难(参见“C中的垃圾收集”)。

相反,我们使用vfork(),它在创建一个新的子进程时暂停父进程,这样子进程可以使用父进程(绝对是堆,它可能会也可能不会得到新的堆栈)。当子进程调用exec()或_exit()时,父进程将恢复。

vfork()ed的子节点必须小心,不要破坏父的堆(并且不应该从调用vfork()的函数返回,以防它使用相同的堆栈)。

如果孩子不能exec()一个新进程,它应该调用_exit()而不是exit(),因为第一个是系统调用,后者执行各种清理工作,如stdio刷新和运行atexit()调用和析构函数是父进程的属性,而不是子进程的业务。

(类似地,由于内存碎片问题,brk()系统调用的用处不大。在某些nommu系统上,brk()只返回-ENOSYS。这主要是人们关心使用nommu支持实现C库。)


官方文档:https://github.com/torvalds/linux/blob/master/Documentation/nommu-mmap.txt