无MMU内存映射支持

内核对无MMU条件下的内存映射(例如在uClinux环境中使用的)有有限的支持。 从用户空间的角度来看,内存映射与mmap()系统调用、shmat()调用和execve()系统调用一起使用。 从内核的角度来看,execve()映射实际上由binfmt驱动程序执行,它们回调mmap()例程来完成实际工作。

内存映射行为还涉及 fork()、vfork()、clone() 和 ptrace() 的工作方式。 在 uClinux 下,没有 fork(),并且必须为 clone() 提供 CLONE_VM 标志。

MMU和无MMU情况之间的行为类似,但不完全相同;并且在后一种情况下也更加受限

  1. 匿名映射,MAP_PRIVATE

    在MMU情况下:由任意页面支持的VM区域;跨fork的写时复制。

    在无MMU情况下:由任意连续的页面运行支持的VM区域。

  2. 匿名映射,MAP_SHARED

    这些行为与私有映射非常相似,只是它们在MMU情况下跨 fork() 或 clone() 共享,而没有 CLONE_VM。 由于无MMU情况不支持这些,因此行为与那里的MAP_PRIVATE相同。

  3. 文件,MAP_PRIVATE,PROT_READ / PROT_EXEC,!PROT_WRITE

    在MMU情况下:由从文件读取的页面支持的VM区域;对底层文件的更改会反映在映射中;跨fork复制。

    在无MMU情况下

    • 如果存在,则内核将重用对同一文件的同一段的现有映射(如果该映射具有兼容的权限),即使该映射是由另一个进程创建的。

    • 如果可能,如果后备设备具有NOMMU_MAP_DIRECT功能和适当的映射保护功能,则文件映射将直接在后备设备上进行。 Ramfs、romfs、cramfs 和 mtd 可能都允许这样做。

    • 如果后备设备不能或不允许直接共享,但确实具有NOMMU_MAP_COPY功能,那么会将文件的相应位复制到一块连续的内存中,并且将清除超出EOF的任何多余空间

    • 对文件的写入不会影响映射;对映射的写入在其他进程中可见(没有MMU保护),但不应该发生。

  4. 文件,MAP_PRIVATE,PROT_READ / PROT_EXEC,PROT_WRITE

    在MMU情况下:类似于非PROT_WRITE情况,除了在实际发生写入之前,相关页面会被复制。 从那时起,对该页面下方的文件的写入不再反映到映射的支持页面中。 然后该页面由交换空间支持。

    在无MMU情况下:其工作方式与非PROT_WRITE情况非常相似,只是始终进行复制并且永远不会共享。

  5. 常规文件/块设备,MAP_SHARED,PROT_READ / PROT_EXEC / PROT_WRITE

    在MMU情况下:由从文件读取的页面支持的VM区域;写入到页面的更改将写回文件;写入文件反映到支持映射的页面中;跨fork共享。

    在无MMU情况下:不支持。

  6. 内存支持的常规文件,MAP_SHARED,PROT_READ / PROT_EXEC / PROT_WRITE

    在MMU情况下:与普通常规文件相同。

    在无MMU情况下:提供内存支持文件(如ramfs或tmpfs)的文件系统可以选择通过提供要映射的连续页面序列来执行打开、截断、mmap序列。 在这种情况下,可以进行共享可写内存映射。 它将像MMU情况一样工作。 如果文件系统不提供任何此类支持,则将拒绝映射请求。

  7. 内存支持的块设备,MAP_SHARED,PROT_READ / PROT_EXEC / PROT_WRITE

    在MMU情况下:与普通常规文件相同。

    在无MMU情况下:与内存支持的常规文件相同,但块设备必须能够提供连续的页面运行,而无需调用截断。 如果ramdisk驱动程序预先将其所有内存分配为连续数组,则可以这样做。

  8. 内存支持的字符设备,MAP_SHARED,PROT_READ / PROT_EXEC / PROT_WRITE

    在MMU情况下:与普通常规文件相同。

    在无MMU情况下:如果字符设备驱动程序提供了可以直接访问的内存或准内存,则可以选择通过提供对底层设备的直接访问来执行 mmap()。 这样的示例包括帧缓冲区和闪存设备。 如果驱动程序不提供任何此类支持,则将拒绝映射请求。

关于无MMU MMAP的进一步说明

  1. 对文件的私有映射的请求可能会返回一个未按页面对齐的缓冲区。 这是因为可能会发生XIP,并且数据在后备存储中可能未按页面对齐。

  2. 对匿名映射的请求将始终按页面对齐。 如果可能,请求的大小应为2的幂,否则可能会浪费一些空间,因为内核必须分配2的幂的粒度,但只有在适当配置后才会丢弃多余的空间,因为这会对碎片产生影响。

  3. 根据Linux手册页(版本2.22或更高版本),匿名映射请求分配的内存通常在返回之前由内核清除。

    在MMU情况下,可以通过合理的性能来实现这一点,因为区域由虚拟页面支持,只有在对该特定页面发生写入时,其内容才会被映射到清除的物理页面(在此之前,这些页面会有效地映射到可以从中读取内容的全局零页面)。 这会分散初始化页面内容所需的时间 - 取决于映射的写入使用情况。

    但是,在无MMU情况下,匿名映射由物理页面支持,并且在分配时清除整个映射。 这可能会在用户空间malloc()期间导致重大延迟,因为C库进行匿名映射,然后内核对整个映射执行memset。

    但是,对于不需要预先清除的内存(例如malloc()返回的内存),mmap()可以采用MAP_UNINITIALIZED标志来指示内核在返回内存之前不应清除内存。 请注意,必须启用CONFIG_MMAP_ALLOW_UNINITIALIZED才能允许这样做,否则将忽略该标志。

    uClibc使用它来加速malloc(),并且ELF-FDPIC binfmt使用它来分配brk和堆栈区域。

  4. 在无MMU模式下,通过/proc/maps可以看到系统上所有私有复制和匿名映射的列表。

  5. 在无MMU模式下,通过/proc/<pid>/maps可以看到进程使用的所有映射的列表。

  6. 提供MAP_FIXED或请求特定的映射地址将导致错误。

  7. 私有映射的文件通常需要驱动程序或文件系统提供读取方法,以便在 mmap() 选择不直接映射后备设备时,可以将内容读取到分配的内存中。如果它们不提供读取方法,则会产生错误。这种情况最可能发生在字符设备文件、管道、FIFO 和套接字上。

进程间共享内存

在 NOMMU 模式下支持 SYSV IPC SHM 共享内存和 POSIX 共享内存。前者通过通常的机制实现,后者通过在 ramfs 或 tmpfs 挂载点上创建的文件实现。

Futexes

如果架构支持,则在 NOMMU 模式下支持 Futexes。如果传递给 futex 系统调用的地址位于进程进行的映射之外,或者地址所在的映射不支持 futex(例如 I/O 字符设备映射),则会报错。

No-MMU mremap

部分支持 mremap() 函数。它可以更改映射的大小,并且如果指定了 MREMAP_MAYMOVE,并且映射的新大小超过了映射所引用的内存当前占用的 slab 对象的大小,或者可以使用较小的 slab 对象,则可以移动它 [1]

不支持 MREMAP_FIXED,但如果没有地址更改并且不需要移动对象,则会忽略它。

共享映射可能不会被移动。可共享的映射也可能不会被移动,即使它们目前没有被共享。

必须为 mremap() 函数提供与先前映射的对象的基地址和大小完全匹配的值。它不能用于在现有映射中创建空洞,移动现有映射的部分或调整映射部分的大小。它必须对完整的映射起作用。

提供可共享字符设备支持

要提供可共享的字符设备支持,驱动程序必须提供 file->f_op->get_unmapped_area() 操作。mmap() 例程将调用此操作来获取建议的映射地址。如果它不想满足该映射(因为它太长、偏移量奇怪、在某些不受支持的标志组合下等等),则可能会返回错误。

驱动程序还应提供后备设备信息,并设置功能以指示此类设备上允许的映射类型。默认情况下假定为可读写、不可执行且只能直接共享(不能复制)。

将调用 file->f_op->mmap() 操作来实际启动映射。可以在那时拒绝它。如果指定了 NOMMU_MAP_COPY,则返回 ENOSYS 错误将导致改为复制映射。

当删除字符设备上的最后一个映射时,将调用 vm_ops->close() 例程。如果可能,将共享现有映射(部分或全部),而无需通知驱动程序。

file->f_op->get_unmapped_area() 操作也允许返回 -ENOSYS。这将表示此操作只是不想处理它,尽管它有一个操作。例如,它可能会尝试将调用定向到另一个驱动程序,但该驱动程序最终没有实现它。例如,帧缓冲区驱动程序尝试将调用定向到特定于设备的驱动程序就是这种情况。在这种情况下,如果没有指定 NOMMU_MAP_COPY,则将拒绝映射请求,否则将映射副本。

重要

某些类型的设备在某些模式下呈现给查看它们的人的外观可能不同。闪存芯片可能就是这样;例如,如果它们处于编程或擦除模式,您可能会在映射中看到状态的反映,而不是数据。

在这种情况下,必须小心,以免用户空间在驱动程序正忙于控制设备时看到显示此类信息的共享或私有映射。尤其要记住:在某些情况下,私有可执行映射仍然可以直接从设备映射!

提供可共享内存支持的文件支持

在内存支持的文件上提供共享映射类似于提供对共享映射字符设备的支持。主要区别在于,提供服务的文件系统可能会分配一个连续的页面集合并允许在该集合上进行映射。

建议将对空文件执行的增加文件大小的截断操作视为请求收集足够的页面以满足映射。这是支持 POSIX 共享内存所必需的。

内存支持的设备通过映射的后备设备信息设置了 memory_backed 标志来指示。

提供可共享块设备支持

在块设备文件上提供共享映射与字符设备完全相同。如果下面没有真正的设备,则驱动程序应分配足够的连续内存以满足任何支持的映射。

调整页面修剪行为

NOMMU mmap 在执行分配时会自动向上舍入到最接近的 2 的幂的页面数。这可能会对内存碎片产生不利影响,因此将其设置为可配置。默认行为是积极修剪分配并将任何多余的页面丢弃回页面分配器。为了保持对碎片更精细的控制,可以完全禁用此行为,或者将其提升到开始修剪的更高页面水位线。

页面修剪行为可以通过 sysctl vm.nr_trim_pages 进行配置。